diff --git a/bower.json b/bower.json index b04ff3a6b..9e6ee5978 100644 --- a/bower.json +++ b/bower.json @@ -39,6 +39,7 @@ "require-css": "0.1.10", "less": "^2.7.2", "bootstrap": "#v4.0.0-alpha.6", - "diff-dom": "2.1.1" + "diff-dom": "2.1.1", + "nthen": "^0.1.5" } } diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js new file mode 100644 index 000000000..e69de29bb diff --git a/www/common/sframe-boot.js b/www/common/sframe-boot.js index 34b25167e..190055682 100644 --- a/www/common/sframe-boot.js +++ b/www/common/sframe-boot.js @@ -3,7 +3,7 @@ window.addEventListener('message', function (msg) { var data = JSON.parse(msg.data); if (data.q !== 'INIT') { return; } - msg.source.postMessage({ txid: data.txid, content: 'OK' }, '*'); + msg.source.postMessage(JSON.stringify({ txid: data.txid, content: 'OK' }), '*'); if (data.content && data.content.requireConf) { require.config(data.content.requireConf); } require(['/common/sframe-boot2.js'], function () { }); }); \ No newline at end of file diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js index 0c98fb5d7..94ec9d511 100644 --- a/www/common/sframe-boot2.js +++ b/www/common/sframe-boot2.js @@ -1,8 +1,9 @@ // This is stage 1, it can be changed but you must bump the version of the project. // Note: This must only be loaded from inside of a sandbox-iframe. define([ - '/common/requireconfig.js' -], function (RequireConfig) { + '/common/requireconfig.js', + '/common/sframe-channel.js' +], function (RequireConfig, SFrameChannel) { require.config(RequireConfig); console.log('boot2'); // most of CryptPad breaks if you don't support isArray @@ -22,5 +23,7 @@ console.log('boot2'); window.__defineGetter__('localStorage', function () { return mkFakeStore(); }); window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); }); + SFrameChannel.init(window.top, function () { }); + require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]); }); diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index 746b1d733..32e1beb0f 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -15,39 +15,17 @@ * along with this program. If not, see . */ define([ - '/bower_components/netflux-websocket/netflux-client.js', + '/common/sframe-channel.js', '/bower_components/chainpad/chainpad.dist.js', -], function (Netflux) { +], function (SFrameChannel) { var ChainPad = window.ChainPad; - var USE_HISTORY = true; var module = { exports: {} }; var verbose = function (x) { console.log(x); }; verbose = function () {}; // comment out to enable verbose logging - var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; - - module.exports.start = function (config) { - console.log(config); - var websocketUrl = config.websocketURL; - var userName = config.userName; - var channel = config.channel; - var Crypto = config.crypto; - var validateKey = config.validateKey; - var readOnly = config.readOnly || false; - - // make sure configuration is defined - config = config || {}; - - var initializing = true; - var toReturn = {}; - var messagesHistory = []; - var chainpadAdapter = {}; - var realtime; - var network = config.network; - var lastKnownHash; - - var userList = { + var mkUserList = function () { + var userList = Object.freeze({ change : [], onChange : function(newData) { userList.change.forEach(function (el) { @@ -55,9 +33,9 @@ define([ }); }, users: [] - }; + }); - var onJoining = function(peer) { + var onJoining = function (peer) { if(peer.length !== 32) { return; } var list = userList.users; var index = list.indexOf(peer); @@ -67,100 +45,8 @@ define([ userList.onChange(); }; - var onReady = function(wc, network) { - // Trigger onReady only if not ready yet. This is important because the history keeper sends a direct - // message through "network" when it is synced, and it triggers onReady for each channel joined. - if (!initializing) { return; } - - realtime.start(); - - if(config.setMyID) { - config.setMyID({ - myID: wc.myID - }); - } - // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced - if (!readOnly) { - onJoining(wc.myID); - } - - // we're fully synced - initializing = false; - - if (config.onReady) { - config.onReady({ - realtime: realtime, - network: network, - userList: userList, - myId: wc.myID, - leave: wc.leave - }); - } - }; - - var onMessage = function(peer, msg, wc, network, direct) { - // unpack the history keeper from the webchannel - var hk = network.historyKeeper; - - // Old server - if(wc && (msg === 0 || msg === '0')) { - onReady(wc, network); - return; - } - if (direct && peer !== hk) { - return; - } - if (direct) { - var parsed = JSON.parse(msg); - if (parsed.validateKey && parsed.channel) { - if (parsed.channel === wc.id && !validateKey) { - validateKey = parsed.validateKey; - } - // We have to return even if it is not the current channel: - // we don't want to continue with other channels messages here - return; - } - if (parsed.state && parsed.state === 1 && parsed.channel) { - if (parsed.channel === wc.id) { - onReady(wc, network); - } - // We have to return even if it is not the current channel: - // we don't want to continue with other channels messages here - return; - } - } - // The history keeper is different for each channel : - // no need to check if the message is related to the current channel - if (peer === hk){ - // if the peer is the 'history keeper', extract their message - var parsed1 = JSON.parse(msg); - msg = parsed1[4]; - // Check that this is a message for us - if (parsed1[3] !== wc.id) { return; } - } - - lastKnownHash = msg.slice(0,64); - var message = chainpadAdapter.msgIn(peer, msg); - - verbose(message); - - if (!initializing) { - if (config.onLocal) { - config.onLocal(); - } - } - - // slice off the bencoded header - // Why are we getting bencoded stuff to begin with? - // FIXME this shouldn't be necessary - message = unBencode(message);//.slice(message.indexOf(':[') + 1); - - // pass the message into Chainpad - realtime.message(message); - }; - // update UI components to show that one of the other peers has left - var onLeaving = function(peer) { + var onLeaving = function (peer) { var list = userList.users; var index = list.indexOf(peer); if(index !== -1) { @@ -169,246 +55,93 @@ define([ userList.onChange(); }; - // shim between chainpad and netflux - chainpadAdapter = { - msgIn : function(peerId, msg) { - msg = msg.replace(/^cp\|/, ''); - try { - var decryptedMsg = Crypto.decrypt(msg, validateKey); - messagesHistory.push(decryptedMsg); - return decryptedMsg; - } catch (err) { - console.error(err); - return msg; - } - }, - msgOut : function(msg) { - if (readOnly) { return; } - try { - var cmsg = Crypto.encrypt(msg); - if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } - return cmsg; - } catch (err) { - console.log(msg); - throw err; - } - } + var onReset = function () { + userList.users.forEach(onLeaving); }; - var createRealtime = function() { - return ChainPad.create({ + return Object.freeze({ + list: userList, + onJoin: onJoining, + onLeave: onLeaving, + onReset: onReset + }); + }; + + module.exports.start = function (config) { + var onConnectionChange = config.onConnectionChange || function () { }; + var onRemote = config.onRemote || function () { }; + var onInit = config.onInit || function () { }; + var onLocal = config.onLocal || function () { }; + var setMyID = config.setMyID || function () { }; + var onReady = config.onReady || function () { }; + var userName = config.userName; + var initialState = config.initialState; + var transformFunction = config.transformFunction; + var validateContent = config.validateContent; + var avgSyncMilliseconds = config.avgSyncMilliseconds; + var logLevel = typeof(config.logLevel) !== 'undefined'? config.logLevel : 1; + var readOnly = config.readOnly || false; + config = undefined; + + var chainpad; + var userList = mkUserList(); + var myID; + var isReady = false; + + SFrameChannel.on('EV_RT_JOIN', userList.onJoin); + SFrameChannel.on('EV_RT_LEAVE', userList.onLeave); + SFrameChannel.on('EV_RT_DISCONNECT', function () { + isReady = false; + userList.onReset(); + onConnectionChange({ state: false }); + }); + SFrameChannel.on('EV_RT_CONNECT', function (content) { + content.members.forEach(userList.onJoin); + myID = content.myID; + isReady = false; + if (chainpad) { + // it's a reconnect + onConnectionChange({ state: true, myId: myID }); + return; + } + chainpad = ChainPad.create({ userName: userName, - initialState: config.initialState, - transformFunction: config.transformFunction, - validateContent: config.validateContent, - avgSyncMilliseconds: config.avgSyncMilliseconds, - logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1 + initialState: initialState, + transformFunction: transformFunction, + validateContent: validateContent, + avgSyncMilliseconds: avgSyncMilliseconds, + logLevel: logLevel }); - }; - - // We use an object to store the webchannel so that we don't have to push new handlers to chainpad - // and remove the old ones when reconnecting and keeping the same 'realtime' object - // See realtime.onMessage below: we call wc.bcast(...) but wc may change - var wcObject = {}; - var onOpen = function(wc, network, initialize) { - wcObject.wc = wc; - channel = wc.id; - - // Add the existing peers in the userList - wc.members.forEach(onJoining); - - // Add the handlers to the WebChannel - wc.on('message', function (msg, sender) { //Channel msg - onMessage(sender, msg, wc, network); + chainpad.onMessage(function(message, cb) { + SFrameChannel.query('Q_RT_MESSAGE', message, cb); }); - wc.on('join', onJoining); - wc.on('leave', onLeaving); - - if (initialize) { - toReturn.realtime = realtime = createRealtime(); - - realtime._patch = realtime.patch; - realtime.patch = function (patch, x, y) { - if (initializing) { - console.error("attempted to change the content before chainpad was synced"); - } - return realtime._patch(patch, x, y); - }; - realtime._change = realtime.change; - realtime.change = function (offset, count, chars) { - if (initializing) { - console.error("attempted to change the content before chainpad was synced"); - } - return realtime._change(offset, count, chars); - }; - - if (config.onInit) { - config.onInit({ - myID: wc.myID, - realtime: realtime, - getLag: network.getLag, - userList: userList, - network: network, - channel: channel - }); - } - - // Sending a message... - realtime.onMessage(function(message, cb) { - // Filter messages sent by Chainpad to make it compatible with Netflux - message = chainpadAdapter.msgOut(message); - if(message) { - // Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we - // want to keep the same chainpad (realtime) object - wcObject.wc.bcast(message).then(function() { - cb(); - }, function(err) { - // The message has not been sent, display the error. - console.error(err); - }); - } - }); - - realtime.onPatch(function () { - if (config.onRemote) { - config.onRemote({ - realtime: realtime - }); - } - }); - } - - // Get the channel history - if(USE_HISTORY) { - var hk; - - wc.members.forEach(function (p) { - if (p.length === 16) { hk = p; } - }); - network.historyKeeper = hk; - - var msg = ['GET_HISTORY', wc.id]; - // Add the validateKey if we are the channel creator and we have a validateKey - msg.push(validateKey); - msg.push(lastKnownHash); - if (hk) { network.sendto(hk, JSON.stringify(msg)); } - } - else { - onReady(wc, network); - } - }; - - // Set a flag to avoid calling onAbort or onConnectionChange when the user is leaving the page - var isIntentionallyLeaving = false; - window.addEventListener("beforeunload", function () { - isIntentionallyLeaving = true; - }); - - var findChannelById = function(webChannels, channelId) { - var webChannel; - - // Array.some terminates once a truthy value is returned - // best case is faster than forEach, though webchannel arrays seem - // to consistently have a length of 1 - webChannels.some(function(chan) { - if(chan.id === channelId) { webChannel = chan; return true;} + chainpad.onPatch(function () { + onRemote({ realtime: chainpad }); }); - return webChannel; - }; - - var onConnectError = function (err) { - if (config.onError) { - config.onError({ - error: err.type - }); - } - }; - - var joinSession = function (endPoint, cb) { - // a websocket URL has been provided - // connect to it with Netflux. - if (typeof(endPoint) === 'string') { - Netflux.connect(endPoint).then(cb, onConnectError); - } else if (typeof(endPoint.then) === 'function') { - // a netflux network promise was provided - // connect to it and use a channel - endPoint.then(cb, onConnectError); - } else { - // assume it's a network and try to connect. - cb(endPoint); - } - }; - - var firstConnection = true; - /* Connect to the Netflux network, or fall back to a WebSocket - in theory this lets us connect to more netflux channels using only - one network. */ - var connectTo = function (network) { - // join the netflux network, promise to handle opening of the channel - network.join(channel || null).then(function(wc) { - onOpen(wc, network, firstConnection); - firstConnection = false; - }, function(error) { - console.error(error); + onInit({ + myID: content.myID, + realtime: chainpad, + userList: userList, + readOnly: readOnly }); - }; - - joinSession(network || websocketUrl, function (network) { - // pass messages that come out of netflux into our local handler - if (firstConnection) { - toReturn.network = network; - - network.on('disconnect', function (reason) { - if (isIntentionallyLeaving) { return; } - if (reason === "network.disconnect() called") { return; } - if (config.onConnectionChange) { - config.onConnectionChange({ - state: false - }); - return; - } - if (config.onAbort) { - config.onAbort({ - reason: reason - }); - } - }); - - network.on('reconnect', function (uid) { - if (config.onConnectionChange) { - config.onConnectionChange({ - state: true, - myId: uid - }); - var afterReconnecting = function () { - initializing = true; - userList.users=[]; - joinSession(network, connectTo); - }; - if (config.beforeReconnecting) { - config.beforeReconnecting(function (newKey, newContent) { - channel = newKey; - config.initialState = newContent; - afterReconnecting(); - }); - return; - } - afterReconnecting(); - } - }); - - network.on('message', function (msg, sender) { // Direct message - var wchan = findChannelById(network.webChannels, channel); - if(wchan) { - onMessage(sender, msg, wchan, network, true); - } - }); + }); + SFrameChannel.on('Q_RT_MESSAGE', function (content, cb) { + if (isReady) { + onLocal(); // should be onBeforeMessage } - - connectTo(network); - }, onConnectError); - - return toReturn; + chainpad.message(content); + cb('OK'); + }); + SFrameChannel.on('EV_RT_READY', function () { + if (isReady) { return; } + isReady = true; + chainpad.start(); + setMyID({ myID: myID }); + // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced + if (!readOnly) { userList.onJoin(myID); } + onReady({ realtime: chainpad }); + }); + return; }; return module.exports; -}); +}); \ No newline at end of file diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index c27be9021..d28fbe631 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -15,10 +15,8 @@ * along with this program. If not, see . */ define([ - '/bower_components/netflux-websocket/netflux-client.js', - '/bower_components/chainpad/chainpad.dist.js', -], function (Netflux) { - var ChainPad = window.ChainPad; + '/common/sframe-channel.js', +], function (SFrameChannel) { var USE_HISTORY = true; var module = { exports: {} }; @@ -27,50 +25,53 @@ define([ var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; - module.exports.start = function (conf) { - var websocketUrl = conf.websocketURL; - var userName = conf.userName; + var start = function (conf) { var channel = conf.channel; var Crypto = conf.crypto; var validateKey = conf.validateKey; var readOnly = conf.readOnly || false; - var websocketURL = conf.websocketURL; var network = conf.network; conf = undefined; var initializing = true; - var toReturn = {}; - var messagesHistory = []; - var chainpadAdapter = {}; - var realtime; var lastKnownHash; - var onReady = function(wc, network) { + var queue = []; + var messageFromInner = function (m, cb) { queue.push([ m, cb ]); }; + SFrameChannel.on('Q_RT_MESSAGE', function (message, cb) { + messageFromInner(message, cb); + }); + + var onReady = function(wc) { // Trigger onReady only if not ready yet. This is important because the history keeper sends a direct // message through "network" when it is synced, and it triggers onReady for each channel joined. if (!initializing) { return; } - - realtime.start(); - - if(setMyID) { - setMyID({ myID: wc.myID }); - } - // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced - if (!readOnly) { - onJoining(wc.myID); - } - + SFrameChannel.event('EV_RT_READY', null); // we're fully synced initializing = false; + }; - if (config.onReady) { - config.onReady({ - realtime: realtime, - network: network, - userList: userList, - myId: wc.myID, - leave: wc.leave - }); + // shim between chainpad and netflux + var msgIn = function (peerId, msg) { + msg = msg.replace(/^cp\|/, ''); + try { + var decryptedMsg = Crypto.decrypt(msg, validateKey); + return decryptedMsg; + } catch (err) { + console.error(err); + return msg; + } + }; + + var msgOut = function (msg) { + if (readOnly) { return; } + try { + var cmsg = Crypto.encrypt(msg); + if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } + return cmsg; + } catch (err) { + console.log(msg); + throw err; } }; @@ -78,11 +79,6 @@ define([ // unpack the history keeper from the webchannel var hk = network.historyKeeper; - // Old server - if(wc && (msg === 0 || msg === '0')) { - onReady(wc, network); - return; - } if (direct && peer !== hk) { return; } @@ -98,7 +94,7 @@ define([ } if (parsed.state && parsed.state === 1 && parsed.channel) { if (parsed.channel === wc.id) { - onReady(wc, network); + onReady(wc); } // We have to return even if it is not the current channel: // we don't want to continue with other channels messages here @@ -107,7 +103,7 @@ define([ } // The history keeper is different for each channel : // no need to check if the message is related to the current channel - if (peer === hk){ + if (peer === hk) { // if the peer is the 'history keeper', extract their message var parsed1 = JSON.parse(msg); msg = parsed1[4]; @@ -116,146 +112,58 @@ define([ } lastKnownHash = msg.slice(0,64); - var message = chainpadAdapter.msgIn(peer, msg); + var message = msgIn(peer, msg); verbose(message); - if (!initializing) { - if (config.onLocal) { - config.onLocal(); - } - } - // slice off the bencoded header // Why are we getting bencoded stuff to begin with? // FIXME this shouldn't be necessary message = unBencode(message);//.slice(message.indexOf(':[') + 1); // pass the message into Chainpad - realtime.message(message); - }; - - // update UI components to show that one of the other peers has left - var onLeaving = function(peer) { - var list = userList.users; - var index = list.indexOf(peer); - if(index !== -1) { - userList.users.splice(index, 1); - } - userList.onChange(); - }; - - // shim between chainpad and netflux - chainpadAdapter = { - msgIn : function(peerId, msg) { - msg = msg.replace(/^cp\|/, ''); - try { - var decryptedMsg = Crypto.decrypt(msg, validateKey); - messagesHistory.push(decryptedMsg); - return decryptedMsg; - } catch (err) { - console.error(err); - return msg; - } - }, - msgOut : function(msg) { - if (readOnly) { return; } - try { - var cmsg = Crypto.encrypt(msg); - if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } - return cmsg; - } catch (err) { - console.log(msg); - throw err; - } - } - }; - - var createRealtime = function() { - return ChainPad.create({ - userName: userName, - initialState: config.initialState, - transformFunction: config.transformFunction, - validateContent: config.validateContent, - avgSyncMilliseconds: config.avgSyncMilliseconds, - logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1 - }); + SFrameChannel.query('Q_RT_MESSAGE', message, function () { }); }; // We use an object to store the webchannel so that we don't have to push new handlers to chainpad // and remove the old ones when reconnecting and keeping the same 'realtime' object // See realtime.onMessage below: we call wc.bcast(...) but wc may change var wcObject = {}; - var onOpen = function(wc, network, initialize) { + var onOpen = function(wc, network, firstConnection) { wcObject.wc = wc; channel = wc.id; // Add the existing peers in the userList - wc.members.forEach(onJoining); + SFrameChannel.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly }); // Add the handlers to the WebChannel wc.on('message', function (msg, sender) { //Channel msg onMessage(sender, msg, wc, network); }); - wc.on('join', onJoining); - wc.on('leave', onLeaving); - - if (initialize) { - toReturn.realtime = realtime = createRealtime(); - - realtime._patch = realtime.patch; - realtime.patch = function (patch, x, y) { - if (initializing) { - console.error("attempted to change the content before chainpad was synced"); - } - return realtime._patch(patch, x, y); - }; - realtime._change = realtime.change; - realtime.change = function (offset, count, chars) { - if (initializing) { - console.error("attempted to change the content before chainpad was synced"); - } - return realtime._change(offset, count, chars); - }; - - if (config.onInit) { - config.onInit({ - myID: wc.myID, - realtime: realtime, - getLag: network.getLag, - userList: userList, - network: network, - channel: channel - }); - } + wc.on('join', function (m) { SFrameChannel.event('EV_RT_JOIN', m); }); + wc.on('leave', function (m) { SFrameChannel.event('EV_RT_LEAVE', m); }); + if (firstConnection) { // Sending a message... - realtime.onMessage(function(message, cb) { + messageFromInner = function(message, cb) { // Filter messages sent by Chainpad to make it compatible with Netflux - message = chainpadAdapter.msgOut(message); - if(message) { + message = msgOut(message); + if (message) { // Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we // want to keep the same chainpad (realtime) object wcObject.wc.bcast(message).then(function() { - cb(); + cb('OK'); }, function(err) { // The message has not been sent, display the error. console.error(err); }); } - }); - - realtime.onPatch(function () { - if (config.onRemote) { - config.onRemote({ - realtime: realtime - }); - } - }); + }; + queue.forEach(function (arr) { messageFromInner(arr[0], arr[1]); }); } // Get the channel history - if(USE_HISTORY) { + if (USE_HISTORY) { var hk; wc.members.forEach(function (p) { @@ -268,19 +176,17 @@ define([ msg.push(validateKey); msg.push(lastKnownHash); if (hk) { network.sendto(hk, JSON.stringify(msg)); } - } - else { - onReady(wc, network); + } else { + onReady(wc); } }; - // Set a flag to avoid calling onAbort or onConnectionChange when the user is leaving the page var isIntentionallyLeaving = false; window.addEventListener("beforeunload", function () { isIntentionallyLeaving = true; }); - var findChannelById = function(webChannels, channelId) { + var findChannelById = function (webChannels, channelId) { var webChannel; // Array.some terminates once a truthy value is returned @@ -292,99 +198,39 @@ define([ return webChannel; }; - var onConnectError = function (err) { - if (config.onError) { - config.onError({ - error: err.type - }); - } - }; - - var joinSession = function (endPoint, cb) { - // a websocket URL has been provided - // connect to it with Netflux. - if (typeof(endPoint) === 'string') { - Netflux.connect(endPoint).then(cb, onConnectError); - } else if (typeof(endPoint.then) === 'function') { - // a netflux network promise was provided - // connect to it and use a channel - endPoint.then(cb, onConnectError); - } else { - // assume it's a network and try to connect. - cb(endPoint); - } - }; - - var firstConnection = true; - /* Connect to the Netflux network, or fall back to a WebSocket - in theory this lets us connect to more netflux channels using only - one network. */ - var connectTo = function (network) { + var connectTo = function (network, firstConnection) { // join the netflux network, promise to handle opening of the channel network.join(channel || null).then(function(wc) { onOpen(wc, network, firstConnection); - firstConnection = false; }, function(error) { console.error(error); }); }; - joinSession(network || websocketUrl, function (network) { - // pass messages that come out of netflux into our local handler - if (firstConnection) { - toReturn.network = network; - - network.on('disconnect', function (reason) { - if (isIntentionallyLeaving) { return; } - if (reason === "network.disconnect() called") { return; } - if (config.onConnectionChange) { - config.onConnectionChange({ - state: false - }); - return; - } - if (config.onAbort) { - config.onAbort({ - reason: reason - }); - } - }); + network.on('disconnect', function (reason) { + if (isIntentionallyLeaving) { return; } + if (reason === "network.disconnect() called") { return; } + SFrameChannel.event('EV_RT_DISCONNECT'); + }); - network.on('reconnect', function (uid) { - if (config.onConnectionChange) { - config.onConnectionChange({ - state: true, - myId: uid - }); - var afterReconnecting = function () { - initializing = true; - userList.users=[]; - joinSession(network, connectTo); - }; - if (config.beforeReconnecting) { - config.beforeReconnecting(function (newKey, newContent) { - channel = newKey; - config.initialState = newContent; - afterReconnecting(); - }); - return; - } - afterReconnecting(); - } - }); + network.on('reconnect', function (uid) { + initializing = true; + connectTo(network, false); + }); - network.on('message', function (msg, sender) { // Direct message - var wchan = findChannelById(network.webChannels, channel); - if(wchan) { - onMessage(sender, msg, wchan, network, true); - } - }); + network.on('message', function (msg, sender) { // Direct message + var wchan = findChannelById(network.webChannels, channel); + if (wchan) { + onMessage(sender, msg, wchan, network, true); } + }); - connectTo(network); - }, onConnectError); + connectTo(network, true); + }; - return toReturn; + return { + start: function (config) { + SFrameChannel.whenReg('EV_RT_READY', function () { start(config); }); + } }; - return module.exports; }); diff --git a/www/common/sframe-channel.js b/www/common/sframe-channel.js index 4291f5108..793906c5a 100644 --- a/www/common/sframe-channel.js +++ b/www/common/sframe-channel.js @@ -1,38 +1,108 @@ -// This file provides the internal API for talking from inside of the sandbox iframe -// The external API is in sframe-ctrl.js -define([], function () { - var iframe; +// This file provides the API for the channel for talking to and from the sandbox iframe. +define([ + '/common/sframe-protocol.js' +], function (SFrameProtocol) { + var otherWindow; var handlers = {}; var queries = {}; + + // list of handlers which are registered from the other side... + var insideHandlers = []; + var callWhenRegistered = {}; + var module = { exports: {} }; var mkTxid = function () { return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', ''); }; + module.exports.init = function (ow, cb) { + if (otherWindow) { throw new Error('already initialized'); } + var intr; + var txid; + window.addEventListener('message', function (msg) { + var data = JSON.parse(msg.data); + if (ow !== msg.source) { + console.log("DROP Message from unexpected source"); + console.log(msg); + } else if (!otherWindow) { + if (data.txid !== txid) { + console.log("DROP Message with weird txid"); + return; + } + clearInterval(intr); + otherWindow = ow; + cb(); + } else if (typeof(data.q) === 'string' && handlers[data.q]) { + handlers[data.q](data, msg); + } else if (typeof(data.q) === 'undefined' && queries[data.txid]) { + queries[data.txid](data, msg); + } else if (data.txid === txid) { + // stray message from init + return; + } else { + console.log("DROP Unhandled message"); + console.log(msg); + } + }); + if (window !== window.top) { + // we're in the sandbox + otherWindow = ow; + cb(); + } else { + require(['/common/requireconfig.js'], function (RequireConfig) { + txid = mkTxid(); + intr = setInterval(function () { + ow.postMessage(JSON.stringify({ + txid: txid, + content: { requireConf: RequireConfig }, + q: 'INIT' + }), '*'); + }); + }); + } + }; + module.exports.query = function (q, content, cb) { - if (!iframe) { throw new Error('not yet initialized'); } + if (!otherWindow) { throw new Error('not yet initialized'); } + if (!SFrameProtocol[q]) { + throw new Error('please only make queries are defined in sframe-protocol.js'); + } var txid = mkTxid(); var timeout = setTimeout(function () { delete queries[txid]; - cb("Timeout making query " + q); - }); + console.log("Timeout making query " + q); + }, 30000); queries[txid] = function (data, msg) { clearTimeout(timeout); delete queries[txid]; cb(undefined, data.content, msg); }; - iframe.contentWindow.postMessage(JSON.stringify({ + otherWindow.postMessage(JSON.stringify({ txid: txid, content: content, q: q }), '*'); }; - module.exports.registerHandler = function (queryType, handler) { + var event = module.exports.event = function (e, content) { + if (!otherWindow) { throw new Error('not yet initialized'); } + if (!SFrameProtocol[e]) { + throw new Error('please only fire events that are defined in sframe-protocol.js'); + } + if (e.indexOf('EV_') !== 0) { + throw new Error('please only use events (starting with EV_) for event messages'); + } + otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*'); + }; + + module.exports.on = function (queryType, handler) { + if (!otherWindow) { throw new Error('not yet initialized'); } if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } - handlers[queryType] = function (msg) { - var data = JSON.parse(msg.data); + if (!SFrameProtocol[queryType]) { + throw new Error('please only register handlers which are defined in sframe-protocol.js'); + } + handlers[queryType] = function (data, msg) { handler(data.content, function (replyContent) { msg.source.postMessage(JSON.stringify({ txid: data.txid, @@ -40,7 +110,28 @@ define([], function () { }), '*'); }, msg); }; + event('EV_REGISTER_HANDLER', queryType); + }; + + module.exports.whenReg = function (queryType, handler) { + if (!otherWindow) { throw new Error('not yet initialized'); } + if (!SFrameProtocol[queryType]) { + throw new Error('please only register handlers which are defined in sframe-protocol.js'); + } + if (insideHandlers.indexOf(queryType) > -1) { + handler(); + } else { + (callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(handler); + } + }; + + handlers['EV_REGISTER_HANDLER'] = function (data) { + if (callWhenRegistered[data.content]) { + callWhenRegistered[data.content].forEach(function (f) { f(); }); + delete callWhenRegistered[data.content]; + } + insideHandlers.push(data.content); }; return module.exports; -}); \ No newline at end of file +}); diff --git a/www/common/sframe-ctrl.js b/www/common/sframe-ctrl.js deleted file mode 100644 index ebae43985..000000000 --- a/www/common/sframe-ctrl.js +++ /dev/null @@ -1,76 +0,0 @@ -// This file provides the external API for launching and talking to the sandboxed iframe. -// The internal API is in sframe-channel.js -define([ - '/common/requireconfig.js' -], function (RequireConfig) { - var iframe; - var handlers = {}; - var queries = {}; - var module = { exports: {} }; - - var mkTxid = function () { - return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', ''); - }; - - module.exports.init = function (frame, cb) { - if (iframe) { throw new Error('already initialized'); } - var txid = mkTxid(); - var intr = setInterval(function () { - frame.contentWindow.postMessage(JSON.stringify({ - txid: txid, - content: { requireConf: RequireConfig }, - q: 'INIT' - }), '*'); - }); - window.addEventListener('message', function (msg) { - var data = JSON.parse(msg.data); - if (!iframe) { - if (data.txid !== txid) { return; } - clearInterval(intr); - iframe = frame; - cb(); - } else if (typeof(data.q) === 'string' && handlers[data.q]) { - handlers[data.q](data, msg); - } else if (typeof(data.q) === 'undefined' && queries[data.txid]) { - queries[data.txid](data, msg); - } else { - console.log("Unhandled message"); - console.log(msg); - } - }); - }; - - module.exports.query = function (q, content, cb) { - if (!iframe) { throw new Error('not yet initialized'); } - var txid = mkTxid(); - var timeout = setTimeout(function () { - delete queries[txid]; - cb("Timeout making query " + q); - }); - queries[txid] = function (data, msg) { - clearTimeout(timeout); - delete queries[txid]; - cb(undefined, data.content, msg); - }; - iframe.contentWindow.postMessage(JSON.stringify({ - txid: txid, - content: content, - q: q - }), '*'); - }; - - module.exports.registerHandler = function (queryType, handler) { - if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } - handlers[queryType] = function (msg) { - var data = JSON.parse(msg.data); - handler(data.content, function (replyContent) { - msg.source.postMessage(JSON.stringify({ - txid: data.txid, - content: replyContent - }), '*'); - }, msg); - }; - }; - - return module.exports; -}); diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index 56d32601a..503dc2983 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -1,5 +1,33 @@ -// This file defines all of the RPC calls -// The internal API is in sframe-channel.js +// This file defines all of the RPC calls which are used between the inner and outer iframe. +// Define *querys* (which expect a response) using Q_ +// Define *events* (which expect no response) using EV_ +// Please document the queries and events you create, and please please avoid making generic +// "do stuff" events/queries which are used for many different things because it makes the +// protocol unclear. define({ + // When the iframe first launches, this query is sent repeatedly by the controller + // to wait for it to awake and give it the requirejs config to use. + 'Q_INIT': true, + // When either the outside or inside registers a query handler, this is sent. + 'EV_REGISTER_HANDLER': true, + + // Realtime events called from the outside. + // When someone joins the pad, argument is a string with their netflux id. + 'EV_RT_JOIN': true, + // When someone leaves the pad, argument is a string with their netflux id. + 'EV_RT_LEAVE': true, + // When you have been disconnected, no arguments. + 'EV_RT_DISCONNECT': true, + // When you have connected, argument is an object with myID: string, members: list, readOnly: boolean. + 'EV_RT_CONNECT': true, + // Called after the history is finished synchronizing, no arguments. + 'EV_RT_READY': true, + // Called from both outside and inside, argument is a (string) chainpad message. + 'Q_RT_MESSAGE': true, + + // Called from the outside, this informs the inside whenever the user's data has been changed. + // The argument is the object representing the content of the user profile minus the netfluxID + // which changes per-reconnect. + 'EV_USERDATA_UPDATE': true }); \ No newline at end of file diff --git a/www/pad2/main.js b/www/pad2/main.js index ce8db2010..bf92010e8 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -13,6 +13,7 @@ define([ '/common/cryptpad-common.js', '/common/cryptget.js', '/pad/links.js', + '/bower_components/nthen/index.js', '/bower_components/file-saver/FileSaver.min.js', '/bower_components/diff-dom/diffDOM.js', @@ -21,18 +22,12 @@ define([ 'less!/customize/src/less/cryptpad.less', 'less!/customize/src/less/toolbar.less' ], function ($, Crypto, realtimeInput, Hyperjson, - Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links) { + Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links, nThen) { var saveAs = window.saveAs; var Messages = Cryptpad.Messages; - - console.log('two'); - - var Ckeditor; // to be initialized later... var DiffDom = window.diffDOM; - var stringify = function (obj) { - return JSONSortify(obj); - }; + var stringify = function (obj) { return JSONSortify(obj); }; window.Toolbar = Toolbar; window.Hyperjson = Hyperjson; @@ -89,7 +84,7 @@ define([ Cryptpad.errorLoadingScreen(Messages.websocketError); }; - var andThen = function (Ckeditor) { + var andThen = function (editor) { //var $iframe = $('#pad-iframe').contents(); //var secret = Cryptpad.getSecrets(); //var readOnly = secret.keys && !secret.keys.editKeyStr; @@ -98,645 +93,665 @@ define([ //} var readOnly = false; // TODO - var editor = window.editor = Ckeditor.replace('editor1', { - customConfig: '/customize/ckeditor-config.js', - }); - - editor.on('instanceReady', Links.addSupportForOpeningLinksInNewTab(Ckeditor)); - editor.on('instanceReady', function () { - var $bar = $('#cke_1_toolbox'); - - var $html = $bar.closest('html'); - var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); - if ($faLink.length) { - $html.find('iframe').contents().find('head').append($faLink.clone()); - } - var isHistoryMode = false; - - if (readOnly) { - $('#cke_1_toolbox > .cke_toolbox_main').hide(); - } - - /* add a class to the magicline plugin so we can pick it out more easily */ - var ml = window.CKEDITOR.instances.editor1.plugins.magicline.backdoor.that.line.$; + var $bar = $('#cke_1_toolbox'); - [ml, ml.parentElement].forEach(function (el) { - el.setAttribute('class', 'non-realtime'); - }); + var $html = $bar.closest('html'); + var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); + if ($faLink.length) { + $html.find('iframe').contents().find('head').append($faLink.clone()); + } + var isHistoryMode = false; - var documentBody = document.body; + if (readOnly) { + $('#cke_1_toolbox > .cke_toolbox_main').hide(); + } - var inner = window.inner = documentBody; + /* add a class to the magicline plugin so we can pick it out more easily */ - // hide all content until the realtime doc is ready - $(inner).css({ - color: '#fff', - }); + var ml = window.CKEDITOR.instances.editor1.plugins.magicline.backdoor.that.line.$; - var cursor = module.cursor = Cursor(inner); + [ml, ml.parentElement].forEach(function (el) { + el.setAttribute('class', 'non-realtime'); + }); - var setEditable = module.setEditable = function (bool) { - if (bool) { - $(inner).css({ - color: '#333', - }); - } - if (!readOnly || !bool) { - inner.setAttribute('contenteditable', bool); - } - }; + var documentBody = $html.find('iframe')[0].contentWindow.document.body; - // don't let the user edit until the pad is ready - setEditable(false); + var inner = window.inner = documentBody; - var forbiddenTags = [ - 'SCRIPT', - 'IFRAME', - 'OBJECT', - 'APPLET', - 'VIDEO', - 'AUDIO' - ]; - - var diffOptions = { - preDiffApply: function (info) { - /* - Don't accept attributes that begin with 'on' - these are probably listeners, and we don't want to - send scripts over the wire. - */ - if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) { - if (info.diff.name === 'href') { - // console.log(info.diff); - //var href = info.diff.newValue; - - // TODO normalize HTML entities - if (/javascript *: */.test(info.diff.newValue)) { - // TODO remove javascript: links - } - } + var cursor = module.cursor = Cursor(inner); - if (/^on/.test(info.diff.name)) { - console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name); - return true; - } - } - /* - Also reject any elements which would insert any one of - our forbidden tag types: script, iframe, object, - applet, video, or audio - */ - if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) { - if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) { - console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName); - return true; - } else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) { - console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName); - return true; + var setEditable = module.setEditable = function (bool) { + if (bool) { + $(inner).css({ + color: '#333', + }); + } + if (!readOnly || !bool) { + inner.setAttribute('contenteditable', bool); + } + }; + + // don't let the user edit until the pad is ready + setEditable(false); + + var forbiddenTags = [ + 'SCRIPT', + 'IFRAME', + 'OBJECT', + 'APPLET', + 'VIDEO', + 'AUDIO' + ]; + + var diffOptions = { + preDiffApply: function (info) { + /* + Don't accept attributes that begin with 'on' + these are probably listeners, and we don't want to + send scripts over the wire. + */ + if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) { + if (info.diff.name === 'href') { + // console.log(info.diff); + //var href = info.diff.newValue; + + // TODO normalize HTML entities + if (/javascript *: */.test(info.diff.newValue)) { + // TODO remove javascript: links } } - if (info.node && info.node.tagName === 'BODY') { - if (info.diff.action === 'removeAttribute' && - ['class', 'spellcheck'].indexOf(info.diff.name) !== -1) { - return true; - } + if (/^on/.test(info.diff.name)) { + console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name); + return true; + } + } + /* + Also reject any elements which would insert any one of + our forbidden tag types: script, iframe, object, + applet, video, or audio + */ + if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) { + if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) { + console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName); + return true; + } else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) { + console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName); + return true; } + } - /* DiffDOM will filter out magicline plugin elements - in practice this will make it impossible to use it - while someone else is typing, which could be annoying. - - we should check when such an element is going to be - removed, and prevent that from happening. */ - if (info.node && info.node.tagName === 'SPAN' && - info.node.getAttribute('contentEditable') === "false") { - // it seems to be a magicline plugin element... - if (info.diff.action === 'removeElement') { - // and you're about to remove it... - // this probably isn't what you want - - /* - I have never seen this in the console, but the - magic line is still getting removed on remote - edits. This suggests that it's getting removed - by something other than diffDom. - */ - console.log("preventing removal of the magic line!"); - - // return true to prevent diff application - return true; - } + if (info.node && info.node.tagName === 'BODY') { + if (info.diff.action === 'removeAttribute' && + ['class', 'spellcheck'].indexOf(info.diff.name) !== -1) { + return true; } + } - // Do not change the contenteditable value in view mode - if (readOnly && info.node && info.node.tagName === 'BODY' && - info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') { + /* DiffDOM will filter out magicline plugin elements + in practice this will make it impossible to use it + while someone else is typing, which could be annoying. + + we should check when such an element is going to be + removed, and prevent that from happening. */ + if (info.node && info.node.tagName === 'SPAN' && + info.node.getAttribute('contentEditable') === "false") { + // it seems to be a magicline plugin element... + if (info.diff.action === 'removeElement') { + // and you're about to remove it... + // this probably isn't what you want + + /* + I have never seen this in the console, but the + magic line is still getting removed on remote + edits. This suggests that it's getting removed + by something other than diffDom. + */ + console.log("preventing removal of the magic line!"); + + // return true to prevent diff application return true; } + } - // no use trying to recover the cursor if it doesn't exist - if (!cursor.exists()) { return; } + // Do not change the contenteditable value in view mode + if (readOnly && info.node && info.node.tagName === 'BODY' && + info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') { + return true; + } - /* frame is either 0, 1, 2, or 3, depending on which - cursor frames were affected: none, first, last, or both - */ - var frame = info.frame = cursor.inNode(info.node); + // no use trying to recover the cursor if it doesn't exist + if (!cursor.exists()) { return; } - if (!frame) { return; } + /* frame is either 0, 1, 2, or 3, depending on which + cursor frames were affected: none, first, last, or both + */ + var frame = info.frame = cursor.inNode(info.node); - if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') { - var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue); + if (!frame) { return; } - if (frame & 1) { - // push cursor start if necessary - if (pushes.commonStart < cursor.Range.start.offset) { - cursor.Range.start.offset += pushes.delta; - } - } - if (frame & 2) { - // push cursor end if necessary - if (pushes.commonStart < cursor.Range.end.offset) { - cursor.Range.end.offset += pushes.delta; - } + if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') { + var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue); + + if (frame & 1) { + // push cursor start if necessary + if (pushes.commonStart < cursor.Range.start.offset) { + cursor.Range.start.offset += pushes.delta; } } - }, - postDiffApply: function (info) { - if (info.frame) { - if (info.node) { - if (info.frame & 1) { cursor.fixStart(info.node); } - if (info.frame & 2) { cursor.fixEnd(info.node); } - } else { console.error("info.node did not exist"); } - - var sel = cursor.makeSelection(); - var range = cursor.makeRange(); - - cursor.fixSelection(sel, range); + if (frame & 2) { + // push cursor end if necessary + if (pushes.commonStart < cursor.Range.end.offset) { + cursor.Range.end.offset += pushes.delta; + } } } - }; + }, + postDiffApply: function (info) { + if (info.frame) { + if (info.node) { + if (info.frame & 1) { cursor.fixStart(info.node); } + if (info.frame & 2) { cursor.fixEnd(info.node); } + } else { console.error("info.node did not exist"); } + + var sel = cursor.makeSelection(); + var range = cursor.makeRange(); + + cursor.fixSelection(sel, range); + } + } + }; - var initializing = true; + var initializing = true; - var Title; - var UserList; - var Metadata; + var Title; + var UserList; + var Metadata; - var getHeadingText = function () { - var text; - if (['h1', 'h2', 'h3'].some(function (t) { - var $header = $(inner).find(t + ':first-of-type'); - if ($header.length && $header.text()) { - text = $header.text(); - return true; - } - })) { return text; } - }; + var getHeadingText = function () { + var text; + if (['h1', 'h2', 'h3'].some(function (t) { + var $header = $(inner).find(t + ':first-of-type'); + if ($header.length && $header.text()) { + text = $header.text(); + return true; + } + })) { return text; } + }; - var DD = new DiffDom(diffOptions); + var DD = new DiffDom(diffOptions); - var openLink = function (e) { - var el = e.currentTarget; - if (!el || el.nodeName !== 'A') { return; } - var href = el.getAttribute('href'); - if (href) { window.open(href, '_blank'); } - }; + var openLink = function (e) { + var el = e.currentTarget; + if (!el || el.nodeName !== 'A') { return; } + var href = el.getAttribute('href'); + if (href) { window.open(href, '_blank'); } + }; - // apply patches, and try not to lose the cursor in the process! - var applyHjson = function (shjson) { - var userDocStateDom = hjsonToDom(JSON.parse(shjson)); + // apply patches, and try not to lose the cursor in the process! + var applyHjson = function (shjson) { + var userDocStateDom = hjsonToDom(JSON.parse(shjson)); - if (!readOnly && !initializing) { - userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf - } - var patch = (DD).diff(inner, userDocStateDom); - (DD).apply(inner, patch); - if (readOnly) { - var $links = $(inner).find('a'); - // off so that we don't end up with multiple identical handlers - $links.off('click', openLink).on('click', openLink); - } - }; + if (!readOnly && !initializing) { + userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf + } + var patch = (DD).diff(inner, userDocStateDom); + (DD).apply(inner, patch); + if (readOnly) { + var $links = $(inner).find('a'); + // off so that we don't end up with multiple identical handlers + $links.off('click', openLink).on('click', openLink); + } + }; - var stringifyDOM = module.stringifyDOM = function (dom) { - var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); - hjson[3] = { - metadata: { - users: UserList.userData, - defaultTitle: Title.defaultTitle, - type: 'pad' - } - }; - if (!initializing) { - hjson[3].metadata.title = Title.title; - } else if (Cryptpad.initialName && !hjson[3].metadata.title) { - hjson[3].metadata.title = Cryptpad.initialName; + var stringifyDOM = module.stringifyDOM = function (dom) { + var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); + + /*hjson[3] = { TODO + users: UserList.userData, + defaultTitle: Title.defaultTitle, + type: 'pad' } - return stringify(hjson); - }; + };*/ + if (!initializing) { + //TODO hjson[3].metadata.title = Title.title; + } else if (Cryptpad.initialName && !hjson[3].metadata.title) { + hjson[3].metadata.title = Cryptpad.initialName; + } + return stringify(hjson); + }; - var realtimeOptions = { - // the websocket URL - websocketURL: Cryptpad.getWebsocketURL(), + var realtimeOptions = { + // the websocket URL + websocketURL: Cryptpad.getWebsocketURL(), - // the channel we will communicate over - channel: 'x',//secret.channel, + // the channel we will communicate over + channel: 'x',//secret.channel, - // the nework used for the file store if it exists - network: Cryptpad.getNetwork(), + // the nework used for the file store if it exists + network: Cryptpad.getNetwork(), - // our public key - validateKey: undefined,//secret.keys.validateKey || undefined, - readOnly: readOnly, + // our public key + validateKey: undefined,//secret.keys.validateKey || undefined, + readOnly: readOnly, - // Pass in encrypt and decrypt methods - crypto: undefined,//Crypto.createEncryptor(secret.keys), + // Pass in encrypt and decrypt methods + crypto: undefined,//Crypto.createEncryptor(secret.keys), - // really basic operational transform - transformFunction : JsonOT.validate, + // really basic operational transform + transformFunction : JsonOT.validate, - // cryptpad debug logging (default is 1) - // logLevel: 0, + // cryptpad debug logging (default is 1) + // logLevel: 0, - validateContent: function (content) { - try { - JSON.parse(content); - return true; - } catch (e) { - console.log("Failed to parse, rejecting patch"); - return false; - } + validateContent: function (content) { + try { + JSON.parse(content); + return true; + } catch (e) { + console.log("Failed to parse, rejecting patch"); + return false; } - }; + } + }; - var setHistory = function (bool, update) { - isHistoryMode = bool; - setEditable(!bool); - if (!bool && update) { - realtimeOptions.onRemote(); - } - }; + var setHistory = function (bool, update) { + isHistoryMode = bool; + setEditable(!bool); + if (!bool && update) { + realtimeOptions.onRemote(); + } + }; - realtimeOptions.onRemote = function () { - if (initializing) { return; } - if (isHistoryMode) { return; } + var meta; + var metaStr; - var oldShjson = stringifyDOM(inner); + realtimeOptions.onRemote = function () { + if (initializing) { return; } + if (isHistoryMode) { return; } - var shjson = module.realtime.getUserDoc(); + var oldShjson = stringifyDOM(inner); - // remember where the cursor is - cursor.update(); + var shjson = module.realtime.getUserDoc(); - // Update the user list (metadata) from the hyperjson - Metadata.update(shjson); + // remember where the cursor is + cursor.update(); - var newInner = JSON.parse(shjson); - var newSInner; - if (newInner.length > 2) { - newSInner = stringify(newInner[2]); - } + // Update the user list (metadata) from the hyperjson + // TODO Metadata.update(shjson); - // build a dom from HJSON, diff, and patch the editor - applyHjson(shjson); + var newInner = JSON.parse(shjson); + var newSInner; + if (newInner.length > 2) { + newSInner = stringify(newInner[2]); + } - if (!readOnly) { - var shjson2 = stringifyDOM(inner); - if (shjson2 !== shjson) { - console.error("shjson2 !== shjson"); - module.patchText(shjson2); - - /* pushing back over the wire is necessary, but it can - result in a feedback loop, which we call a browser - fight */ - if (module.logFights) { - // what changed? - var op = TextPatcher.diff(shjson, shjson2); - // log the changes - TextPatcher.log(shjson, op); - var sop = JSON.stringify(TextPatcher.format(shjson, op)); - - var index = module.fights.indexOf(sop); - if (index === -1) { - module.fights.push(sop); - console.log("Found a new type of browser disagreement"); - console.log("You can inspect the list in your " + - "console at `REALTIME_MODULE.fights`"); - console.log(module.fights); - } else { - console.log("Encountered a known browser disagreement: " + - "available at `REALTIME_MODULE.fights[%s]`", index); - } + // build a dom from HJSON, diff, and patch the editor + applyHjson(shjson); + + if (!readOnly) { + var shjson2 = stringifyDOM(inner); + + // TODO + //shjson = JSON.stringify(JSON.parse(shjson).slice(0,3)); + + if (shjson2 !== shjson) { + console.error("shjson2 !== shjson"); + module.patchText(shjson2); + + /* pushing back over the wire is necessary, but it can + result in a feedback loop, which we call a browser + fight */ + if (module.logFights) { + // what changed? + var op = TextPatcher.diff(shjson, shjson2); + // log the changes + TextPatcher.log(shjson, op); + var sop = JSON.stringify(TextPatcher.format(shjson, op)); + + var index = module.fights.indexOf(sop); + if (index === -1) { + module.fights.push(sop); + console.log("Found a new type of browser disagreement"); + console.log("You can inspect the list in your " + + "console at `REALTIME_MODULE.fights`"); + console.log(module.fights); + } else { + console.log("Encountered a known browser disagreement: " + + "available at `REALTIME_MODULE.fights[%s]`", index); } } } + } - // Notify only when the content has changed, not when someone has joined/left - var oldSInner = stringify(JSON.parse(oldShjson)[2]); - if (newSInner && newSInner !== oldSInner) { - Cryptpad.notify(); - } - }; - - var getHTML = function () { - return ('\n' + '\n' + inner.innerHTML); - }; + // Notify only when the content has changed, not when someone has joined/left + var oldSInner = stringify(JSON.parse(oldShjson)[2]); + if (newSInner && newSInner !== oldSInner) { + Cryptpad.notify(); + } - var domFromHTML = function (html) { - return new DOMParser().parseFromString(html, 'text/html'); - }; + var newMeta = newInner[3]; + var newMetaStr = JSON.stringify(newMeta); + if (newMetaStr !== metaStr) { + metaStr = newMetaStr; + meta = newMeta; + //meta[] HERE + } + }; + + var getHTML = function () { + return ('\n' + '\n' + inner.innerHTML); + }; + + var domFromHTML = function (html) { + return new DOMParser().parseFromString(html, 'text/html'); + }; + + var exportFile = function () { + var html = getHTML(); + var suggestion = Title.suggestTitle('cryptpad-document'); + Cryptpad.prompt(Messages.exportPrompt, + Cryptpad.fixFileName(suggestion) + '.html', function (filename) { + if (!(typeof(filename) === 'string' && filename)) { return; } + var blob = new Blob([html], {type: "text/html;charset=utf-8"}); + saveAs(blob, filename); + }); + }; + var importFile = function (content) { + var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body)); + applyHjson(shjson); + realtimeOptions.onLocal(); + }; - var exportFile = function () { - var html = getHTML(); - var suggestion = Title.suggestTitle('cryptpad-document'); - Cryptpad.prompt(Messages.exportPrompt, - Cryptpad.fixFileName(suggestion) + '.html', function (filename) { - if (!(typeof(filename) === 'string' && filename)) { return; } - var blob = new Blob([html], {type: "text/html;charset=utf-8"}); - saveAs(blob, filename); - }); - }; - var importFile = function (content) { - var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body)); - applyHjson(shjson); - realtimeOptions.onLocal(); - }; + realtimeOptions.onInit = function (info) { - realtimeOptions.onInit = function (info) { - UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad); - - var titleCfg = { getHeadingText: getHeadingText }; - Title = Cryptpad.createTitle(titleCfg, realtimeOptions.onLocal, Cryptpad); - - Metadata = Cryptpad.createMetadata(UserList, Title, null, Cryptpad); - - var configTb = { - displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'], - userList: UserList.getToolbarConfig(), - share: { - secret: secret, - channel: info.channel - }, - title: Title.getTitleConfig(), - common: Cryptpad, - readOnly: readOnly, - ifrw: window, - realtime: info.realtime, - network: info.network, - $container: $bar, - $contentContainer: $('#cke_1_contents'), - }; - toolbar = info.realtime.toolbar = Toolbar.create(configTb); - - var src = 'less!/customize/src/less/toolbar.less'; - require([ - src - ], function () { - var $html = $bar.closest('html'); - $html - .find('head style[data-original-src="' + src.replace(/less!/, '') + '"]') - .appendTo($html.find('head')); - }); + // TODO + return; - Title.setToolbar(toolbar); + UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad); - var $rightside = toolbar.$rightside; - var $drawer = toolbar.$drawer; + var titleCfg = { getHeadingText: getHeadingText }; + Title = Cryptpad.createTitle(titleCfg, realtimeOptions.onLocal, Cryptpad); - var editHash; + Metadata = Cryptpad.createMetadata(UserList, Title, null, Cryptpad); - if (!readOnly) { - editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); - } + var configTb = { + displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'], + userList: UserList.getToolbarConfig(), + share: { + secret: secret, + channel: info.channel + }, + title: Title.getTitleConfig(), + common: Cryptpad, + readOnly: readOnly, + ifrw: window, + realtime: info.realtime, + network: info.network, + $container: $bar, + $contentContainer: $('#cke_1_contents'), + }; + toolbar = info.realtime.toolbar = Toolbar.create(configTb); + + var src = 'less!/customize/src/less/toolbar.less'; + require([ + src + ], function () { + var $html = $bar.closest('html'); + $html + .find('head style[data-original-src="' + src.replace(/less!/, '') + '"]') + .appendTo($html.find('head')); + }); - $bar.find('#cke_1_toolbar_collapser').hide(); - if (!readOnly) { - // Expand / collapse the toolbar - var $collapse = Cryptpad.createButton(null, true); - $collapse.removeClass('fa-question'); - var updateIcon = function () { - $collapse.removeClass('fa-caret-down').removeClass('fa-caret-up'); - var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible'); - if (isCollapsed) { - if (!initializing) { Cryptpad.feedback('HIDETOOLBAR_PAD'); } - $collapse.addClass('fa-caret-down'); - } - else { - if (!initializing) { Cryptpad.feedback('SHOWTOOLBAR_PAD'); } - $collapse.addClass('fa-caret-up'); - } - }; - updateIcon(); - $collapse.click(function () { - $(window).trigger('resize'); - $iframe.find('.cke_toolbox_main').toggle(); - $(window).trigger('cryptpad-ck-toolbar'); - updateIcon(); - }); - $rightside.append($collapse); - } + Title.setToolbar(toolbar); - /* add a history button */ - var histConfig = { - onLocal: realtimeOptions.onLocal, - onRemote: realtimeOptions.onRemote, - setHistory: setHistory, - applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); }, - $toolbar: $bar - }; - var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig}); - $drawer.append($hist); - - /* save as template */ - if (!Cryptpad.isTemplate(window.location.href)) { - var templateObj = { - rt: info.realtime, - Crypt: Cryptget, - getTitle: function () { return document.title; } - }; - var $templateButton = Cryptpad.createButton('template', true, templateObj); - $rightside.append($templateButton); - } + var $rightside = toolbar.$rightside; + var $drawer = toolbar.$drawer; - /* add an export button */ - var $export = Cryptpad.createButton('export', true, {}, exportFile); - $drawer.append($export); + var editHash; - if (!readOnly) { - /* add an import button */ - var $import = Cryptpad.createButton('import', true, { - accept: 'text/html' - }, importFile); - $drawer.append($import); - } + if (!readOnly) { + editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); + } - /* add a forget button */ - var forgetCb = function (err) { - if (err) { return; } - setEditable(false); + $bar.find('#cke_1_toolbar_collapser').hide(); + if (!readOnly) { + // Expand / collapse the toolbar + var $collapse = Cryptpad.createButton(null, true); + $collapse.removeClass('fa-question'); + var updateIcon = function () { + $collapse.removeClass('fa-caret-down').removeClass('fa-caret-up'); + var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible'); + if (isCollapsed) { + if (!initializing) { Cryptpad.feedback('HIDETOOLBAR_PAD'); } + $collapse.addClass('fa-caret-down'); + } + else { + if (!initializing) { Cryptpad.feedback('SHOWTOOLBAR_PAD'); } + $collapse.addClass('fa-caret-up'); + } }; - var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb); - $rightside.append($forgetPad); + updateIcon(); + $collapse.click(function () { + $(window).trigger('resize'); + $('.cke_toolbox_main').toggle(); + $(window).trigger('cryptpad-ck-toolbar'); + updateIcon(); + }); + $rightside.append($collapse); + } - // set the hash - if (!readOnly) { Cryptpad.replaceHash(editHash); } + /* add a history button */ + var histConfig = { + onLocal: realtimeOptions.onLocal, + onRemote: realtimeOptions.onRemote, + setHistory: setHistory, + applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); }, + $toolbar: $bar }; + var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig}); + $drawer.append($hist); + + /* save as template */ + if (!Cryptpad.isTemplate(window.location.href)) { + var templateObj = { + rt: info.realtime, + Crypt: Cryptget, + getTitle: function () { return document.title; } + }; + var $templateButton = Cryptpad.createButton('template', true, templateObj); + $rightside.append($templateButton); + } - // this should only ever get called once, when the chain syncs - realtimeOptions.onReady = function (info) { - if (!module.isMaximized) { - module.isMaximized = true; - $iframe.find('iframe.cke_wysiwyg_frame').css('width', ''); - $iframe.find('iframe.cke_wysiwyg_frame').css('height', ''); - } - $iframe.find('body').addClass('app-pad'); - - if (module.realtime !== info.realtime) { - module.patchText = TextPatcher.create({ - realtime: info.realtime, - //logging: true, - }); - } + /* add an export button */ + var $export = Cryptpad.createButton('export', true, {}, exportFile); + $drawer.append($export); - module.realtime = info.realtime; + if (!readOnly) { + /* add an import button */ + var $import = Cryptpad.createButton('import', true, { + accept: 'text/html' + }, importFile); + $drawer.append($import); + } - var shjson = module.realtime.getUserDoc(); + /* add a forget button */ + var forgetCb = function (err) { + if (err) { return; } + setEditable(false); + }; + var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb); + $rightside.append($forgetPad); + + // set the hash + if (!readOnly) { Cryptpad.replaceHash(editHash); } + }; + + // this should only ever get called once, when the chain syncs + realtimeOptions.onReady = function (info) { + if (!module.isMaximized) { + module.isMaximized = true; + $('iframe.cke_wysiwyg_frame').css('width', ''); + $('iframe.cke_wysiwyg_frame').css('height', ''); + } + $('body').addClass('app-pad'); - var newPad = false; - if (shjson === '') { newPad = true; } + if (module.realtime !== info.realtime) { + module.patchText = TextPatcher.create({ + realtime: info.realtime, + //logging: true, + }); + } - if (!newPad) { - applyHjson(shjson); + module.realtime = info.realtime; - // Update the user list (metadata) from the hyperjson - Metadata.update(shjson); + var shjson = module.realtime.getUserDoc(); - if (!readOnly) { - var shjson2 = stringifyDOM(inner); - var hjson2 = JSON.parse(shjson2).slice(0,-1); - var hjson = JSON.parse(shjson).slice(0,-1); - if (stringify(hjson2) !== stringify(hjson)) { - console.log('err'); - console.error("shjson2 !== shjson"); - Cryptpad.errorLoadingScreen(Messages.wrongApp); - throw new Error(); - } - } - } else { - Title.updateTitle(Cryptpad.initialName || Title.defaultTitle); - documentBody.innerHTML = Messages.initialState; - } + var newPad = false; + if (shjson === '') { newPad = true; } - Cryptpad.removeLoadingScreen(emitResize); - setEditable(!readOnly); - initializing = false; - - if (readOnly) { return; } - UserList.getLastName(toolbar.$userNameButton, newPad); - editor.focus(); - if (newPad) { - cursor.setToEnd(); - } else { - cursor.setToStart(); - } - }; + if (!newPad) { + applyHjson(shjson); - realtimeOptions.onAbort = function () { - console.log("Aborting the session!"); - // stop the user from continuing to edit - setEditable(false); - toolbar.failed(); - Cryptpad.alert(Messages.common_connectionLost, undefined, true); - }; + // Update the user list (metadata) from the hyperjson + // XXX Metadata.update(shjson); - realtimeOptions.onConnectionChange = function (info) { - setEditable(info.state); - toolbar.failed(); - if (info.state) { - initializing = true; - toolbar.reconnecting(info.myId); - Cryptpad.findOKButton().click(); - } else { - Cryptpad.alert(Messages.common_connectionLost, undefined, true); + if (!readOnly) { + var shjson2 = stringifyDOM(inner); + var hjson2 = JSON.parse(shjson2).slice(0,3); + var hjson = JSON.parse(shjson).slice(0,3); + if (stringify(hjson2) !== stringify(hjson)) { + console.log('err'); + console.error("shjson2 !== shjson"); + console.log(stringify(hjson2)); + console.log(stringify(hjson)); + Cryptpad.errorLoadingScreen(Messages.wrongApp); + throw new Error(); + } } - }; - - realtimeOptions.onError = onConnectError; + } else { + Title.updateTitle(Cryptpad.initialName || Title.defaultTitle); + documentBody.innerHTML = Messages.initialState; + } - var onLocal = realtimeOptions.onLocal = function () { - if (initializing) { return; } - if (isHistoryMode) { return; } - if (readOnly) { return; } + Cryptpad.removeLoadingScreen(emitResize); + setEditable(!readOnly); + initializing = false; - // stringify the json and send it into chainpad - var shjson = stringifyDOM(inner); + if (readOnly) { return; } + //TODO UserList.getLastName(toolbar.$userNameButton, newPad); + editor.focus(); + if (newPad) { + cursor.setToEnd(); + } else { + cursor.setToStart(); + } + }; +/* unreachable + realtimeOptions.onAbort = function () { + console.log("Aborting the session!"); + // stop the user from continuing to edit + setEditable(false); + toolbar.failed(); + Cryptpad.alert(Messages.common_connectionLost, undefined, true); + }; */ + + realtimeOptions.onConnectionChange = function (info) { + setEditable(info.state); + toolbar.failed(); + if (info.state) { + initializing = true; + toolbar.reconnecting(info.myId); + Cryptpad.findOKButton().click(); + } else { + Cryptpad.alert(Messages.common_connectionLost, undefined, true); + } + }; - module.patchText(shjson); - if (module.realtime.getUserDoc() !== shjson) { - console.error("realtime.getUserDoc() !== shjson"); - } - }; + realtimeOptions.onError = onConnectError; - module.realtimeInput = realtimeInput.start(realtimeOptions); - - Cryptpad.onLogout(function () { setEditable(false); }); - - /* hitting enter makes a new line, but places the cursor inside - of the
instead of the

. This makes it such that you - cannot type until you click, which is rather unnacceptable. - If the cursor is ever inside such a
, you probably want - to push it out to the parent element, which ought to be a - paragraph tag. This needs to be done on keydown, otherwise - the first such keypress will not be inserted into the P. */ - inner.addEventListener('keydown', cursor.brFix); - - editor.on('change', onLocal); - - // export the typing tests to the window. - // call like `test = easyTest()` - // terminate the test like `test.cancel()` - window.easyTest = function () { - cursor.update(); - var start = cursor.Range.start; - var test = TypingTest.testInput(inner, start.el, start.offset, onLocal); - onLocal(); - return test; - }; + var onLocal = realtimeOptions.onLocal = function () { + if (initializing) { return; } + if (isHistoryMode) { return; } + if (readOnly) { return; } - $bar.find('.cke_button').click(function () { - var e = this; - var classString = e.getAttribute('class'); - var classes = classString.split(' ').filter(function (c) { - return /cke_button__/.test(c); - }); + // stringify the json and send it into chainpad + var shjson = stringifyDOM(inner); - var id = classes[0]; - if (typeof(id) === 'string') { - Cryptpad.feedback(id.toUpperCase()); - } + module.patchText(shjson); + if (module.realtime.getUserDoc() !== shjson) { + console.error("realtime.getUserDoc() !== shjson"); + } + }; + + module.realtimeInput = realtimeInput.start(realtimeOptions); + + Cryptpad.onLogout(function () { setEditable(false); }); + + /* hitting enter makes a new line, but places the cursor inside + of the
instead of the

. This makes it such that you + cannot type until you click, which is rather unnacceptable. + If the cursor is ever inside such a
, you probably want + to push it out to the parent element, which ought to be a + paragraph tag. This needs to be done on keydown, otherwise + the first such keypress will not be inserted into the P. */ + inner.addEventListener('keydown', cursor.brFix); + + editor.on('change', onLocal); + + // export the typing tests to the window. + // call like `test = easyTest()` + // terminate the test like `test.cancel()` + window.easyTest = function () { + cursor.update(); + var start = cursor.Range.start; + var test = TypingTest.testInput(inner, start.el, start.offset, onLocal); + onLocal(); + return test; + }; + + $bar.find('.cke_button').click(function () { + var e = this; + var classString = e.getAttribute('class'); + var classes = classString.split(' ').filter(function (c) { + return /cke_button__/.test(c); }); + + var id = classes[0]; + if (typeof(id) === 'string') { + Cryptpad.feedback(id.toUpperCase()); + } }); }; - var interval = 100; - var second = function (Ckeditor) { - //Cryptpad.ready(function () { - andThen(Ckeditor); - //Cryptpad.reportAppUsage(); - //}); - Cryptpad.onError(function (info) { - if (info && info.type === "store") { - onConnectError(); + + var CKEDITOR_CHECK_INTERVAL = 100; + var ckEditorAvailable = function (cb) { + var intr; + var check = function () { + if (window.CKEDITOR) { + clearTimeout(intr); + cb(window.CKEDITOR); } - }); + }; + intr = setInterval(function () { + console.log("Ckeditor was not defined. Trying again in %sms", CKEDITOR_CHECK_INTERVAL); + check(); + }, CKEDITOR_CHECK_INTERVAL); + check(); }; - var first = function () { - Ckeditor = window.CKEDITOR; - if (Ckeditor) { - // mobile configuration + var main = function () { + var Ckeditor; + var editor; + + nThen(function (waitFor) { + ckEditorAvailable(waitFor(function (ck) { Ckeditor = ck; })); + $(waitFor(function () { + Cryptpad.addLoadingScreen(); + })); + }).nThen(function (waitFor) { Ckeditor.config.toolbarCanCollapse = true; if (screen.height < 800) { Ckeditor.config.toolbarStartupExpanded = false; @@ -744,15 +759,19 @@ define([ } else { $('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes'); } - second(Ckeditor); - } else { - //console.log("Ckeditor was not defined. Trying again in %sms",interval); - setTimeout(first, interval); - } + editor = Ckeditor.replace('editor1', { + customConfig: '/customize/ckeditor-config.js', + }); + editor.on('instanceReady', waitFor()); + }).nThen(function (waitFor) { + Links.addSupportForOpeningLinksInNewTab(Ckeditor); + Cryptpad.onError(function (info) { + if (info && info.type === "store") { + onConnectError(); + } + }); + andThen(editor); + }); }; - - $(function () { - Cryptpad.addLoadingScreen(); - first(); - }); + main(); }); diff --git a/www/pad2/outer.js b/www/pad2/outer.js index 5c7c11364..8e09a6ea0 100644 --- a/www/pad2/outer.js +++ b/www/pad2/outer.js @@ -1,13 +1,39 @@ define([ - '/common/sframe-ctrl.js', - 'jquery' -], function (SFrameCtrl, $) { + '/common/sframe-channel.js', + 'jquery', + '/common/sframe-chainpad-netflux-outer.js', + '/bower_components/nthen/index.js', + '/common/cryptpad-common.js', + '/bower_components/chainpad-crypto/crypto.js' +], function (SFrameChannel, $, CpNfOuter, nThen, Cryptpad, Crypto) { console.log('xxx'); - $(function () { - console.log('go'); - SFrameCtrl.init($('#sbox-iframe')[0], function () { - console.log('\n\ndone\n\n'); + nThen(function (waitFor) { + $(waitFor()); + }).nThen(function (waitFor) { + SFrameChannel.init($('#sbox-iframe')[0].contentWindow, waitFor(function () { + console.log('sframe initialized'); + })); + Cryptpad.ready(waitFor()); + }).nThen(function (waitFor) { + Cryptpad.onError(function (info) { + console.log('error'); + console.log(info); + if (info && info.type === "store") { + //onConnectError(); + } + }); + }).nThen(function (waitFor) { + var secret = Cryptpad.getSecrets(); + var readOnly = secret.keys && !secret.keys.editKeyStr; + if (!secret.keys) { secret.keys = secret.key; } + + var outer = CpNfOuter.start({ + channel: secret.channel, + network: Cryptpad.getNetwork(), + validateKey: secret.keys.validateKey || undefined, + readOnly: readOnly, + crypto: Crypto.createEncryptor(secret.keys), }); }); }); \ No newline at end of file