diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js index b5a12da0b..1c75927fc 100644 --- a/www/common/common-messaging.js +++ b/www/common/common-messaging.js @@ -89,7 +89,7 @@ define([ }; /* Used to accept friend requests within apps other than /contacts/ */ - Msg.addDirectMessageHandler = function (cfg) { + Msg.addDirectMessageHandler = function (cfg, href) { var network = cfg.network; var proxy = cfg.proxy; if (!network) { return void console.error('Network not ready'); } @@ -97,13 +97,12 @@ define([ var msg; if (sender === network.historyKeeper) { return; } try { - var parsed = Hash.parsePadUrl(window.location.href); + var parsed = Hash.parsePadUrl(href); + var secret = Hash.getSecrets(parsed.type, parsed.hash); if (!parsed.hashData) { return; } - var chan = Hash.hrefToHexChannelId(window.location.href); + var chan = secret.channel; // Decrypt - var keyStr = parsed.hashData.key; - var cryptor = Crypto.createEditCryptor(keyStr); - var key = cryptor.cryptKey; + var key = secret.keys ? secret.keys.cryptKey : Hash.decodeBase64(secret.key); var decryptMsg; try { decryptMsg = Crypto.decrypt(message, key); @@ -197,15 +196,14 @@ define([ var network = cfg.network; var netfluxId = data.netfluxId; var parsed = Hash.parsePadUrl(data.href); + var secret = Hash.getSecrets(parsed.type, parsed.hash); if (!parsed.hashData) { return; } // Message - var chan = Hash.hrefToHexChannelId(data.href); + var chan = secret.channel; var myData = createData(cfg.proxy); var msg = ["FRIEND_REQ", chan, myData]; // Encryption - var keyStr = parsed.hashData.key; - var cryptor = Crypto.createEditCryptor(keyStr); - var key = cryptor.cryptKey; + var key = secret.keys ? secret.keys.cryptKey : Hash.decodeBase64(secret.key); var msgStr = Crypto.encrypt(JSON.stringify(msg), key); // Send encrypted message if (pendingRequests.indexOf(netfluxId) === -1) { diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 001358651..46ad72026 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -7,25 +7,27 @@ define([ '/common/common-constants.js', '/common/common-feedback.js', '/common/outer/local-store.js', - '/common/outer/store-rpc.js', + '/common/outer/worker-channel.js', '/customize/application_config.js', '/bower_components/nthen/index.js', ], function (Config, Messages, Util, Hash, - Messaging, Constants, Feedback, LocalStore, AStore, + Messaging, Constants, Feedback, LocalStore, Channel, AppConfig, Nthen) { - /* This file exposes functionality which is specific to Cryptpad, but not to any particular pad type. This includes functions for committing metadata about pads to your local storage for future use and improved usability. Additionally, there is some basic functionality for import/export. */ - var postMessage = function (cmd, data, cb) { - setTimeout(function () { + var urlArgs = Util.find(Config, ['requireConf', 'urlArgs']) || ''; + + var postMessage = function (/*cmd, data, cb*/) { + /*setTimeout(function () { AStore.query(cmd, data, cb); - }); + });*/ + console.error('NOT_READY'); }; var tryParsing = function (x) { try { return JSON.parse(x); } @@ -531,6 +533,11 @@ define([ var messaging = common.messaging = {}; messaging.onFriendRequest = Util.mkEvent(); messaging.onFriendComplete = Util.mkEvent(); + messaging.addHandlers = function (href) { + postMessage("ADD_DIRECT_MESSAGE_HANDLERS", { + href: href + }); + }; // Messenger var messenger = common.messenger = {}; @@ -570,8 +577,8 @@ define([ // Pad RPC var pad = common.padRpc = {}; - pad.joinPad = function (data, cb) { - postMessage("JOIN_PAD", data, cb); + pad.joinPad = function (data) { + postMessage("JOIN_PAD", data); }; pad.sendPadMsg = function (data, cb) { postMessage("SEND_PAD_MSG", data, cb); @@ -581,6 +588,7 @@ define([ pad.onJoinEvent = Util.mkEvent(); pad.onLeaveEvent = Util.mkEvent(); pad.onDisconnectEvent = Util.mkEvent(); + pad.onConnectEvent = Util.mkEvent(); pad.onErrorEvent = Util.mkEvent(); // Loading events @@ -669,108 +677,57 @@ define([ window.location.href = '/login/'; }; - common.startAccountDeletion = function (cb) { + common.startAccountDeletion = function (data, cb) { // Logout other tabs LocalStore.logout(null, true); cb(); }; - var onMessage = function (cmd, data, cb) { - cb = cb || function () {}; - switch (cmd) { - case 'REQUEST_LOGIN': { - requestLogin(); - break; - } - case 'UPDATE_METADATA': { - common.changeMetadata(); - break; - } - case 'UPDATE_TOKEN': { - var localToken = tryParsing(localStorage.getItem(Constants.tokenKey)); - if (localToken !== data.token) { requestLogin(); } - break; - } - case 'Q_FRIEND_REQUEST': { - common.messaging.onFriendRequest.fire(data, cb); - break; - } - case 'EV_FRIEND_COMPLETE': { - common.messaging.onFriendComplete.fire(data); - break; - } - // Network - case 'NETWORK_DISCONNECT': { - common.onNetworkDisconnect.fire(); break; - } - case 'NETWORK_RECONNECT': { - require(['/api/config?' + (+new Date())], function (NewConfig) { - var update = updateLocalVersion(NewConfig.requireConf && NewConfig.requireConf.urlArgs); - if (update) { - postMessage('DISCONNECT'); - return; - } - common.onNetworkReconnect.fire(data); - }); - break; - } - // Messenger - case 'CONTACTS_MESSAGE': { - common.messenger.onMessageEvent.fire(data); break; - } - case 'CONTACTS_JOIN': { - common.messenger.onJoinEvent.fire(data); break; - } - case 'CONTACTS_LEAVE': { - common.messenger.onLeaveEvent.fire(data); break; - } - case 'CONTACTS_UPDATE': { - common.messenger.onUpdateEvent.fire(data); break; - } - case 'CONTACTS_FRIEND': { - common.messenger.onFriendEvent.fire(data); break; - } - case 'CONTACTS_UNFRIEND': { - common.messenger.onUnfriendEvent.fire(data); break; - } - // Pad - case 'PAD_READY': { - common.padRpc.onReadyEvent.fire(); break; - } - case 'PAD_MESSAGE': { - common.padRpc.onMessageEvent.fire(data); break; - } - case 'PAD_JOIN': { - common.padRpc.onJoinEvent.fire(data); break; - } - case 'PAD_LEAVE': { - common.padRpc.onLeaveEvent.fire(data); break; - } - case 'PAD_DISCONNECT': { - common.padRpc.onDisconnectEvent.fire(data); break; - } - case 'PAD_ERROR': { - common.padRpc.onErrorEvent.fire(data); break; - } - // Drive - case 'DRIVE_LOG': { - common.drive.onLog.fire(data); break; - } - case 'DRIVE_CHANGE': { - common.drive.onChange.fire(data); break; - } - case 'DRIVE_REMOVE': { - common.drive.onRemove.fire(data); break; - } - // Account deletion - case 'DELETE_ACCOUNT': { - common.startAccountDeletion(cb); break; - } - // Loading - case 'LOADING_DRIVE': { - common.loading.onDriveEvent.fire(data); break; - } - } + var queries = { + REQUEST_LOGIN: requestLogin, + UPDATE_METADATA: common.changeMetadata, + UPDATE_TOKEN: function (data) { + var localToken = tryParsing(localStorage.getItem(Constants.tokenKey)); + if (localToken !== data.token) { requestLogin(); } + }, + // Messaging + Q_FRIEND_REQUEST: common.messaging.onFriendRequest.fire, + EV_FIREND_COMPLETE: common.messaging.onFriendComplete.fire, + // Network + NETWORK_DISCONNECT: common.onNetworkDisconnect.fire, + NETWORK_RECONNECT: function (data) { + require(['/api/config?' + (+new Date())], function (NewConfig) { + var update = updateLocalVersion(NewConfig.requireConf && NewConfig.requireConf.urlArgs); + if (update) { + postMessage('DISCONNECT'); + return; + } + common.onNetworkReconnect.fire(data); + }); + }, + // Messenger + CONTACTS_MESSAGE: common.messenger.onMessageEvent.fire, + CONTACTS_JOIN: common.messenger.onJoinEvent.fire, + CONTACTS_LEAVE: common.messenger.onLeaveEvent.fire, + CONTACTS_UPDATE: common.messenger.onUpdateEvent.fire, + CONTACTS_FRIEND: common.messenger.onFriendEvent.fire, + CONTACTS_UNFRIEND: common.messenger.onUnfriendEvent.fire, + // Pad + PAD_READY: common.padRpc.onReadyEvent.fire, + PAD_MESSAGE: common.padRpc.onMessageEvent.fire, + PAD_JOIN: common.padRpc.onJoinEvent.fire, + PAD_LEAVE: common.padRpc.onLeaveEvent.fire, + PAD_DISCONNECT: common.padRpc.onDisconnectEvent.fire, + PAD_CONNECT: common.padRpc.onConnectEvent.fire, + PAD_ERROR: common.padRpc.onErrorEvent.fire, + // Drive + DRIVE_LOG: common.drive.onLog.fire, + DRIVE_CHANGE: common.drive.onChange.fire, + DRIVE_REMOVE: common.drive.onRemove.fire, + // Account deletion + DELETE_ACCOUNT: common.startAccountDeletion, + // Loading + LOADING_DRIVE: common.loading.onDriveEvent.fire }; common.ready = (function () { @@ -828,43 +785,166 @@ define([ } }).nThen(function (waitFor) { var cfg = { - query: onMessage, // TODO temporary, will be replaced by a webworker channel + init: true, + //query: onMessage, // TODO temporary, will be replaced by a webworker channel userHash: LocalStore.getUserHash(), anonHash: LocalStore.getFSHash(), localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), language: common.getLanguage(), - messenger: rdyCfg.messenger, - driveEvents: rdyCfg.driveEvents + messenger: rdyCfg.messenger, // Boolean + driveEvents: rdyCfg.driveEvents // Boolean }; if (sessionStorage[Constants.newPadPathKey]) { cfg.initialPath = sessionStorage[Constants.newPadPathKey]; delete sessionStorage[Constants.newPadPathKey]; } - AStore.query("CONNECT", cfg, waitFor(function (data) { - if (data.error) { throw new Error(data.error); } - if (data.state === 'ALREADY_INIT') { - data = data.returned; - } - - if (data.anonHash && !cfg.userHash) { LocalStore.setFSHash(data.anonHash); } - /*if (cfg.userHash && sessionStorage) { - // copy User_hash into sessionStorage because cross-domain iframes - // on safari replaces localStorage with sessionStorage or something - sessionStorage.setItem(Constants.userHashKey, cfg.userHash); - }*/ - - if (cfg.userHash) { - var localToken = tryParsing(localStorage.getItem(Constants.tokenKey)); - if (localToken === null) { - // if that number hasn't been set to localStorage, do so. - localStorage.setItem(Constants.tokenKey, data[Constants.tokenKey]); - } + var channelIsReady = waitFor(); + + var msgEv = Util.mkEvent(); + var postMsg, worker; + Nthen(function (waitFor2) { + if (typeof(SharedWorker) !== "undefined") { + worker = new SharedWorker('/common/outer/sharedworker.js?' + urlArgs); + worker.onerror = function (e) { + console.error(e); + }; + worker.port.onmessage = function (ev) { + if (ev.data === "SW_READY") { + return; + } + msgEv.fire(ev); + }; + postMsg = function (data) { + worker.port.postMessage(data); + }; + postMsg('INIT'); + + window.addEventListener('beforeunload', function () { + postMsg('CLOSE'); + }); + } else if (false && 'serviceWorker' in navigator) { + var initializing = true; + var stopWaiting = waitFor2(); // Call this function when we're ready + + postMsg = function (data) { + if (worker) { return void worker.postMessage(data); } + }; + + navigator.serviceWorker.register('/common/outer/serviceworker.js?' + urlArgs, {scope: '/'}) + .then(function(reg) { + // Add handler for receiving messages from the service worker + navigator.serviceWorker.addEventListener('message', function (ev) { + if (initializing && ev.data === "SW_READY") { + initializing = false; + } else { + msgEv.fire(ev); + } + }); + + // Initialize the worker + // If it is active (probably running in another tab), just post INIT + if (reg.active) { + worker = reg.active; + postMsg("INIT"); + } + // If it was not active, wait for the "activated" state and post INIT + reg.onupdatefound = function () { + if (initializing) { + var w = reg.installing; + var onStateChange = function () { + if (w.state === "activated") { + worker = w; + postMsg("INIT"); + w.removeEventListener("statechange", onStateChange); + } + }; + w.addEventListener('statechange', onStateChange); + return; + } + // XXX + // New version detected (from another tab): kill? + console.error('New version detected: ABORT?'); + }; + return void stopWaiting(); + }).catch(function(error) { + /**/console.log('Registration failed with ' + error); + }); + + window.addEventListener('beforeunload', function () { + postMsg('CLOSE'); + }); + } else if (Worker) { + worker = new Worker('/common/outer/webworker.js?' + urlArgs); + worker.onmessage = function (ev) { + msgEv.fire(ev); + }; + postMsg = function (data) { + worker.postMessage(data); + }; + } else { + require(['/common/outer/noworker.js'], waitFor2(function (NoWorker) { + NoWorker.onMessage(function (data) { + msgEv.fire({data: data}); + }); + postMsg = function (d) { setTimeout(function () { NoWorker.query(d); }); }; + NoWorker.create(); + })); } + }).nThen(function () { + Channel.create(msgEv, postMsg, function (chan) { + console.log('Outer ready'); + Object.keys(queries).forEach(function (q) { + chan.on(q, function (data, cb) { + try { + queries[q](data, cb); + } catch (e) { + console.error("Error in outer when executing query " + q); + console.error(e); + console.log(data); + } + }); + }); + + postMessage = function (cmd, data, cb) { + cb = cb || function () {}; + chan.query(cmd, data, function (err, data) { + if (err) { return void cb ({error: err}); } + cb(data); + }); + }; + + console.log('Posting CONNECT'); + postMessage('CONNECT', cfg, function (data) { + if (data.error) { throw new Error(data.error); } + if (data.state === 'ALREADY_INIT') { + data = data.returned; + } + + if (data.anonHash && !cfg.userHash) { LocalStore.setFSHash(data.anonHash); } + + /*if (cfg.userHash && sessionStorage) { + // copy User_hash into sessionStorage because cross-domain iframes + // on safari replaces localStorage with sessionStorage or something + sessionStorage.setItem(Constants.userHashKey, cfg.userHash); + }*/ + + if (cfg.userHash) { + var localToken = tryParsing(localStorage.getItem(Constants.tokenKey)); + if (localToken === null) { + // if that number hasn't been set to localStorage, do so. + localStorage.setItem(Constants.tokenKey, data[Constants.tokenKey]); + } + } + + initFeedback(data.feedback); + initialized = true; + channelIsReady(); + }); + + }, false); + }); - initFeedback(data.feedback); - initialized = true; - })); }).nThen(function (waitFor) { // Load the new pad when the hash has changed var oldHref = document.location.href; @@ -893,9 +973,12 @@ define([ document.location.reload(); } else if (o && !n) { LocalStore.logout(); - postMessage("DISCONNECT"); } }); + LocalStore.onLogout(function () { + console.log('onLogout: disconnect'); + postMessage("DISCONNECT"); + }); if (PINNING_ENABLED && LocalStore.isLoggedIn()) { console.log("logged in. pads will be pinned"); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 680712f17..22fe9cbe1 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -23,1276 +23,1441 @@ define([ Crypto, ChainPad, Listmap, nThen, Saferphore) { var Store = {}; - var postMessage = function () {}; + var create = function () { + var postMessage = function () {}; + var broadcast = function () {}; + var sendDriveEvent = function () {}; - var storeHash; + var storeHash; - var store = window.CryptPad_AsyncStore = {}; + var store = window.CryptPad_AsyncStore = {}; - var onSync = function (cb) { - Realtime.whenRealtimeSyncs(store.realtime, cb); - }; + var onSync = function (cb) { + Realtime.whenRealtimeSyncs(store.realtime, cb); + }; - Store.get = function (key, cb) { - cb(Util.find(store.proxy, key)); - }; - Store.set = function (data, cb) { - var path = data.key.slice(); - var key = path.pop(); - var obj = Util.find(store.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; - } - onSync(cb); - }; + Store.get = function (clientId, key, cb) { + cb(Util.find(store.proxy, key)); + }; + Store.set = function (clientId, data, cb) { + var path = data.key.slice(); + var key = path.pop(); + var obj = Util.find(store.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; + } + broadcast([clientId], "UPDATE_METADATA"); + onSync(cb); + }; - Store.hasSigningKeys = function () { - if (!store.proxy) { return; } - return typeof(store.proxy.edPrivate) === 'string' && - typeof(store.proxy.edPublic) === 'string'; - }; + 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'; - }; + Store.hasCurveKeys = function () { + if (!store.proxy) { return; } + return typeof(store.proxy.curvePrivate) === 'string' && + typeof(store.proxy.curvePublic) === 'string'; + }; - var getUserChannelList = function () { - // start with your userHash... - var userHash = storeHash; - if (!userHash) { return null; } - - // No password for drive - var secret = Hash.getSecrets('drive', userHash); - var userChannel = secret.channel; - 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 (you should - // not pin these pads) - var files = store.userObject.getFiles([store.userObject.FILES_DATA]); - var edPublic = store.proxy.edPublic; - var list = files.map(function (id) { - var d = store.userObject.getFileData(id); - if (d.owners && d.owners.length && edPublic && - d.owners.indexOf(edPublic) === -1) { return; } - return d.channel; - }) - .filter(function (x) { return x; }); - - // Get the avatar - 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.friends) { - var fList = Messaging.getFriendChannelsList(store.proxy); - list = list.concat(fList); - } - - list.push(userChannel); - list.sort(); - - return list; - }; + var getUserChannelList = function () { + // start with your userHash... + var userHash = storeHash; + if (!userHash) { return null; } + + // No password for drive + var secret = Hash.getSecrets('drive', userHash); + var userChannel = secret.channel; + if (!userChannel) { return null; } - var getExpirableChannelList = function () { - var list = []; - store.userObject.getFiles([store.userObject.FILES_DATA]).forEach(function (id) { - var data = store.userObject.getFileData(id); + // 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 (you should + // not pin these pads) + var files = store.userObject.getFiles([store.userObject.FILES_DATA]); var edPublic = store.proxy.edPublic; + var list = files.map(function (id) { + var d = store.userObject.getFileData(id); + if (d.owners && d.owners.length && edPublic && + d.owners.indexOf(edPublic) === -1) { return; } + return d.channel; + }) + .filter(function (x) { return x; }); + + // Get the avatar + 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); } + } - // Push channels owned by someone else or channel that should have expired - // because of the expiration time - if ((data.owners && data.owners.length && data.owners.indexOf(edPublic) === -1) || - (data.expire && data.expire < (+new Date()))) { - list.push(data.channel); + if (store.proxy.friends) { + var fList = Messaging.getFriendChannelsList(store.proxy); + list = list.concat(fList); } - }); - return list; - }; - var getCanonicalChannelList = function (expirable) { - var list = expirable ? getExpirableChannelList() : getUserChannelList(); - return Util.deduplicateString(list).sort(); - }; + list.push(userChannel); + list.sort(); + + return list; + }; - ////////////////////////////////////////////////////////////////// - /////////////////////// RPC ////////////////////////////////////// - ////////////////////////////////////////////////////////////////// + var getExpirableChannelList = function () { + var list = []; + store.userObject.getFiles([store.userObject.FILES_DATA]).forEach(function (id) { + var data = store.userObject.getFileData(id); + var edPublic = store.proxy.edPublic; + + // Push channels owned by someone else or channel that should have expired + // because of the expiration time + if ((data.owners && data.owners.length && data.owners.indexOf(edPublic) === -1) || + (data.expire && data.expire < (+new Date()))) { + list.push(data.channel); + } + }); + return list; + }; - Store.pinPads = function (data, cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - if (typeof(cb) !== 'function') { - console.error('expected a callback'); - } + var getCanonicalChannelList = function (expirable) { + var list = expirable ? getExpirableChannelList() : getUserChannelList(); + return Util.deduplicateString(list).sort(); + }; - store.rpc.pin(data, function (e, hash) { - if (e) { return void cb({error: e}); } - cb({hash: hash}); - }); - }; + ////////////////////////////////////////////////////////////////// + /////////////////////// RPC ////////////////////////////////////// + ////////////////////////////////////////////////////////////////// - Store.unpinPads = function (data, cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + Store.pinPads = function (clientId, data, cb) { + if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + if (typeof(cb) !== 'function') { + console.error('expected a callback'); + } - store.rpc.unpin(data, function (e, hash) { - if (e) { return void cb({error: e}); } - cb({hash: hash}); - }); - }; + store.rpc.pin(data, function (e, hash) { + if (e) { return void cb({error: e}); } + cb({hash: hash}); + }); + }; - var account = {}; + Store.unpinPads = function (clientId, data, cb) { + if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - Store.getPinnedUsage = function (data, cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + store.rpc.unpin(data, function (e, hash) { + if (e) { return void cb({error: e}); } + cb({hash: hash}); + }); + }; - store.rpc.getFileListSize(function (err, bytes) { - if (typeof(bytes) === 'number') { - account.usage = bytes; - } - cb({bytes: bytes}); - }); - }; + var account = {}; - // Update for all users from accounts and return current user limits - Store.updatePinLimit = function (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 (data, cb) { - if (!store.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 store.rpc.getLimit(function (e, limit, plan, note) { + Store.getPinnedUsage = function (clientId, data, cb) { + if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + + store.rpc.getFileListSize(function (err, bytes) { + if (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); }); - } - cb(account); - }; + }; + // Get current user limits + Store.getPinLimit = function (clientId, data, cb) { + if (!store.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 store.rpc.getLimit(function (e, limit, plan, note) { + if (e) { return void cb({error: e}); } + account.limit = limit; + account.plan = plan; + account.note = note; + cb(account); + }); + } + cb(account); + }; - Store.clearOwnedChannel = function (data, cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - store.rpc.clearOwnedChannel(data, function (err) { - cb({error:err}); - }); - }; + 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 (data, cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - store.rpc.removeOwnedChannel(data, function (err) { - cb({error:err}); - }); - }; + Store.removeOwnedChannel = function (clientId, data, cb) { + if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + store.rpc.removeOwnedChannel(data, function (err) { + cb({error:err}); + }); + }; - var arePinsSynced = function (cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + 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 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 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); - }); - }; + var list = getCanonicalChannelList(false); + store.rpc.reset(list, function (e, hash) { + if (e) { return void cb(e); } + cb(null, hash); + }); + }; - Store.uploadComplete = function (data, cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - if (data.owned) { - // Owned file - store.rpc.ownedUploadComplete(data.id, function (err, res) { + Store.uploadComplete = function (clientId, data, cb) { + if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + if (data.owned) { + // Owned file + store.rpc.ownedUploadComplete(data.id, function (err, res) { + if (err) { return void cb({error:err}); } + cb(res); + }); + return; + } + // Normal upload + store.rpc.uploadComplete(data.id, function (err, res) { if (err) { return void cb({error:err}); } cb(res); }); - return; - } - // Normal upload - store.rpc.uploadComplete(data.id, function (err, res) { - if (err) { return void cb({error:err}); } - cb(res); - }); - }; + }; - Store.uploadStatus = function (data, cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - store.rpc.uploadStatus(data.size, function (err, res) { - if (err) { return void cb({error:err}); } - cb(res); - }); - }; + Store.uploadStatus = function (clientId, data, cb) { + if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + store.rpc.uploadStatus(data.size, function (err, res) { + if (err) { return void cb({error:err}); } + cb(res); + }); + }; - Store.uploadCancel = function (data, cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - store.rpc.uploadCancel(data.size, function (err, res) { - if (err) { return void cb({error:err}); } - cb(res); - }); - }; + Store.uploadCancel = function (clientId, data, cb) { + if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + store.rpc.uploadCancel(data.size, function (err, res) { + if (err) { return void cb({error:err}); } + cb(res); + }); + }; - Store.uploadChunk = function (data, cb) { - store.rpc.send.unauthenticated('UPLOAD', data.chunk, function (e, msg) { - cb({ - error: e, - msg: msg + Store.uploadChunk = function (clientId, data, cb) { + store.rpc.send.unauthenticated('UPLOAD', data.chunk, function (e, msg) { + cb({ + error: e, + msg: msg + }); }); - }); - }; + }; - Store.initRpc = function (data, cb) { - require(['/common/pinpad.js'], function (Pinpad) { - Pinpad.create(store.network, store.proxy, function (e, call) { - if (e) { return void cb({error: e}); } + Store.initRpc = function (clientId, data, cb) { + if (store.rpc) { return void cb(account); } + require(['/common/pinpad.js'], function (Pinpad) { + Pinpad.create(store.network, store.proxy, function (e, call) { + if (e) { return void cb({error: e}); } - store.rpc = call; + store.rpc = call; - Store.getPinLimit(null, function (obj) { - if (obj.error) { console.error(obj.error); } - account.limit = obj.limit; - account.plan = obj.plan; - account.note = obj.note; - cb(obj); - }); + 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); + }); - arePinsSynced(function (err, yes) { - if (!yes) { - resetPins(function (err) { - if (err) { return console.error(err); } - console.log('RESET DONE'); - }); - } + arePinsSynced(function (err, yes) { + if (!yes) { + resetPins(function (err) { + if (err) { return console.error(err); } + console.log('RESET DONE'); + }); + } + }); }); }); - }); - }; - - ////////////////////////////////////////////////////////////////// - ////////////////// ANON RPC ////////////////////////////////////// - ////////////////////////////////////////////////////////////////// - Store.anonRpcMsg = function (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 (data, cb) { - if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); } + ////////////////////////////////////////////////////////////////// + ////////////////// 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); + }); + }; - var channelId = 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') { - return void cb({size: response[0]}); - } else { - cb({error: 'INVALID_RESPONSE'}); - } - }); - }; + Store.getFileSize = function (clientId, data, cb) { + if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); } - Store.isNewChannel = function (data, cb) { - if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); } - var channelId = 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') { - return void cb({ - isNew: response[0] - }); - } else { - cb({error: 'INVALID_RESPONSE'}); - } - }); - }; + var channelId = 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') { + return void cb({size: response[0]}); + } else { + cb({error: 'INVALID_RESPONSE'}); + } + }); + }; - Store.getMultipleFileSize = function (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.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); + 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') { + return void cb({ + isNew: response[0] + }); + } else { + cb({error: 'INVALID_RESPONSE'}); + } + }); + }; - 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.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.getDeletedPads = function (data, cb) { - if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); } - var 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'}); + 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 = getCanonicalChannelList(true); + if (!Array.isArray(list)) { + return void cb({error: 'INVALID_FILE_LIST'}); } - }); - }; - Store.initAnonRpc = function (data, cb) { - require([ - '/common/rpc.js', - ], function (Rpc) { - Rpc.createAnonymous(store.network, function (e, call) { + store.anon_rpc.send('GET_DELETED_PADS', list, function (e, res) { if (e) { return void cb({error: e}); } - store.anon_rpc = call; - cb(); + if (res && res.length && Array.isArray(res[0])) { + cb(res[0]); + } else { + cb({error: 'UNEXPECTED_RESPONSE'}); + } }); - }); - }; + }; - ////////////////////////////////////////////////////////////////// - /////////////////////// Store //////////////////////////////////// - ////////////////////////////////////////////////////////////////// - - // Get the metadata for sframe-common-outer - Store.getMetadata = function (data, cb) { - var disableThumbnails = Util.find(store.proxy, ['settings', 'general', 'disableThumbnails']); - 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']), - curvePublic: store.proxy.curvePublic, - }, - // "priv" is not shared with other users but is needed by the apps - priv: { - edPublic: store.proxy.edPublic, - friends: store.proxy.friends || {}, - settings: store.proxy.settings, - thumbnails: disableThumbnails === false - } + Store.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(); + }); + }); }; - cb(JSON.parse(JSON.stringify(metadata))); - }; - var makePad = function (href, title) { - var now = +new Date(); - return { - href: href, - atime: now, - ctime: now, - title: title || Hash.getDefaultName(Hash.parsePadUrl(href)), + ////////////////////////////////////////////////////////////////// + /////////////////////// Store //////////////////////////////////// + ////////////////////////////////////////////////////////////////// + + // Get the metadata for sframe-common-outer + Store.getMetadata = function (clientId, data, cb) { + var disableThumbnails = Util.find(store.proxy, ['settings', 'general', 'disableThumbnails']); + 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']), + curvePublic: store.proxy.curvePublic, + }, + // "priv" is not shared with other users but is needed by the apps + priv: { + edPublic: store.proxy.edPublic, + friends: store.proxy.friends || {}, + settings: store.proxy.settings, + thumbnails: disableThumbnails === false + } + }; + cb(JSON.parse(JSON.stringify(metadata))); }; - }; - Store.addPad = function (data, cb) { - if (!data.href) { return void cb({error:'NO_HREF'}); } - var pad = makePad(data.href, 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) { pad.channel = data.channel; } - store.userObject.pushData(pad, function (e, id) { - if (e) { return void cb({error: "Error while adding a template:"+ e}); } - var path = data.path || ['root']; - store.userObject.add(id, path); - onSync(cb); - }); - }; + var makePad = function (href, title) { + var now = +new Date(); + return { + href: href, + atime: now, + ctime: now, + title: title || Hash.getDefaultName(Hash.parsePadUrl(href)), + }; + }; - var getOwnedPads = function () { - var list = []; - store.userObject.getFiles([store.userObject.FILES_DATA]).forEach(function (id) { - var data = store.userObject.getFileData(id); - var edPublic = store.proxy.edPublic; + Store.addPad = function (clientId, data, cb) { + if (!data.href) { return void cb({error:'NO_HREF'}); } + var pad = makePad(data.href, 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) { pad.channel = data.channel; } + store.userObject.pushData(pad, function (e, id) { + if (e) { return void cb({error: "Error while adding a template:"+ e}); } + var path = data.path || ['root']; + store.userObject.add(id, path); + sendDriveEvent('DRIVE_CHANGE', { + path: ['drive', UserObject.FILES_DATA] + }, clientId); + onSync(cb); + }); + }; - // Push channels owned by someone else or channel that should have expired - // because of the expiration time - if (data.owners && data.owners.length === 1 && data.owners.indexOf(edPublic) !== -1) { - list.push(data.channel); + var getOwnedPads = function () { + var list = []; + store.userObject.getFiles([store.userObject.FILES_DATA]).forEach(function (id) { + var data = store.userObject.getFileData(id); + var edPublic = store.proxy.edPublic; + + // Push channels owned by someone else or channel that should have expired + // because of the expiration time + if (data.owners && data.owners.length === 1 && data.owners.indexOf(edPublic) !== -1) { + list.push(data.channel); + } + }); + if (store.proxy.todo) { + // No password for todo + list.push(Hash.hrefToHexChannelId('/todo/#' + store.proxy.todo, null)); } - }); - 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)); - } - return list; - }; - var removeOwnedPads = function (waitFor) { - // Delete owned pads - var ownedPads = getOwnedPads(); - var sem = Saferphore.create(10); - ownedPads.forEach(function (c) { - var w = waitFor(); - sem.take(function (give) { - Store.removeOwnedChannel(c, give(function (obj) { - if (obj && obj.error) { console.error(obj.error); } - w(); - })); + if (store.proxy.profile && store.proxy.profile.edit) { + // No password for profile + list.push(Hash.hrefToHexChannelId('/profile/#' + store.proxy.profile.edit, null)); + } + return list; + }; + var removeOwnedPads = function (waitFor) { + // Delete owned pads + var ownedPads = getOwnedPads(); + var sem = Saferphore.create(10); + ownedPads.forEach(function (c) { + var w = waitFor(); + sem.take(function (give) { + Store.removeOwnedChannel(null, c, give(function (obj) { + if (obj && obj.error) { console.error(obj.error); } + w(); + })); + }); }); - }); - }; + }; - Store.deleteAccount = function (data, cb) { - var edPublic = store.proxy.edPublic; - // No password for drive - var secret = Hash.getSecrets('drive', storeHash); - Store.anonRpcMsg({ - msg: 'GET_METADATA', - data: secret.channel - }, function (data) { - var metadata = data[0]; - // Owned drive - if (metadata && metadata.owners && metadata.owners.length === 1 && - metadata.owners.indexOf(edPublic) !== -1) { - nThen(function (waitFor) { - var token = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER); - store.proxy[Constants.tokenKey] = token; - postMessage("DELETE_ACCOUNT", token, 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(secret.channel, waitFor()); - }).nThen(function () { - store.network.disconnect(); - cb({ - state: true + Store.deleteAccount = function (clientId, data, cb) { + var edPublic = store.proxy.edPublic; + // No password for drive + var secret = Hash.getSecrets('drive', storeHash); + Store.anonRpcMsg(clientId, { + msg: 'GET_METADATA', + data: secret.channel + }, function (data) { + var metadata = data[0]; + // Owned drive + if (metadata && metadata.owners && metadata.owners.length === 1 && + metadata.owners.indexOf(edPublic) !== -1) { + nThen(function (waitFor) { + var token = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER); + store.proxy[Constants.tokenKey] = token; + postMessage(clientId, "DELETE_ACCOUNT", token, 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, secret.channel, waitFor()); + }).nThen(function () { + store.network.disconnect(); + cb({ + state: true + }); }); - }); - return; - } + return; + } - // Not owned drive - var toSign = { - intent: 'Please delete my account.' - }; - toSign.drive = secret.channel; - 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); + // Not owned drive + var toSign = { + intent: 'Please delete my account.' + }; + toSign.drive = secret.channel; + 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)); + 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'); } + if (!check) { console.error('signed message failed verification'); } - var proofTxt = Crypto.Nacl.util.encodeBase64(proof); - cb({ - proof: proofTxt, - toSign: JSON.parse(Sortify(toSign)) + 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 (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, - }; - Store.addPad(fileData, cb); + /** + * 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, + }; + Store.addPad(clientId, fileData, cb); + }); }); - }); - }; + }; - /** - * Merge the anonymous drive into the user drive at registration - * data - * - anonHash - */ - Store.migrateAnonDrive = function (data, cb) { - require(['/common/mergeDrive.js'], function (Merge) { - var hash = data.anonHash; - Merge.anonDriveIntoUser(store, hash, cb); - }); - }; + /** + * 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); + }); + }; - var getAttributeObject = function (attr) { - if (typeof attr === "string") { - console.error('DEPRECATED: use setAttribute with an array, not a string'); + 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 { - obj: store.proxy.settings, - key: attr + path: ['settings'].concat(attr), + obj: obj, + key: attr[attr.length-1] }; - } - 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 { - obj: obj, - key: attr[attr.length-1] }; - }; - // Set the display name (username) in the proxy - Store.setDisplayName = function (value, cb) { - store.proxy[Constants.displayNameKey] = value; - onSync(cb); - }; + // Set the display name (username) in the proxy + Store.setDisplayName = function (clientId, value, cb) { + store.proxy[Constants.displayNameKey] = value; + broadcast([clientId], "UPDATE_METADATA"); + onSync(cb); + }; - // Reset the drive part of the userObject (from settings) - Store.resetDrive = function (data, cb) { - nThen(function (waitFor) { - removeOwnedPads(waitFor); - }).nThen(function () { - store.proxy.drive = store.fo.getStructure(); + // 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.fo.getStructure(); + sendDriveEvent('DRIVE_CHANGE', { + path: ['drive', 'filesData'] + }, clientId); + onSync(cb); + }); + }; + + /** + * Settings & pad attributes + * data + * - href (String) + * - attr (Array) + * - value (String) + */ + Store.setPadAttribute = function (clientId, data, cb) { + store.userObject.setPadAttribute(data.href, data.attr, data.value, function () { + sendDriveEvent('DRIVE_CHANGE', { + path: ['drive', UserObject.FILES_DATA] + }, clientId); + onSync(cb); + }); + }; + Store.getPadAttribute = function (clientId, data, cb) { + store.userObject.getPadAttribute(data.href, data.attr, function (err, val) { + if (err) { return void cb({error: err}); } + cb(val); + }); + }; + 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(cb); - }); - }; + }; + 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) { + cb(store.userObject.getTagsList()); + }; - /** - * Settings & pad attributes - * data - * - href (String) - * - attr (Array) - * - value (String) - */ - Store.setPadAttribute = function (data, cb) { - store.userObject.setPadAttribute(data.href, data.attr, data.value, function () { + // Templates + Store.getTemplates = function (clientId, data, cb) { + var templateFiles = store.userObject.getFiles(['template']); + var res = []; + templateFiles.forEach(function (f) { + var data = store.userObject.getFileData(f); + res.push(JSON.parse(JSON.stringify(data))); + }); + cb(res); + }; + Store.incrementTemplateUse = function (clientId, href) { + store.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; + store.userObject.setPadAttribute(href, 'used', used); + }); + }; + + // Pads + Store.moveToTrash = function (clientId, data, cb) { + var href = Hash.getRelativeHref(data.href); + store.userObject.forget(href); + sendDriveEvent('DRIVE_CHANGE', { + path: ['drive', UserObject.FILES_DATA] + }, clientId); onSync(cb); - }); - }; - Store.getPadAttribute = function (data, cb) { - store.userObject.getPadAttribute(data.href, data.attr, function (err, val) { - if (err) { return void cb({error: err}); } - cb(val); - }); - }; - Store.setAttribute = function (data, cb) { - try { - var object = getAttributeObject(data.attr); - object.obj[object.key] = data.value; - } catch (e) { return void cb({error: e}); } - onSync(cb); - }; - Store.getAttribute = function (data, cb) { - var object; - try { - object = getAttributeObject(data.attr); - } catch (e) { return void cb({error: e}); } - cb(object.obj[object.key]); - }; + }; + Store.setPadTitle = function (clientId, data, cb) { + var title = data.title; + var href = data.href; + var channel = data.channel; + var p = Hash.parsePadUrl(href); + var h = p.hashData; - // Tags - Store.listAllTags = function (data, cb) { - cb(store.userObject.getTagsList()); - }; + if (AppConfig.disableAnonymousStore && !store.loggedIn) { return void cb(); } - // Templates - Store.getTemplates = function (data, cb) { - var templateFiles = store.userObject.getFiles(['template']); - var res = []; - templateFiles.forEach(function (f) { - var data = store.userObject.getFileData(f); - res.push(JSON.parse(JSON.stringify(data))); - }); - cb(res); - }; - Store.incrementTemplateUse = function (href) { - store.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; - store.userObject.setPadAttribute(href, 'used', used); - }); - }; + var channelData = Store.channels && Store.channels[channel]; - // Pads - Store.moveToTrash = function (data, cb) { - var href = Hash.getRelativeHref(data.href); - store.userObject.forget(href); - onSync(cb); - }; - Store.setPadTitle = function (data, cb) { - var title = data.title; - var href = data.href; - var channel = data.channel; - var p = Hash.parsePadUrl(href); - var h = p.hashData; - - if (AppConfig.disableAnonymousStore && !store.loggedIn) { return void cb(); } - - var owners; - if (Store.channel && Store.channel.wc && channel === Store.channel.wc.id) { - owners = Store.channel.data.owners || undefined; - } - - var expire; - if (Store.channel && Store.channel.wc && channel === Store.channel.wc.id) { - expire = +Store.channel.data.expire || undefined; - } - - var allPads = Util.find(store.proxy, ['drive', 'filesData']) || {}; - var isStronger; - - // If we don't find the new channel in our existing pads, we'll have to add the pads - // to filesData - var contains; - - // Update all pads that use the same channel but with a weaker hash - // Edit > Edit (present) > View > View (present) - for (var id in allPads) { - var pad = allPads[id]; - if (!pad.href) { continue; } - - var p2 = Hash.parsePadUrl(pad.href); - var h2 = p2.hashData; - - // Different types, proceed to the next one - // No hash data: corrupted pad? - if (p.type !== p2.type || !h2) { continue; } - // Different channel: continue - if (pad.channel !== channel) { continue; } - - var shouldUpdate = p.hash.replace(/\/$/, '') === p2.hash.replace(/\/$/, ''); - - // If the hash is different but represents the same channel, check if weaker or stronger - if (!shouldUpdate && h.version !== 0) { - // We had view & now we have edit, update - if (h2.mode === 'view' && h.mode === 'edit') { shouldUpdate = true; } - // Same mode and we had present URL, update - else if (h.mode === h2.mode && h2.present) { shouldUpdate = true; } - // If we're here it means we have a weaker URL: - // update the date but keep the existing hash - else { - pad.atime = +new Date(); - contains = true; - continue; - } + var owners; + if (channelData && channelData.wc && channel === channelData.wc.id) { + owners = channelData.data.owners || undefined; } - if (shouldUpdate) { - contains = true; - 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 the href is different, it means we have a stronger one - if (href !== pad.href) { isStronger = true; } - pad.href = href; + var expire; + if (channelData && channelData.wc && channel === channelData.wc.id) { + expire = +channelData.data.expire || undefined; } - } - if (isStronger) { - // 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 - store.userObject.restoreHref(href); - } + var allPads = Util.find(store.proxy, ['drive', 'filesData']) || {}; + var isStronger; + + // If we don't find the new channel in our existing pads, we'll have to add the pads + // to filesData + var contains; + + // Update all pads that use the same channel but with a weaker hash + // Edit > Edit (present) > View > View (present) + for (var id in allPads) { + var pad = allPads[id]; + if (!pad.href) { continue; } + + var p2 = Hash.parsePadUrl(pad.href); + var h2 = p2.hashData; + + // Different types, proceed to the next one + // No hash data: corrupted pad? + if (p.type !== p2.type || !h2) { continue; } + // Different channel: continue + if (pad.channel !== channel) { continue; } + + var shouldUpdate = p.hash.replace(/\/$/, '') === p2.hash.replace(/\/$/, ''); + + // If the hash is different but represents the same channel, check if weaker or stronger + if (!shouldUpdate && h.version !== 0) { + // We had view & now we have edit, update + if (h2.mode === 'view' && h.mode === 'edit') { shouldUpdate = true; } + // Same mode and we had present URL, update + else if (h.mode === h2.mode && h2.present) { shouldUpdate = true; } + // If we're here it means we have a weaker URL: + // update the date but keep the existing hash + else { + pad.atime = +new Date(); + contains = true; + continue; + } + } - // Add the pad if it does not exist in our drive - if (!contains) { - Store.addPad({ - href: href, - channel: channel, - title: title, - owners: owners, - expire: expire, - password: data.password, - path: data.path - }, cb); - return; - } - onSync(cb); - }; + if (shouldUpdate) { + contains = true; + 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; - // Filepicker app - Store.getSecureFilesList = function (query, cb) { - var list = {}; - var hashes = []; - 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; - }); + // If the href is different, it means we have a stronger one + if (href !== pad.href) { isStronger = true; } + pad.href = href; + } } - return filtered; - }; - store.userObject.getFiles(where).forEach(function (id) { - var data = store.userObject.getFileData(id); - var parsed = Hash.parsePadUrl(data.href); - if ((!types || types.length === 0 || types.indexOf(parsed.type) !== -1) && - hashes.indexOf(parsed.hash) === -1 && - !isFiltered(parsed.type, data)) { - hashes.push(parsed.hash); - list[id] = data; + + if (isStronger) { + // 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 + store.userObject.restoreHref(href); } - }); - cb(list); - }; - Store.getPadData = function (id, cb) { - cb(store.userObject.getFileData(id)); - }; + // Add the pad if it does not exist in our drive + if (!contains) { + Store.addPad(clientId, { + href: href, + channel: channel, + title: title, + owners: owners, + expire: expire, + password: data.password, + path: data.path + }, cb); + return; + } else { + sendDriveEvent('DRIVE_CHANGE', { + path: ['drive', UserObject.FILES_DATA] + }, clientId); + } + onSync(cb); + }; - // Messaging (manage friends from the userlist) - var getMessagingCfg = function () { - return { - proxy: store.proxy, - realtime: store.realtime, - network: store.network, - updateMetadata: function () { - postMessage("UPDATE_METADATA"); - }, - pinPads: Store.pinPads, - friendComplete: function (data) { - postMessage("EV_FRIEND_COMPLETE", data); - }, - friendRequest: function (data, cb) { - postMessage("Q_FRIEND_REQUEST", data, cb); - }, + // Filepicker app + Store.getSecureFilesList = function (clientId, query, cb) { + var list = {}; + var hashes = []; + 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; + }; + store.userObject.getFiles(where).forEach(function (id) { + var data = store.userObject.getFileData(id); + var parsed = Hash.parsePadUrl(data.href); + if ((!types || types.length === 0 || types.indexOf(parsed.type) !== -1) && + hashes.indexOf(parsed.hash) === -1 && + !isFiltered(parsed.type, data)) { + hashes.push(parsed.hash); + list[id] = data; + } + }); + cb(list); }; - }; - Store.inviteFromUserlist = function (data, cb) { - var messagingCfg = getMessagingCfg(); - Messaging.inviteFromUserlist(messagingCfg, data, cb); - }; + Store.getPadData = function (clientId, id, cb) { + cb(store.userObject.getFileData(id)); + }; + - // Messenger + // Messaging (manage friends from the userlist) + var getMessagingCfg = function (clientId) { + return { + proxy: store.proxy, + realtime: store.realtime, + network: store.network, + updateMetadata: function () { + postMessage(clientId, "UPDATE_METADATA"); + }, + pinPads: function (data, cb) { Store.pinPads(null, data, cb); }, + friendComplete: function (data) { + postMessage(clientId, "EV_FRIEND_COMPLETE", data); + }, + friendRequest: function (data, cb) { + postMessage(clientId, "Q_FRIEND_REQUEST", data, cb); + }, + }; + }; + Store.inviteFromUserlist = function (clientId, data, cb) { + var messagingCfg = getMessagingCfg(clientId); + Messaging.inviteFromUserlist(messagingCfg, data, cb); + }; + Store.addDirectMessageHandlers = function (clientId, data) { + var messagingCfg = getMessagingCfg(clientId); + Messaging.addDirectMessageHandler(messagingCfg, data.href); + }; - // Get hashes for the share button - Store.getStrongerHash = function (data, cb) { - var allPads = Util.find(store.proxy, ['drive', 'filesData']) || {}; + // Messenger - // If we have a stronger version in drive, add it and add a redirect button - var stronger = Hash.findStronger(data.href, data.channel, allPads); - if (stronger) { - var parsed2 = Hash.parsePadUrl(stronger.href); - return void cb(parsed2.hash); - } - cb(); - }; + // Get hashes for the share button + Store.getStrongerHash = function (clientId, data, cb) { + var allPads = Util.find(store.proxy, ['drive', 'filesData']) || {}; - Store.messenger = { - getFriendList: function (data, cb) { - store.messenger.getFriendList(function (e, keys) { - cb({ - error: e, - data: keys, + // If we have a stronger version in drive, add it and add a redirect button + var stronger = Hash.findStronger(data.href, data.channel, allPads); + if (stronger) { + var parsed2 = Hash.parsePadUrl(stronger.href); + return void cb(parsed2.hash); + } + cb(); + }; + + Store.messenger = { + getFriendList: function (data, cb) { + store.messenger.getFriendList(function (e, keys) { + cb({ + error: e, + data: keys, + }); }); - }); - }, - getMyInfo: function (data, cb) { - store.messenger.getMyInfo(function (e, info) { - cb({ - error: e, - data: info, + }, + getMyInfo: function (data, cb) { + store.messenger.getMyInfo(function (e, info) { + cb({ + error: e, + data: info, + }); }); - }); - }, - getFriendInfo: function (data, cb) { - store.messenger.getFriendInfo(data, function (e, info) { - cb({ - error: e, - data: info, + }, + getFriendInfo: function (data, cb) { + store.messenger.getFriendInfo(data, function (e, info) { + cb({ + error: e, + data: info, + }); }); - }); - }, - removeFriend: function (data, cb) { - store.messenger.removeFriend(data, function (e, info) { - cb({ - error: e, - data: info, + }, + removeFriend: function (data, cb) { + store.messenger.removeFriend(data, function (e, info) { + cb({ + error: e, + data: info, + }); }); - }); - }, - openFriendChannel: function (data, cb) { - store.messenger.openFriendChannel(data, function (e) { - cb({ error: e, }); - }); - }, - getFriendStatus: function (data, cb) { - store.messenger.getStatus(data, function (e, online) { - cb({ - error: e, - data: online, + }, + openFriendChannel: function (data, cb) { + store.messenger.openFriendChannel(data, function (e) { + cb({ error: e, }); }); - }); - }, - getMoreHistory: function (data, cb) { - store.messenger.getMoreHistory(data.curvePublic, data.sig, data.count, function (e, history) { - cb({ - error: e, - data: history, + }, + getFriendStatus: function (data, cb) { + store.messenger.getStatus(data, function (e, online) { + cb({ + error: e, + data: online, + }); }); - }); - }, - sendMessage: function (data, cb) { - store.messenger.sendMessage(data.curvePublic, data.content, function (e) { - cb({ - error: e, + }, + getMoreHistory: function (data, cb) { + store.messenger.getMoreHistory(data.curvePublic, data.sig, data.count, function (e, history) { + cb({ + error: e, + data: history, + }); }); - }); - }, - setChannelHead: function (data, cb) { - store.messenger.setChannelHead(data.curvePublic, data.sig, function (e) { - cb({ - error: e + }, + sendMessage: function (data, cb) { + store.messenger.sendMessage(data.curvePublic, data.content, function (e) { + cb({ + error: e, + }); }); - }); - } - }; + }, + setChannelHead: function (data, cb) { + store.messenger.setChannelHead(data.curvePublic, data.sig, function (e) { + cb({ + error: e + }); + }); + } + }; - ////////////////////////////////////////////////////////////////// - /////////////////////// PAD ////////////////////////////////////// - ////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + /////////////////////// PAD ////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + + var channels = Store.channels = {}; + + Store.joinPad = function (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) { + channel.history.push('cp|' + msg); + var i; + for (i = channel.history.length - 2; i > 0; i--) { + if (/^cp\|/.test(channel.history[i])) { break; } + } + channel.history = channel.history.slice(i); + return; + } + channel.history.push(msg); + } + }; + if (channel.clients.indexOf(clientId) === -1) { + channel.clients.push(clientId); + } - // TODO with sharedworker - // channel will be an object storing the webchannel associated to each browser tab - var channel = Store.channel = { - queue: [], - data: {} - }; - Store.joinPad = function (data, cb) { - var conf = { - onReady: function (padData) { - channel.data = padData || {}; - postMessage("PAD_READY"); - }, // post EV_PAD_READY - onMessage: function (user, m, validateKey) { - postMessage("PAD_MESSAGE", { - user: user, - msg: m, - validateKey: validateKey + if (!isNew && channel.wc) { + postMessage(clientId, "PAD_CONNECT", { + myID: channel.wc.myID, + id: channel.wc.id, + members: channel.wc.members }); - }, // post EV_PAD_MESSAGE - onJoin: function (m) { - postMessage("PAD_JOIN", m); - }, // post EV_PAD_JOIN - onLeave: function (m) { - postMessage("PAD_LEAVE", m); - }, // post EV_PAD_LEAVE - onDisconnect: function () { - postMessage("PAD_DISCONNECT"); - }, // post EV_PAD_DISCONNECT - onError: function (err) { - postMessage("PAD_ERROR", err); - }, // post EV_PAD_ERROR - channel: data.channel, - validateKey: data.validateKey, - owners: data.owners, - password: data.password, - expire: data.expire, - network: store.network, - readOnly: data.readOnly, - onConnect: function (wc, sendMessage) { - channel.sendMessage = sendMessage; - channel.wc = wc; - channel.queue.forEach(function (data) { - sendMessage(data.message); + channel.wc.members.forEach(function (m) { + postMessage(clientId, "PAD_JOIN", m); }); - cb({ - myID: wc.myID, - id: wc.id, - members: wc.members + channel.history.forEach(function (msg) { + postMessage(clientId, "PAD_MESSAGE", { + msg: CpNfWorker.removeCp(msg), + user: channel.wc.myID, + validateKey: channel.data.validateKey + }); }); - } - }; - CpNfWorker.start(conf); - }; - Store.sendPadMsg = function (data, cb) { - if (!channel.wc) { channel.queue.push(data); } - channel.sendMessage(data, cb); - }; + postMessage(clientId, "PAD_READY"); - // TODO - // GET_FULL_HISTORY from sframe-common-outer - Store.getFullHistory = function (data, cb) { - var network = store.network; - var hkn = 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 onMsg = function (msg) { - var parsed = parse(msg); - if (parsed[0] === 'FULL_HISTORY_END') { - cb(msgs); 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); - msgs.push(msg); - } + var conf = { + onReady: function (padData) { + channel.data = padData || {}; + postMessage(clientId, "PAD_READY"); + }, + onMessage: function (user, m, validateKey, isCp) { + 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); + }, + onDisconnect: function () { + channel.bcast("PAD_DISCONNECT"); + }, + onError: function (err) { + channel.bcast("PAD_ERROR", err); + delete channels[data.channel]; // TODO test? + }, + channel: data.channel, + validateKey: data.validateKey, + owners: data.owners, + password: data.password, + expire: data.expire, + network: store.network, + //readOnly: data.readOnly, + onConnect: function (wc, sendMessage) { + channel.sendMessage = function (msg, cId, cb) { + // Send to server + sendMessage(msg, cb); + // Broadcast to other tabs + channel.pushHistory(CpNfWorker.removeCp(msg), /^cp\|/.test(msg)); + channel.bcast("PAD_MESSAGE", { + user: wc.myID, + msg: CpNfWorker.removeCp(msg), + validateKey: channel.data.validateKey + }, cId); + }; + channel.wc = wc; + channel.queue.forEach(function (data) { + channel.sendMessage(data.message, clientId); + }); + channel.bcast("PAD_CONNECT", { + myID: wc.myID, + id: wc.id, + members: wc.members + }); + } + }; + CpNfWorker.start(conf); }; - network.on('message', onMsg); - network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey])); - }; - - // TODO with sharedworker - // when the tab is closed, leave the pad - - // Drive - Store.userObjectCommand = function (cmdData, cb) { - if (!cmdData || !cmdData.cmd) { return; } - var data = cmdData.data; - switch (cmdData.cmd) { - case 'move': - store.userObject.move(data.paths, data.newPath, cb); break; - case 'restore': - store.userObject.restore(data.path, cb); break; - case 'addFolder': - store.userObject.addFolder(data.path, data.name, cb); break; - case 'delete': - store.userObject.delete(data.paths, cb, data.nocheck, data.isOwnPadRemoved); break; - case 'emptyTrash': - store.userObject.emptyTrash(cb); break; - case 'rename': - store.userObject.rename(data.path, data.newName, cb); break; - default: - cb(); - } - }; - - ////////////////////////////////////////////////////////////////// - /////////////////////// Init ///////////////////////////////////// - ////////////////////////////////////////////////////////////////// - - var onReady = function (returned, cb) { - var proxy = store.proxy; - var userObject = store.userObject = UserObject.init(proxy.drive, { - pinPads: Store.pinPads, - unpinPads: Store.unpinPads, - removeOwnedChannel: Store.removeOwnedChannel, - edPublic: store.proxy.edPublic, - loggedIn: store.loggedIn, - log: function (msg) { - postMessage("DRIVE_LOG", msg); + 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(); } - }); - nThen(function (waitFor) { - postMessage('LOADING_DRIVE', { - state: 2 - }); - userObject.migrate(waitFor()); - }).nThen(function (waitFor) { - Migrate(proxy, waitFor(), function (version, progress) { - postMessage('LOADING_DRIVE', { - state: 2, - progress: progress - }); - }); - }).nThen(function () { - postMessage('LOADING_DRIVE', { - state: 3 - }); - userObject.fixFiles(); + channel.sendMessage(msg, clientId, cb); + }; - var requestLogin = function () { - postMessage("REQUEST_LOGIN"); + // GET_FULL_HISTORY from sframe-common-outer + Store.getFullHistory = function (clientId, data, cb) { + var network = store.network; + var hkn = 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; + } }; - - if (store.loggedIn) { - /* 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); + var msgs = []; + var onMsg = function (msg) { + var parsed = parse(msg); + if (parsed[0] === 'FULL_HISTORY_END') { + cb(msgs); + return; } - 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(); + if (parsed[0] !== 'FULL_HISTORY') { return; } + if (parsed[1] && parsed[1].validateKey) { // First message + return; } - } - - if (!proxy.settings || !proxy.settings.general || - typeof(proxy.settings.general.allowUserFeedback) !== 'boolean') { - proxy.settings = proxy.settings || {}; - proxy.settings.general = proxy.settings.general || {}; - proxy.settings.general.allowUserFeedback = true; - } - returned.feedback = proxy.settings.general.allowUserFeedback; - - if (typeof(cb) === 'function') { cb(returned); } + 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); + msgs.push(msg); + } + }; + network.on('message', onMsg); + network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey])); + }; - 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(); + // Drive + Store.userObjectCommand = function (clientId, cmdData, cb) { + if (!cmdData || !cmdData.cmd) { return; } + var data = cmdData.data; + var cb2 = function (data2) { + var paths = data.paths || [data.path] || []; + paths = paths.concat(data.newPath || []); + paths.forEach(function (p) { + sendDriveEvent('DRIVE_CHANGE', { + //path: ['drive', UserObject.FILES_DATA] + path: ['drive'].concat(p) + }, clientId); + }); + cb(data2); + }; + switch (cmdData.cmd) { + case 'move': + store.userObject.move(data.paths, data.newPath, cb2); break; + case 'restore': + store.userObject.restore(data.path, cb2); break; + case 'addFolder': + store.userObject.addFolder(data.path, data.name, cb2); break; + case 'delete': + store.userObject.delete(data.paths, cb2, data.nocheck, data.isOwnPadRemoved); break; + case 'emptyTrash': + store.userObject.emptyTrash(cb2); break; + case 'rename': + store.userObject.rename(data.path, data.newName, cb2); break; + default: + cb(); } + }; - // if the user is logged in, but does not have signing keys... - if (store.loggedIn && (!Store.hasSigningKeys() || - !Store.hasCurveKeys())) { - return void requestLogin(); - } + // Clients management + var driveEventClients = []; + var messengerEventClients = []; - proxy.on('change', [Constants.displayNameKey], function (o, n) { - if (typeof(n) !== "string") { return; } - postMessage("UPDATE_METADATA"); - }); - proxy.on('change', ['profile'], function () { - // Trigger userlist update when the avatar has changed - postMessage("UPDATE_METADATA"); - }); - proxy.on('change', ['friends'], function () { - // Trigger userlist update when the friendlist has changed - postMessage("UPDATE_METADATA"); - }); - proxy.on('change', ['settings'], function () { - postMessage("UPDATE_METADATA"); - }); - proxy.on('change', [Constants.tokenKey], function () { - postMessage("UPDATE_TOKEN", { token: proxy[Constants.tokenKey] }); - }); - }); - }; + var dropChannel = function (chanId) { + if (!Store.channels[chanId]) { return; } - var connect = function (data, cb) { - var hash = data.userHash || data.anonHash || Hash.createRandomHash('drive'); - storeHash = hash; - if (!hash) { - throw new Error('[Store.init] Unable to find or create a drive hash. Aborting...'); - } - // No password for drive - var secret = Hash.getSecrets('drive', hash); - var listmapConfig = { - data: {}, - websocketURL: NetConfig.getWebsocketURL(), - channel: secret.channel, - readOnly: false, - validateKey: secret.keys.validateKey || undefined, - crypto: Crypto.createEncryptor(secret.keys), - userName: 'fs', - logLevel: 1, - ChainPad: ChainPad, - classic: true, - }; - var rt = window.rt = Listmap.create(listmapConfig); - 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); + if (Store.channels[chanId].wc) { + Store.channels[chanId].wc.leave(''); } - }).on('ready', function () { - if (store.userObject) { return; } // the store is already ready, it is a reconnection - 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] = []; + delete Store.channels[chanId]; + }; + Store._removeClient = function (clientId) { + var driveIdx = driveEventClients.indexOf(clientId); + if (driveIdx !== -1) { + driveEventClients.splice(driveIdx, 1); } - postMessage('LOADING_DRIVE', { state: 1 }); - // Drive already exist: return the existing drive, don't load data from legacy store - onReady(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(); - postMessage('NETWORK_DISCONNECT'); + var messengerIdx = messengerEventClients.indexOf(clientId); + if (messengerIdx !== -1) { + messengerEventClients.splice(messengerIdx, 1); } - }); - - rt.proxy.on('disconnect', function () { - postMessage('NETWORK_DISCONNECT'); - }); - rt.proxy.on('reconnect', function (info) { - postMessage('NETWORK_RECONNECT', {myId: info.myId}); - }); - }; - - /** - * Data: - * - userHash or anonHash - * Todo in cb - * - LocalStore.setFSHash if needed - * - sessionStorage.User_Hash - * - stuff with tokenKey - * Event to outer - * - requestLogin - */ - var initialized = false; - Store.init = function (data, callback) { - if (initialized) { - return void callback({ - state: 'ALREADY_INIT', - returned: store.returned - }); - } - initialized = true; - postMessage = function (cmd, d, cb) { - setTimeout(function () { - data.query(cmd, d, cb); // TODO temporary, will be replaced by webworker channel + 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); + } }); }; - store.data = data; - connect(data, function (ret) { - if (Object.keys(store.proxy).length === 1) { - Feedback.send("FIRST_APP_USE", true); - } - store.returned = ret; - - callback(ret); + // Special events - var messagingCfg = getMessagingCfg(); - Messaging.addDirectMessageHandler(messagingCfg); - - // Send events whenever there is a change or a removal in the drive - if (data.driveEvents) { + var driveEventInit = false; + sendDriveEvent = function (q, data, sender) { + driveEventClients.forEach(function (cId) { + if (cId === sender) { return; } + postMessage(cId, q, data); + }); + }; + Store._subscribeToDrive = function (clientId) { + if (driveEventClients.indexOf(clientId) === -1) { + driveEventClients.push(clientId); + } + if (!driveEventInit) { store.proxy.on('change', [], function (o, n, p) { - postMessage('DRIVE_CHANGE', { + sendDriveEvent('DRIVE_CHANGE', { old: o, new: n, path: p }); }); store.proxy.on('remove', [], function (o, p) { - postMessage('DRIVE_REMOVE', { + sendDriveEvent(clientId, 'DRIVE_REMOVE', { old: o, path: p }); }); + driveEventInit = true; } + }; - if (data.messenger) { + var messengerEventInit = false; + var sendMessengerEvent = function (q, data) { + messengerEventClients.forEach(function (cId) { + postMessage(cId, q, data); + }); + }; + Store._subscribeToMessenger = function (clientId) { + if (messengerEventClients.indexOf(clientId) === -1) { + messengerEventClients.push(clientId); + } + if (!messengerEventInit) { var messenger = store.messenger = Messenger.messenger(store); messenger.on('message', function (message) { - postMessage('CONTACTS_MESSAGE', message); + sendMessengerEvent('CONTACTS_MESSAGE', message); }); messenger.on('join', function (curvePublic, channel) { - postMessage('CONTACTS_JOIN', { + sendMessengerEvent('CONTACTS_JOIN', { curvePublic: curvePublic, channel: channel, }); }); messenger.on('leave', function (curvePublic, channel) { - postMessage('CONTACTS_LEAVE', { + sendMessengerEvent('CONTACTS_LEAVE', { curvePublic: curvePublic, channel: channel, }); }); messenger.on('update', function (info, curvePublic) { - postMessage('CONTACTS_UPDATE', { + sendMessengerEvent('CONTACTS_UPDATE', { curvePublic: curvePublic, info: info, }); }); messenger.on('friend', function (curvePublic) { - postMessage('CONTACTS_FRIEND', { + sendMessengerEvent('CONTACTS_FRIEND', { curvePublic: curvePublic, }); }); messenger.on('unfriend', function (curvePublic) { - postMessage('CONTACTS_UNFRIEND', { + sendMessengerEvent('CONTACTS_UNFRIEND', { curvePublic: curvePublic, }); }); + messengerEventInit = true; } - }); + }; + + + ////////////////////////////////////////////////////////////////// + /////////////////////// Init ///////////////////////////////////// + ////////////////////////////////////////////////////////////////// + + var onReady = function (clientId, returned, cb) { + var proxy = store.proxy; + var userObject = store.userObject = UserObject.init(proxy.drive, { + pinPads: function (data, cb) { Store.pinPads(null, data, cb); }, + unpinPads: function (data, cb) { Store.unpinPads(null, data, cb); }, + removeOwnedChannel: function (data, cb) { Store.removeOwnedChannel(null, data, cb); }, + edPublic: store.proxy.edPublic, + loggedIn: store.loggedIn, + log: function (msg) { + // broadcast to all drive apps + sendDriveEvent("DRIVE_LOG", msg); + } + }); + nThen(function (waitFor) { + postMessage(clientId, 'LOADING_DRIVE', { + state: 2 + }); + userObject.migrate(waitFor()); + }).nThen(function (waitFor) { + Migrate(proxy, waitFor(), function (version, progress) { + postMessage(clientId, 'LOADING_DRIVE', { + state: 2, + progress: progress + }); + }); + }).nThen(function () { + postMessage(clientId, 'LOADING_DRIVE', { + state: 3 + }); + userObject.fixFiles(); + + var requestLogin = function () { + broadcast([], "REQUEST_LOGIN"); + }; + + if (store.loggedIn) { + /* 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(); + } + } + + if (!proxy.settings || !proxy.settings.general || + typeof(proxy.settings.general.allowUserFeedback) !== 'boolean') { + proxy.settings = proxy.settings || {}; + proxy.settings.general = proxy.settings.general || {}; + proxy.settings.general.allowUserFeedback = true; + } + returned.feedback = proxy.settings.general.allowUserFeedback; + + if (typeof(cb) === 'function') { cb(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 () { + // Trigger userlist update when the friendlist has changed + broadcast([], "UPDATE_METADATA"); + }); + proxy.on('change', ['settings'], function () { + broadcast([], "UPDATE_METADATA"); + }); + proxy.on('change', [Constants.tokenKey], function () { + broadcast([], "UPDATE_TOKEN", { token: proxy[Constants.tokenKey] }); + }); + }); + }; + + var connect = function (clientId, data, cb) { + var hash = data.userHash || data.anonHash || Hash.createRandomHash('drive'); + storeHash = hash; + if (!hash) { + throw new Error('[Store.init] Unable to find or create a drive hash. Aborting...'); + } + // No password for drive + var secret = Hash.getSecrets('drive', hash); + var listmapConfig = { + data: {}, + websocketURL: NetConfig.getWebsocketURL(), + channel: secret.channel, + readOnly: false, + validateKey: secret.keys.validateKey || undefined, + crypto: Crypto.createEncryptor(secret.keys), + userName: 'fs', + logLevel: 1, + ChainPad: ChainPad, + classic: true, + }; + var rt = window.rt = Listmap.create(listmapConfig); + 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('ready', function () { + if (store.userObject) { return; } // the store is already ready, it is a reconnection + 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] = []; + } + postMessage(clientId, 'LOADING_DRIVE', { state: 1 }); + // 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(); + broadcast([], 'NETWORK_DISCONNECT'); + } + }); + + rt.proxy.on('disconnect', function () { + broadcast([], 'NETWORK_DISCONNECT'); + }); + rt.proxy.on('reconnect', function (info) { + broadcast([], 'NETWORK_RECONNECT', {myId: info.myId}); + }); + }; + + /** + * Data: + * - userHash or anonHash + * Todo in cb + * - LocalStore.setFSHash if needed + * - sessionStorage.User_Hash + * - stuff with tokenKey + * Event to outer + * - requestLogin + */ + var initialized = false; + Store.init = function (clientId, data, callback) { + if (initialized) { + return void callback({ + state: 'ALREADY_INIT', + returned: store.returned + }); + } + 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); + } + store.returned = ret; + + callback(ret); + }); + }; + + Store.disconnect = function () { + if (!store.network) { return; } + store.network.disconnect(); + }; + return Store; }; - Store.disconnect = function () { - if (!store.network) { return; } - store.network.disconnect(); + return { + create: create }; - return Store; }); diff --git a/www/common/outer/chainpad-netflux-worker.js b/www/common/outer/chainpad-netflux-worker.js index 631d9c433..a21b9e293 100644 --- a/www/common/outer/chainpad-netflux-worker.js +++ b/www/common/outer/chainpad-netflux-worker.js @@ -22,6 +22,10 @@ define([], function () { var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; + var removeCp = function (str) { + return str.replace(/^cp\|([A-Za-z0-9+\/=]{0,20}\|)?/, ''); + }; + var start = function (conf) { var channel = conf.channel; var validateKey = conf.validateKey; @@ -72,7 +76,7 @@ define([], function () { // at the beginning of each message on the server. // We have to make sure our regex ignores this nonce using {0,20} (our IDs // should only be 8 characters long) - return msg.replace(/^cp\|([A-Za-z0-9+\/=]{0,20}\|)?/, ''); + return removeCp(msg); }; var msgOut = function (msg) { @@ -124,6 +128,8 @@ define([], function () { lastKnownHash = msg.slice(0,64); + + var isCp = /^cp\|/.test(msg); var message = msgIn(peer, msg); verbose(message); @@ -134,7 +140,7 @@ define([], function () { message = unBencode(message);//.slice(message.indexOf(':[') + 1); // pass the message into Chainpad - onMessage(peer, message, validateKey); + onMessage(peer, message, validateKey, isCp); //sframeChan.query('Q_RT_MESSAGE', message, function () { }); }; @@ -259,7 +265,8 @@ define([], function () { }; return { - start: start + start: start, + removeCp: removeCp /*function (config) { config.sframeChan.whenReg('EV_RT_READY', function () { start(config); diff --git a/www/common/outer/noworker.js b/www/common/outer/noworker.js new file mode 100644 index 000000000..a412f2da0 --- /dev/null +++ b/www/common/outer/noworker.js @@ -0,0 +1,103 @@ +define([ + '/common/common-util.js', + '/common/outer/worker-channel.js', + '/common/outer/store-rpc.js', +], function (Util, Channel, SRpc) { + + var msgEv = Util.mkEvent(); + var sendMsg = Util.mkEvent(); + var create = function () { + var Rpc = SRpc(); + + var postMessage = function (data) { + sendMsg.fire(data); + }; + + Channel.create(msgEv, postMessage, function (chan) { + var clientId = '1'; + Object.keys(Rpc.queries).forEach(function (q) { + if (q === 'CONNECT') { return; } + if (q === 'JOIN_PAD') { return; } + if (q === 'SEND_PAD_MSG') { return; } + chan.on(q, function (data, cb) { + try { + Rpc.queries[q](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query ' + q); + console.error(e); + console.log(data); + } + }); + }); + chan.on('CONNECT', function (cfg, cb) { + // load Store here, with cfg, and pass a "query" (chan.query) + // cId is a clientId used in ServiceWorker or SharedWorker + cfg.query = function (cId, cmd, data, cb) { + cb = cb || function () {}; + chan.query(cmd, data, function (err, data2) { + if (err) { return void cb({error: err}); } + cb(data2); + }); + }; + cfg.broadcast = function (excludes, cmd, data, cb) { + cb = cb || function () {}; + if (excludes.indexOf(clientId) !== -1) { return; } + chan.query(cmd, data, function (err, data2) { + if (err) { return void cb({error: err}); } + cb(data2); + }); + }; + Rpc.queries['CONNECT'](clientId, cfg, function (data) { + if (data && data.state === "ALREADY_INIT") { + return void cb(data); + } + if (cfg.driveEvents) { + Rpc._subscribeToDrive(clientId); + } + if (cfg.messenger) { + Rpc._subscribeToMessenger(clientId); + } + cb(data); + }); + }); + var chanId; + chan.on('JOIN_PAD', function (data, cb) { + chanId = data.channel; + try { + Rpc.queries['JOIN_PAD'](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query JOIN_PAD'); + console.error(e); + console.log(data); + } + }); + chan.on('SEND_PAD_MSG', function (msg, cb) { + var data = { + msg: msg, + channel: chanId + }; + try { + Rpc.queries['SEND_PAD_MSG'](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query SEND_PAD_MSG'); + console.error(e); + console.log(data); + } + }); + }, true); + }; + + return { + query: function (data) { + msgEv.fire({data: data}); + }, + onMessage: function (cb) { + sendMsg.reg(function (data) { + setTimeout(function () { + cb(data); + }); + }); + }, + create: create + }; +}); diff --git a/www/common/outer/serviceworker.js b/www/common/outer/serviceworker.js new file mode 100644 index 000000000..68afb4f32 --- /dev/null +++ b/www/common/outer/serviceworker.js @@ -0,0 +1,175 @@ +/* jshint ignore:start */ +importScripts('/bower_components/requirejs/require.js'); + +window = self; +localStorage = { + setItem: function (k, v) { localStorage[k] = v; }, + getItem: function (k) { return localStorage[k]; } +}; + +self.tabs = {}; + +var postMsg = function (client, data) { + client.postMessage(data); +}; + +var debug = function (msg) { console.log(msg); }; +// debug = function () {}; + +var init = function (client, cb) { + debug('SW INIT'); + + require([ + '/common/requireconfig.js' + ], function (RequireConfig) { + require.config(RequireConfig()); + require([ + '/common/common-util.js', + '/common/outer/worker-channel.js', + '/common/outer/store-rpc.js' + ], function (Util, Channel, SRpc) { + debug('SW Required ressources loaded'); + var msgEv = Util.mkEvent(); + + if (!self.Rpc) { + self.Rpc = SRpc(); + } + var Rpc = self.Rpc; + + var postToClient = function (data) { + postMsg(client, data); + }; + Channel.create(msgEv, postToClient, function (chan) { + debug('SW Channel created'); + + var clientId = client.id; + self.tabs[clientId].chan = chan; + Object.keys(Rpc.queries).forEach(function (q) { + if (q === 'CONNECT') { return; } + if (q === 'JOIN_PAD') { return; } + if (q === 'SEND_PAD_MSG') { return; } + chan.on(q, function (data, cb) { + try { + Rpc.queries[q](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query ' + q); + console.error(e); + console.log(data); + } + if (q === "DISCONNECT") { + console.log('Deleting existing store!'); + delete self.Rpc; + delete self.store; + } + }); + }); + chan.on('CONNECT', function (cfg, cb) { + debug('SW Connect callback'); + if (self.store) { + debug('Store already exists!'); + if (cfg.driveEvents) { + Rpc._subscribeToDrive(clientId); + } + if (cfg.messenger) { + Rpc._subscribeToMessenger(clientId); + } + return void cb(self.store); + } + + debug('Loading new async store'); + // One-time initialization (init async-store) + cfg.query = function (cId, cmd, data, cb) { + cb = cb || function () {}; + self.tabs[cId].chan.query(cmd, data, function (err, data2) { + if (err) { return void cb({error: err}); } + cb(data2); + }); + }; + cfg.broadcast = function (excludes, cmd, data, cb) { + cb = cb || function () {}; + Object.keys(self.tabs).forEach(function (cId) { + if (excludes.indexOf(cId) !== -1) { return; } + self.tabs[cId].chan.query(cmd, data, function (err, data2) { + if (err) { return void cb({error: err}); } + cb(data2); + }); + }); + }; + Rpc.queries['CONNECT'](clientId, cfg, function (data) { + if (cfg.driveEvents) { + Rpc._subscribeToDrive(clientId); + } + if (cfg.messenger) { + Rpc._subscribeToMessenger(clientId); + } + if (data && data.state === "ALREADY_INIT") { + return void cb(data.returned); + } + self.store = data; + cb(data); + }); + }); + chan.on('JOIN_PAD', function (data, cb) { + self.tabs[clientId].channelId = data.channel; + try { + Rpc.queries['JOIN_PAD'](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query JOIN_PAD'); + console.error(e); + console.log(data); + } + }); + chan.on('SEND_PAD_MSG', function (msg, cb) { + var data = { + msg: msg, + channel: self.tabs[clientId].channelId + }; + try { + Rpc.queries['SEND_PAD_MSG'](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query SEND_PAD_MSG'); + console.error(e); + console.log(data); + } + }); + cb(); + }, true); + + self.tabs[client.id].msgEv = msgEv; + + self.tabs[client.id].close = function () { + Rpc._removeClient(client.id); + }; + }); + }); +}; + +self.addEventListener('message', function (e) { + var cId = e.source.id; + if (e.data === "INIT") { + if (tabs[cId]) { return; } + tabs[cId] = { + client: e.source + }; + init(e.source, function () { + postMsg(e.source, 'SW_READY'); + }); + } else if (e.data === "CLOSE") { + if (tabs[cId] && tabs[cId].close) { + console.log('leave'); + tabs[cId].close(); + } + } else if (self.tabs[cId] && self.tabs[cId].msgEv) { + self.tabs[cId].msgEv.fire(e); + } +}); +self.addEventListener('install', function (e) { + debug('V1 installing…'); + self.skipWaiting(); +}); + +self.addEventListener('activate', function (e) { + debug('V1 now ready to handle fetches!'); +}); + + diff --git a/www/common/outer/sharedworker.js b/www/common/outer/sharedworker.js new file mode 100644 index 000000000..97686da24 --- /dev/null +++ b/www/common/outer/sharedworker.js @@ -0,0 +1,174 @@ +/* jshint ignore:start */ +importScripts('/bower_components/requirejs/require.js'); + +window = self; +localStorage = { + setItem: function (k, v) { localStorage[k] = v; }, + getItem: function (k) { return localStorage[k]; } +}; + +self.tabs = {}; + +var postMsg = function (client, data) { + client.port.postMessage(data); +}; + +var debug = function (msg) { console.log(msg); }; +// debug = function () {}; + +var init = function (client, cb) { + debug('SharedW INIT'); + + require([ + '/common/requireconfig.js' + ], function (RequireConfig) { + require.config(RequireConfig()); + require([ + '/common/common-util.js', + '/common/outer/worker-channel.js', + '/common/outer/store-rpc.js' + ], function (Util, Channel, SRpc) { + debug('SharedW Required ressources loaded'); + var msgEv = Util.mkEvent(); + + if (!self.Rpc) { + self.Rpc = SRpc(); + } + var Rpc = self.Rpc; + + var postToClient = function (data) { + postMsg(client, data); + }; + Channel.create(msgEv, postToClient, function (chan) { + debug('SharedW Channel created'); + + var clientId = client.id; + client.chan = chan; + Object.keys(Rpc.queries).forEach(function (q) { + if (q === 'CONNECT') { return; } + if (q === 'JOIN_PAD') { return; } + if (q === 'SEND_PAD_MSG') { return; } + chan.on(q, function (data, cb) { + try { + Rpc.queries[q](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query ' + q); + console.error(e); + console.log(data); + } + if (q === "DISCONNECT") { + console.log('Deleting existing store!'); + delete self.Rpc; + delete self.store; + } + }); + }); + chan.on('CONNECT', function (cfg, cb) { + debug('SharedW connecting to store...'); + if (self.store) { + debug('Store already exists!'); + if (cfg.driveEvents) { + Rpc._subscribeToDrive(clientId); + } + if (cfg.messenger) { + Rpc._subscribeToMessenger(clientId); + } + return void cb(self.store); + } + + debug('Loading new async store'); + // One-time initialization (init async-store) + cfg.query = function (cId, cmd, data, cb) { + cb = cb || function () {}; + self.tabs[cId].chan.query(cmd, data, function (err, data2) { + if (err) { return void cb({error: err}); } + cb(data2); + }); + }; + cfg.broadcast = function (excludes, cmd, data, cb) { + cb = cb || function () {}; + Object.keys(self.tabs).forEach(function (cId) { + if (excludes.indexOf(cId) !== -1) { return; } + self.tabs[cId].chan.query(cmd, data, function (err, data2) { + if (err) { return void cb({error: err}); } + cb(data2); + }); + }); + }; + Rpc.queries['CONNECT'](clientId, cfg, function (data) { + if (cfg.driveEvents) { + Rpc._subscribeToDrive(clientId); + } + if (cfg.messenger) { + Rpc._subscribeToMessenger(clientId); + } + if (data && data.state === "ALREADY_INIT") { + self.store = data.returned; + return void cb(data.returned); + } + self.store = data; + cb(data); + }); + }); + chan.on('JOIN_PAD', function (data, cb) { + client.channelId = data.channel; + try { + Rpc.queries['JOIN_PAD'](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query JOIN_PAD'); + console.error(e); + console.log(data); + } + }); + chan.on('SEND_PAD_MSG', function (msg, cb) { + var data = { + msg: msg, + channel: client.channelId + }; + try { + Rpc.queries['SEND_PAD_MSG'](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query SEND_PAD_MSG'); + console.error(e); + console.log(data); + } + }); + cb(); + }, true); + + client.msgEv = msgEv; + + client.close = function () { + Rpc._removeClient(client.id); + }; + }); + }); +}; + +onconnect = function(e) { + debug('New SharedWorker client'); + var port = e.ports[0]; + var cId = Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)) + var client = self.tabs[cId] = { + id: cId, + port: port + }; + + port.onmessage = function (e) { + if (e.data === "INIT") { + if (client.init) { return; } + client.init = true; + init(client, function () { + postMsg(client, 'SW_READY'); + }); + } else if (e.data === "CLOSE") { + if (client && client.close) { + console.log('leave'); + client.close(); + } + } else if (client && client.msgEv) { + client.msgEv.fire(e); + } + }; +}; + diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index b134435b2..f4fb180f1 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -1,193 +1,96 @@ define([ '/common/outer/async-store.js' -], function (Store) { - var Rpc = {}; +], function (AStore) { - Rpc.query = function (cmd, data, cb) { - switch (cmd) { - // READY - case 'CONNECT': { - Store.init(data, cb); break; - } - case 'DISCONNECT': { - Store.disconnect(data, cb); break; - } - case 'CREATE_README': { - Store.createReadme(data, cb); break; - } - case 'MIGRATE_ANON_DRIVE': { - Store.migrateAnonDrive(data, cb); break; - } + var create = function () { + var Store = AStore.create(); + + var Rpc = {}; + + var queries = Rpc.queries = { + // Ready + CONNECT: Store.init, + DISCONNECT: Store.disconnect, + CREATE_README: Store.createReadme, + MIGRATE_ANON_DRIVE: Store.migrateAnonDrive, // RPC - case 'INIT_RPC': { - Store.initRpc(data, cb); break; - } - case 'UPDATE_PIN_LIMIT': { - Store.updatePinLimit(data, cb); break; - } - case 'GET_PIN_LIMIT': { - Store.getPinLimit(data, cb); break; - } - case 'CLEAR_OWNED_CHANNEL': { - Store.clearOwnedChannel(data, cb); break; - } - case 'REMOVE_OWNED_CHANNEL': { - Store.removeOwnedChannel(data, cb); break; - } - case 'UPLOAD_CHUNK': { - Store.uploadChunk(data, cb); break; - } - case 'UPLOAD_COMPLETE': { - Store.uploadComplete(data, cb); break; - } - case 'UPLOAD_STATUS': { - Store.uploadStatus(data, cb); break; - } - case 'UPLOAD_CANCEL': { - Store.uploadCancel(data, cb); break; - } - case 'PIN_PADS': { - Store.pinPads(data, cb); break; - } - case 'UNPIN_PADS': { - Store.unpinPads(data, cb); break; - } - case 'GET_DELETED_PADS': { - Store.getDeletedPads(data, cb); break; - } - case 'GET_PINNED_USAGE': { - Store.getPinnedUsage(data, cb); break; - } + INIT_RPC: Store.initRpc, + UPDATE_PIN_LIMIT: Store.updatePinLimit, + GET_PIN_LIMIT: Store.getPinLimit, + CLEAR_OWNED_CHANNEL: Store.clearOwnedChannel, + REMOVE_OWNED_CHANNEL: Store.removeOwnedChannel, + UPLOAD_CHUNK: Store.uploadChunk, + UPLOAD_COMPLETE: Store.uploadComplete, + UPLOAD_STATUS: Store.uploadStatus, + UPLOAD_CANCEL: Store.uploadCancel, + PIN_PADS: Store.pinPads, + UNPIN_PADS: Store.unpinPads, + GET_DELETED_PADS: Store.getDeletedPads, + GET_PINNED_USAGE: Store.getPinnedUsage, // ANON RPC - case 'INIT_ANON_RPC': { - Store.initAnonRpc(data, cb); break; - } - case 'ANON_RPC_MESSAGE': { - Store.anonRpcMsg(data, cb); break; - } - case 'GET_FILE_SIZE': { - Store.getFileSize(data, cb); break; - } - case 'GET_MULTIPLE_FILE_SIZE': { - Store.getMultipleFileSize(data, cb); break; - } + INIT_ANON_RPC: Store.initAnonRpc, + ANON_RPC_MESSAGE: Store.anonRpcMsg, + GET_FILE_SIZE: Store.getFileSize, + GET_MULTIPLE_FILE_SIZE: Store.getMultipleFileSize, // Store - case 'GET': { - Store.get(data, cb); break; - } - case 'SET': { - Store.set(data, cb); break; - } - case 'ADD_PAD': { - Store.addPad(data, cb); break; - } - case 'SET_PAD_TITLE': { - Store.setPadTitle(data, cb); break; - } - case 'MOVE_TO_TRASH': { - Store.moveToTrash(data, cb); break; - } - case 'RESET_DRIVE': { - Store.resetDrive(data, cb); break; - } - case 'GET_METADATA': { - Store.getMetadata(data, cb); break; - } - case 'SET_DISPLAY_NAME': { - Store.setDisplayName(data, cb); break; - } - case 'SET_PAD_ATTRIBUTE': { - Store.setPadAttribute(data, cb); break; - } - case 'GET_PAD_ATTRIBUTE': { - Store.getPadAttribute(data, cb); break; - } - case 'SET_ATTRIBUTE': { - Store.setAttribute(data, cb); break; - } - case 'GET_ATTRIBUTE': { - Store.getAttribute(data, cb); break; - } - case 'LIST_ALL_TAGS': { - Store.listAllTags(data, cb); break; - } - case 'GET_TEMPLATES': { - Store.getTemplates(data, cb); break; - } - case 'GET_SECURE_FILES_LIST': { - Store.getSecureFilesList(data, cb); break; - } - case 'GET_PAD_DATA': { - Store.getPadData(data, cb); break; - } - case 'GET_STRONGER_HASH': { - Store.getStrongerHash(data, cb); break; - } - case 'INCREMENT_TEMPLATE_USE': { - Store.incrementTemplateUse(data); break; - } + GET: Store.get, + SET: Store.set, + ADD_PAD: Store.addPad, + SET_PAD_TITLE: Store.setPadTitle, + MOVE_TO_TRASH: Store.moveToTrash, + RESET_DRIVE: Store.resetDrive, + GET_METADATA: Store.getMetadata, + SET_DISPLAY_NAME: Store.setDisplayName, + SET_PAD_ATTRIBUTE: Store.setPadAttribute, + GET_PAD_ATTRIBUTE: Store.getPadAttribute, + SET_ATTRIBUTE: Store.setAttribute, + GET_ATTRIBUTE: Store.getAttribute, + LIST_ALL_TAGS: Store.listAllTags, + GET_TEMPLATES: Store.getTemplates, + GET_SECURE_FILES_LIST: Store.getSecureFilesList, + GET_PAD_DATA: Store.getPadData, + GET_STRONGER_HASH: Store.getStrongerHash, + INCREMENT_TEMPLATE_USE: Store.incrementTemplateUse, // Messaging - case 'INVITE_FROM_USERLIST': { - Store.inviteFromUserlist(data, cb); break; - } + INVITE_FROM_USERLIST: Store.inviteFromUserlist, + ADD_DIRECT_MESSAGE_HANDLERS: Store.addDirectMessageHandlers, // Messenger - case 'CONTACTS_GET_FRIEND_LIST': { - Store.messenger.getFriendList(data, cb); break; - } - case 'CONTACTS_GET_MY_INFO': { - Store.messenger.getMyInfo(data, cb); break; - } - case 'CONTACTS_GET_FRIEND_INFO': { - Store.messenger.getFriendInfo(data, cb); break; - } - case 'CONTACTS_REMOVE_FRIEND': { - Store.messenger.removeFriend(data, cb); break; - } - case 'CONTACTS_OPEN_FRIEND_CHANNEL': { - Store.messenger.openFriendChannel(data, cb); break; - } - case 'CONTACTS_GET_FRIEND_STATUS': { - Store.messenger.getFriendStatus(data, cb); break; - } - case 'CONTACTS_GET_MORE_HISTORY': { - Store.messenger.getMoreHistory(data, cb); break; - } - case 'CONTACTS_SEND_MESSAGE': { - Store.messenger.sendMessage(data, cb); break; - } - case 'CONTACTS_SET_CHANNEL_HEAD': { - Store.messenger.setChannelHead(data, cb); break; - } + CONTACTS_GET_FRIEND_LIST: Store.messenger.getFriendList, + CONTACTS_GET_MY_INFO: Store.messenger.getMyInfo, + CONTACTS_GET_FRIEND_INFO: Store.messenger.getFriendInfo, + CONTACTS_REMOVE_FRIEND: Store.messenger.removeFriend, + CONTACTS_OPEN_FRIEND_CHANNEL: Store.messenger.openFriendChannel, + CONTACTS_GET_FRIEND_STATUS: Store.messenger.getFriendStatus, + CONTACTS_GET_MORE_HISTORY: Store.messenger.getMoreHistory, + CONTACTS_SEND_MESSAGE: Store.messenger.sendMessage, + CONTACTS_SET_CHANNEL_HEAD: Store.messenger.setChannelHead, // Pad - case 'SEND_PAD_MSG': { - Store.sendPadMsg(data, cb); break; - } - case 'JOIN_PAD': { - Store.joinPad(data, cb); break; - } - case 'GET_FULL_HISTORY': { - Store.getFullHistory(data, cb); break; - } + SEND_PAD_MSG: Store.sendPadMsg, + JOIN_PAD: Store.joinPad, + GET_FULL_HISTORY: Store.getFullHistory, + IS_NEW_CHANNEL: Store.isNewChannel, // Drive - case 'DRIVE_USEROBJECT': { - Store.userObjectCommand(data, cb); break; - } - // Settings - case 'DELETE_ACCOUNT': { - Store.deleteAccount(data, cb); break; - } - case 'IS_NEW_CHANNEL': { - Store.isNewChannel(data, cb); break; - } - default: { - console.error("UNHANDLED_STORE_RPC"); + DRIVE_USEROBJECT: Store.userObjectCommand, + // Settings, + DELETE_ACCOUNT: Store.deleteAccount, + }; - break; + Rpc.query = function (cmd, data, cb) { + if (queries[cmd]) { + queries[cmd]('0', data, cb); + } else { + console.error('UNHANDLED_STORE_RPC'); } - } + }; + + // Internal calls + Rpc._removeClient = Store._removeClient; + Rpc._subscribeToDrive = Store._subscribeToDrive; + Rpc._subscribeToMessenger = Store._subscribeToMessenger; + return Rpc; }; - return Rpc; + return create; }); diff --git a/www/common/outer/webworker.js b/www/common/outer/webworker.js new file mode 100644 index 000000000..201f3db9e --- /dev/null +++ b/www/common/outer/webworker.js @@ -0,0 +1,100 @@ +/* jshint ignore:start */ +importScripts('/bower_components/requirejs/require.js'); + +window = self; +localStorage = { + setItem: function (k, v) { localStorage[k] = v; }, + getItem: function (k) { return localStorage[k]; } +}; + +require([ + '/common/requireconfig.js' +], function (RequireConfig) { + require.config(RequireConfig()); + require([ + '/common/common-util.js', + '/common/outer/worker-channel.js', + '/common/outer/store-rpc.js' + ], function (Util, Channel, SRpc) { + var msgEv = Util.mkEvent(); + + var Rpc = SRpc(); + + Channel.create(msgEv, postMessage, function (chan) { + var clientId = '1'; + Object.keys(Rpc.queries).forEach(function (q) { + if (q === 'CONNECT') { return; } + if (q === 'JOIN_PAD') { return; } + if (q === 'SEND_PAD_MSG') { return; } + chan.on(q, function (data, cb) { + try { + Rpc.queries[q](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query ' + q); + console.error(e); + console.log(data); + } + }); + }); + chan.on('CONNECT', function (cfg, cb) { + // load Store here, with cfg, and pass a "query" (chan.query) + // cId is a clientId used in ServiceWorker or SharedWorker + cfg.query = function (cId, cmd, data, cb) { + cb = cb || function () {}; + chan.query(cmd, data, function (err, data2) { + if (err) { return void cb({error: err}); } + cb(data2); + }); + }; + cfg.broadcast = function (excludes, cmd, data, cb) { + cb = cb || function () {}; + if (excludes.indexOf(clientId) !== -1) { return; } + chan.query(cmd, data, function (err, data2) { + if (err) { return void cb({error: err}); } + cb(data2); + }); + }; + Rpc.queries['CONNECT'](clientId, cfg, function (data) { + if (data && data.state === "ALREADY_INIT") { + return void cb(data); + } + if (cfg.driveEvents) { + Rpc._subscribeToDrive(clientId); + } + if (cfg.messenger) { + Rpc._subscribeToMessenger(clientId); + } + cb(data); + }); + }); + var chanId; + chan.on('JOIN_PAD', function (data, cb) { + chanId = data.channel; + try { + Rpc.queries['JOIN_PAD'](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query JOIN_PAD'); + console.error(e); + console.log(data); + } + }); + chan.on('SEND_PAD_MSG', function (msg, cb) { + var data = { + msg: msg, + channel: chanId + }; + try { + Rpc.queries['SEND_PAD_MSG'](clientId, data, cb); + } catch (e) { + console.error('Error in webworker when executing query SEND_PAD_MSG'); + console.error(e); + console.log(data); + } + }); + }, true); + + onmessage = function (e) { + msgEv.fire(e); + }; + }); +}); diff --git a/www/common/outer/worker-channel.js b/www/common/outer/worker-channel.js new file mode 100644 index 000000000..bff81a27b --- /dev/null +++ b/www/common/outer/worker-channel.js @@ -0,0 +1,147 @@ +// This file provides the API for the channel for talking to and from the sandbox iframe. +define([ + //'/common/sframe-protocol.js', + '/common/common-util.js' +], function (/*SFrameProtocol,*/ Util) { + + var mkTxid = function () { + return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', ''); + }; + + var create = function (onMsg, postMsg, cb, isWorker) { + if (!isWorker) { + var chanLoaded = false; + var waitingData = []; + onMsg.reg(function (data) { + if (chanLoaded) { return; } + waitingData.push(data); + }); + } + + var evReady = Util.mkEvent(true); + + var handlers = {}; + var queries = {}; + + // list of handlers which are registered from the other side... + var insideHandlers = []; + var callWhenRegistered = {}; + + var chan = {}; + + // Send a query. channel.query('Q_SOMETHING', { args: "whatever" }, function (reply) { ... }); + chan.query = function (q, content, cb) { + var txid = mkTxid(); + var timeout = setTimeout(function () { + delete queries[txid]; + //console.log("Timeout making query " + q); + }, 30000); + queries[txid] = function (data, msg) { + clearTimeout(timeout); + delete queries[txid]; + cb(undefined, data.content, msg); + }; + evReady.reg(function () { + postMsg(JSON.stringify({ + txid: txid, + content: content, + q: q + })); + }); + }; + + // Fire an event. channel.event('EV_SOMETHING', { args: "whatever" }); + var event = chan.event = function (e, content) { + evReady.reg(function () { + postMsg(JSON.stringify({ content: content, q: e })); + }); + }; + + // Be notified on query or event. channel.on('EV_SOMETHING', function (args, reply) { ... }); + // If the type is a query, your handler will be invoked with a reply function that takes + // one argument (the content to reply with). + chan.on = function (queryType, handler, quiet) { + (handlers[queryType] = handlers[queryType] || []).push(function (data, msg) { + handler(data.content, function (replyContent) { + postMsg(JSON.stringify({ + txid: data.txid, + content: replyContent + })); + }, msg); + }); + if (!quiet) { + event('EV_REGISTER_HANDLER', queryType); + } + }; + + // If a particular handler is registered, call the callback immediately, otherwise it will be called + // when that handler is first registered. + // channel.whenReg('Q_SOMETHING', function () { ...query Q_SOMETHING?... }); + chan.whenReg = function (queryType, cb, always) { + var reg = always; + if (insideHandlers.indexOf(queryType) > -1) { + cb(); + } else { + reg = true; + } + if (reg) { + (callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(cb); + } + }; + + // Same as whenReg except it will invoke every time there is another registration, not just once. + chan.onReg = function (queryType, cb) { chan.whenReg(queryType, cb, true); }; + + chan.on('EV_REGISTER_HANDLER', function (content) { + if (callWhenRegistered[content]) { + callWhenRegistered[content].forEach(function (f) { f(); }); + delete callWhenRegistered[content]; + } + insideHandlers.push(content); + }); + chan.whenReg('EV_REGISTER_HANDLER', evReady.fire); + + // Make sure both iframes are ready + var isReady =false; + chan.onReady = function (h) { + if (isReady) { + return void h(); + } + if (typeof(h) !== "function") { return; } + chan.on('EV_RPC_READY', function () { isReady = true; h(); }); + }; + chan.ready = function () { + chan.whenReg('EV_RPC_READY', function () { + chan.event('EV_RPC_READY'); + }); + }; + + onMsg.reg(function (msg) { + var data = JSON.parse(msg.data); + if (typeof(data.q) === 'string' && handlers[data.q]) { + handlers[data.q].forEach(function (f) { + f(data || JSON.parse(msg.data), msg); + data = undefined; + }); + } else if (typeof(data.q) === 'undefined' && queries[data.txid]) { + queries[data.txid](data, msg); + } else { + console.log("DROP Unhandled message"); + console.log(msg.data, isWorker); + console.log(msg); + } + }); + if (isWorker) { + evReady.fire(); + } else { + chanLoaded = true; + waitingData.forEach(function (d) { + onMsg.fire(d); + }); + waitingData = []; + } + cb(chan); + }; + + return { create: create }; +}); diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index 45c4bdabf..b08f1fe08 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -116,6 +116,10 @@ define([], function () { sframeChan.event('EV_RT_DISCONNECT'); }); + padRpc.onConnectEvent.reg(function (data) { + onOpen(data); + }); + padRpc.onErrorEvent.reg(function (err) { sframeChan.event('EV_RT_ERROR', err); }); @@ -128,8 +132,6 @@ define([], function () { owners: owners, password: password, expire: expire - }, function(data) { - onOpen(data); }); }; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 45738265d..6c1b55809 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -704,6 +704,9 @@ define([ readOnly: readOnly, crypto: Crypto.createEncryptor(secret.keys), onConnect: function () { + var href = parsed.getUrl(); + // Add friends requests handlers when we have the final href + Cryptpad.messaging.addHandlers(href); if (window.location.hash && window.location.hash !== '#') { window.location = parsed.getUrl({ present: parsed.hashData.present, diff --git a/www/worker/inner.js b/www/worker/inner.js index 667f09221..20992b023 100644 --- a/www/worker/inner.js +++ b/www/worker/inner.js @@ -51,6 +51,8 @@ define([ if (!window.Worker) { return void $container.text("WebWorkers not supported by your browser"); } + /* + // Shared worker console.log('ready'); var myWorker = new SharedWorker('/worker/worker.js'); console.log(myWorker); @@ -65,7 +67,73 @@ define([ } $container.append('
'); $container.append(e.data); - }; + };*/ + + // Service worker + if ('serviceWorker' in navigator) { + console.log('here'); + var initializing = true; + var worker; + var postMessage = function (data) { + console.log(data, navigator.serviceWorker); + if (worker) { + return void worker.postMessage(data); + } + console.log('NOT READY'); + /*if (navigator.serviceWorker && navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage(data); + }*/ + }; + navigator.serviceWorker.register('/worker/sw.js', {scope: '/'}) + .then(function(reg) { + console.log(reg); + console.log('Registration succeeded. Scope is ' + reg.scope); + $container.append('
'); + $container.append('Registered! (scope: ' + reg.scope +')'); + reg.onupdatefound = function () { + if (initializing) { + var w = reg.installing; + var onStateChange = function () { + if (w.state === "activated") { + console.log(w); + worker = w; + postMessage("INIT"); + w.removeEventListener("statechange", onStateChange); + } + }; + w.addEventListener('statechange', onStateChange); + return; + } + console.log('new SW version found!'); + // KILL EVERYTHING + UI.confirm("New version detected, you have to reload", function (yes) { + if (yes) { common.gotoURL(); } + }); + }; + // Here we add the event listener for receiving messages + navigator.serviceWorker.addEventListener('message', function (e) { + var data = e.data; + if (data && data.state === "READY") { + initializing = false; + $container.append('
sw.js ready'); + postMessage(["Hello worker"]); + return; + } + $container.append('
'); + $container.append(e.data); + }); + if (reg.active) { + worker = reg.active; + postMessage("INIT"); + } + }).catch(function(error) { + console.log('Registration failed with ' + error); + $container.append('Registration error: ' + error); + }); + } else { + console.log('NO SERVICE WORKER'); + } + $container.append('
inner.js ready'); }); }); diff --git a/www/worker/sw.js b/www/worker/sw.js new file mode 100644 index 000000000..4daebc47f --- /dev/null +++ b/www/worker/sw.js @@ -0,0 +1,69 @@ +/* jshint ignore:start */ +var id; +//= Math.floor(Math.random()*100000); + +var postMsg = function (client, data) { + client.postMessage(data); +}; + +var broadcast = function (data, excludes) { + // Loop over all available clients + clients.matchAll().then(function (clients) { + clients.forEach(function (client) { + if (excludes.indexOf(client.id) === -1) { + postMsg(client, data); + } + }) + }) +}; +var sendTo = function (data, clientId){ + clients.matchAll().then(function (clients) { + clients.some(function (client) { + if (client.id === clientId) { + postMsg(client, data) + } + }) + }) +}; +var getClients = function () { + clients.matchAll().then(function (clients) { + var cl = clients.map(function (c) { + console.log(JSON.stringify(c)); + return c.id; + }); + console.log(cl); + }); +}; + + + +self.addEventListener('message', function (e) { + console.log(clients); + console.log('worker received'); + console.log(e.data); + console.log(e.source); + var cId = e.source.id; + if (e.data === "INIT") { + if (!id) { + id = Math.floor(Math.random()*100000); + } + broadcast(cId + ' has joined!', [cId]); + postMsg(e.source, {state: 'READY'}); + postMsg(e.source, "Welcome to SW " + id + "!"); + postMsg(e.source, "You are identified as " + cId); + } else { + console.log(e.data); + postMsg(e.source, 'Yo (Re: '+e.data+')'); + } +}); +self.addEventListener('install', function (e) { + console.log(e); + console.log('V1 installing…'); + self.skipWaiting(); +}); + +self.addEventListener('activate', function (e) { + console.log(e); + console.log('V1 now ready to handle fetches!'); +}); +