From 65dfd99171e0f449a4c574fa6a9f0d43aae30bd6 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Mon, 7 Aug 2017 16:27:57 +0200 Subject: [PATCH 001/121] major wip --- server.js | 4 + www/common/boot2.js | 24 +- www/common/requireconfig.js | 25 + www/common/sframe-boot.js | 10 + www/common/sframe-boot2.js | 27 + www/common/sframe-chainpad-netflux-inner.js | 414 +++++++++++ www/common/sframe-chainpad-netflux-outer.js | 413 +++++++++++ www/common/sframe-ctrl.js | 51 ++ www/common/sframe-noscriptfix.js | 3 + www/pad2/index.html | 31 + www/pad2/inner.html | 41 ++ www/pad2/links.js | 60 ++ www/pad2/main.js | 758 ++++++++++++++++++++ www/pad2/outer.js | 13 + 14 files changed, 1854 insertions(+), 20 deletions(-) create mode 100644 www/common/requireconfig.js create mode 100644 www/common/sframe-boot.js create mode 100644 www/common/sframe-boot2.js create mode 100644 www/common/sframe-chainpad-netflux-inner.js create mode 100644 www/common/sframe-chainpad-netflux-outer.js create mode 100644 www/common/sframe-ctrl.js create mode 100644 www/common/sframe-noscriptfix.js create mode 100644 www/pad2/index.html create mode 100644 www/pad2/inner.html create mode 100644 www/pad2/links.js create mode 100644 www/pad2/main.js create mode 100644 www/pad2/outer.js diff --git a/server.js b/server.js index 351f0c695..1ee8b08e5 100644 --- a/server.js +++ b/server.js @@ -32,6 +32,9 @@ var setHeaders = (function () { if (typeof(config.httpHeaders) !== 'object') { return function () {}; } const headers = clone(config.httpHeaders); + + headers['Access-Control-Allow-Origin'] = "*"; + if (config.contentSecurity && false) { headers['Content-Security-Policy'] = clone(config.contentSecurity); if (!/;$/.test(headers['Content-Security-Policy'])) { headers['Content-Security-Policy'] += ';' } @@ -149,6 +152,7 @@ httpServer.listen(config.httpPort,config.httpAddress,function(){ console.log('\n[%s] server available http://%s%s', new Date().toISOString(), hostName, ps); }); +Http.createServer(app).listen(config.httpPort+1, config.httpAddress); var wsConfig = { server: httpServer }; diff --git a/www/common/boot2.js b/www/common/boot2.js index 928b57d48..b24ce38af 100644 --- a/www/common/boot2.js +++ b/www/common/boot2.js @@ -1,24 +1,8 @@ // This is stage 1, it can be changed but you must bump the version of the project. -define([], function () { - // fix up locations so that relative urls work. - require.config({ - baseUrl: window.location.pathname, - paths: { - // jquery declares itself as literally "jquery" so it cannot be pulled by path :( - "jquery": "/bower_components/jquery/dist/jquery.min", - // json.sortify same - "json.sortify": "/bower_components/json.sortify/dist/JSON.sortify", - //"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf", - //"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker" - cm: '/bower_components/codemirror' - }, - map: { - '*': { - 'css': '/bower_components/require-css/css.js', - 'less': '/common/RequireLess.js', - } - } - }); +define([ + '/common/requireconfig.js' +], function (RequireConfig) { + require.config(RequireConfig); // most of CryptPad breaks if you don't support isArray if (!Array.isArray) { diff --git a/www/common/requireconfig.js b/www/common/requireconfig.js new file mode 100644 index 000000000..2571b24e5 --- /dev/null +++ b/www/common/requireconfig.js @@ -0,0 +1,25 @@ +define([ + '/api/config' +], function (ApiConfig) { + var out = { + // fix up locations so that relative urls work. + baseUrl: window.location.pathname, + paths: { + // jquery declares itself as literally "jquery" so it cannot be pulled by path :( + "jquery": "/bower_components/jquery/dist/jquery.min", + // json.sortify same + "json.sortify": "/bower_components/json.sortify/dist/JSON.sortify", + //"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf", + //"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker" + cm: '/bower_components/codemirror' + }, + map: { + '*': { + 'css': '/bower_components/require-css/css.js', + 'less': '/common/RequireLess.js', + } + } + }; + Object.keys(ApiConfig.requireConf).forEach(function (k) { out[k] = ApiConfig.requireConf[k]; }); + return out; +}); diff --git a/www/common/sframe-boot.js b/www/common/sframe-boot.js new file mode 100644 index 000000000..127505ef1 --- /dev/null +++ b/www/common/sframe-boot.js @@ -0,0 +1,10 @@ +// Stage 0, this gets cached which means we can't change it. boot2-sframe.js is changable. +// Note that this file is meant to be executed only inside of a sandbox iframe. +window.addEventListener('message', function (msg) { + var data = msg.data; + if (data.q !== 'INIT') { return; } + msg.source.postMessage({ txid: data.txid, res: 'OK' }, '*'); + if (data.requireConf) { require.config(data.requireConf); } + require(['/common/sframe-boot2.js'], function () { }); +}); +console.log('boot'); \ No newline at end of file diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js new file mode 100644 index 000000000..ac0c060e9 --- /dev/null +++ b/www/common/sframe-boot2.js @@ -0,0 +1,27 @@ +// 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) { + require.config(RequireConfig); +console.log('boot2'); + // most of CryptPad breaks if you don't support isArray + if (!Array.isArray) { + Array.isArray = function(arg) { // CRYPTPAD_SHIM + return Object.prototype.toString.call(arg) === '[object Array]'; + }; + } + + var mkFakeStore = function () { + var fakeStorage = { + getItem: function (k) { return fakeStorage[k]; }, + setItem: function (k, v) { fakeStorage[k] = v; return v; } + }; + return fakeStorage; + }; + window.__defineGetter__('localStorage', function () { return mkFakeStore(); }); + window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); }); + + + 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 new file mode 100644 index 000000000..746b1d733 --- /dev/null +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -0,0 +1,414 @@ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * 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; + 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 = { + change : [], + onChange : function(newData) { + userList.change.forEach(function (el) { + el(newData); + }); + }, + users: [] + }; + + var onJoining = function(peer) { + if(peer.length !== 32) { return; } + var list = userList.users; + var index = list.indexOf(peer); + if(index === -1) { + userList.users.push(peer); + } + 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 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 + }); + }; + + // 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); + }); + 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;} + }); + 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); + }); + }; + + 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); + } + }); + } + + connectTo(network); + }, onConnectError); + + return toReturn; + }; + return module.exports; +}); diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js new file mode 100644 index 000000000..0da737c0f --- /dev/null +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -0,0 +1,413 @@ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * 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; + 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) { + 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 = { + change : [], + onChange : function(newData) { + userList.change.forEach(function (el) { + el(newData); + }); + }, + users: [] + }; + + var onJoining = function(peer) { + if(peer.length !== 32) { return; } + var list = userList.users; + var index = list.indexOf(peer); + if(index === -1) { + userList.users.push(peer); + } + 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 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 + }); + }; + + // 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); + }); + 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;} + }); + 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); + }); + }; + + 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); + } + }); + } + + connectTo(network); + }, onConnectError); + + return toReturn; + }; + return module.exports; +}); diff --git a/www/common/sframe-ctrl.js b/www/common/sframe-ctrl.js new file mode 100644 index 000000000..536f2689e --- /dev/null +++ b/www/common/sframe-ctrl.js @@ -0,0 +1,51 @@ +// This file provides the external API for launching the sandboxed iframe. +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.', ''); + }; + + var init = module.exports.init = function (frame, cb) { + if (iframe) { throw new Error('already initialized'); } + var txid = mkTxid(); + var intr = setInterval(function () { + frame.contentWindow.postMessage({ + txid: txid, + requireConf: RequireConfig, + q: 'INIT' + }, '*'); + }); + window.addEventListener('message', function (msg) { + console.log('recv'); + console.log(msg.origin); + var data = msg.data; + if (data.txid !== txid) { return; } + clearInterval(intr); + iframe = frame; + cb(); + }); + }; + var query = module.exports.query = function (msg, cb) { + if (!iframe) { throw new Error('not yet initialized'); } + var txid = mkTxid(); + queries[txid] = { + txid: txid, + timeout: setTimeout(function () { + delete queries[txid]; + console.log("Error") + }) + }; + }; + var registerHandler = module.exports.registerHandler = function (queryType, handler) { + if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } + handlers[queryType] = handler; + }; + + return module.exports; +}); diff --git a/www/common/sframe-noscriptfix.js b/www/common/sframe-noscriptfix.js new file mode 100644 index 000000000..f5e6fb1c0 --- /dev/null +++ b/www/common/sframe-noscriptfix.js @@ -0,0 +1,3 @@ +// Fix for noscript bugs when caching iframe content. +// Caution, this file will get cached, you must change the name if you change it. +document.getElementById('sbox-iframe').setAttribute('src', 'http://localhost:3001/pad2/inner.html?cb=' + (+new Date())); diff --git a/www/pad2/index.html b/www/pad2/index.html new file mode 100644 index 000000000..0a2a3b922 --- /dev/null +++ b/www/pad2/index.html @@ -0,0 +1,31 @@ + + + + CryptPad + + + + + + + + + diff --git a/www/pad2/inner.html b/www/pad2/inner.html new file mode 100644 index 000000000..2e528c49b --- /dev/null +++ b/www/pad2/inner.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/www/pad2/links.js b/www/pad2/links.js new file mode 100644 index 000000000..6a53a4bc7 --- /dev/null +++ b/www/pad2/links.js @@ -0,0 +1,60 @@ +define(['/common/cryptpad-common.js'], function (Cryptpad) { + // Adds a context menu entry to open the selected link in a new tab. + // See https://github.com/xwiki-contrib/application-ckeditor/commit/755d193497bf23ed874d874b4ae92fbee887fc10 + var Messages = Cryptpad.Messages; + return { + addSupportForOpeningLinksInNewTab : function (Ckeditor) { + // Returns the DOM element of the active (currently focused) link. It has also support for linked image widgets. + // @return {CKEDITOR.dom.element} + var getActiveLink = function(editor) { + var anchor = Ckeditor.plugins.link.getSelectedLink(editor), + // We need to do some special checking against widgets availability. + activeWidget = editor.widgets && editor.widgets.focused; + // If default way of getting links didn't return anything useful.. + if (!anchor && activeWidget && activeWidget.name === 'image' && activeWidget.parts.link) { + // Since CKEditor 4.4.0 image widgets may be linked. + anchor = activeWidget.parts.link; + } + return anchor; + }; + + return function(event) { + var editor = event.editor; + if (!Ckeditor.plugins.link) { + return; + } + editor.addCommand( 'openLink', { + exec: function(editor) { + var anchor = getActiveLink(editor); + if (anchor) { + var href = anchor.getAttribute('href'); + if (href) { + window.open(href); + } + } + } + }); + if (typeof editor.addMenuItem === 'function') { + editor.addMenuItem('openLink', { + label: Messages.openLinkInNewTab, + command: 'openLink', + group: 'link', + order: -1 + }); + } + if (editor.contextMenu) { + editor.contextMenu.addListener(function(startElement) { + if (startElement) { + var anchor = getActiveLink(editor); + if (anchor && anchor.getAttribute('href')) { + return {openLink: Ckeditor.TRISTATE_OFF}; + } + } + }); + editor.contextMenu._.panelDefinition.css.push('.cke_button__openLink_icon {' + + Ckeditor.skin.getIconStyle('link') + '}'); + } + }; + } + }; +}); diff --git a/www/pad2/main.js b/www/pad2/main.js new file mode 100644 index 000000000..ce8db2010 --- /dev/null +++ b/www/pad2/main.js @@ -0,0 +1,758 @@ +console.log('one'); +define([ + 'jquery', + '/bower_components/chainpad-crypto/crypto.js', + '/common/sframe-chainpad-netflux-inner.js', + '/bower_components/hyperjson/hyperjson.js', + '/common/toolbar2.js', + '/common/cursor.js', + '/bower_components/chainpad-json-validator/json-ot.js', + '/common/TypingTests.js', + 'json.sortify', + '/bower_components/textpatcher/TextPatcher.js', + '/common/cryptpad-common.js', + '/common/cryptget.js', + '/pad/links.js', + + '/bower_components/file-saver/FileSaver.min.js', + '/bower_components/diff-dom/diffDOM.js', + + 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', + '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) { + 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); + }; + + window.Toolbar = Toolbar; + window.Hyperjson = Hyperjson; + + var slice = function (coll) { + return Array.prototype.slice.call(coll); + }; + + var removeListeners = function (root) { + slice(root.attributes).map(function (attr) { + if (/^on/.test(attr.name)) { + root.attributes.removeNamedItem(attr.name); + } + }); + slice(root.children).forEach(removeListeners); + }; + + var hjsonToDom = function (H) { + var dom = Hyperjson.toDOM(H); + removeListeners(dom); + return dom; + }; + + var module = window.REALTIME_MODULE = window.APP = { + Hyperjson: Hyperjson, + TextPatcher: TextPatcher, + logFights: true, + fights: [], + Cryptpad: Cryptpad, + Cursor: Cursor, + }; + + var emitResize = module.emitResize = function () { + var evt = window.document.createEvent('UIEvents'); + evt.initUIEvent('resize', true, false, window, 0); + window.dispatchEvent(evt); + }; + + var toolbar; + + var isNotMagicLine = function (el) { + return !(el && typeof(el.getAttribute) === 'function' && + el.getAttribute('class') && + el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1); + }; + + /* catch `type="_moz"` before it goes over the wire */ + var brFilter = function (hj) { + if (hj[1].type === '_moz') { hj[1].type = undefined; } + return hj; + }; + + var onConnectError = function () { + Cryptpad.errorLoadingScreen(Messages.websocketError); + }; + + var andThen = function (Ckeditor) { + //var $iframe = $('#pad-iframe').contents(); + //var secret = Cryptpad.getSecrets(); + //var readOnly = secret.keys && !secret.keys.editKeyStr; + //if (!secret.keys) { + // secret.keys = secret.key; + //} + 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.$; + + [ml, ml.parentElement].forEach(function (el) { + el.setAttribute('class', 'non-realtime'); + }); + + var documentBody = document.body; + + var inner = window.inner = documentBody; + + // hide all content until the realtime doc is ready + $(inner).css({ + color: '#fff', + }); + + var cursor = module.cursor = Cursor(inner); + + 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 (/^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; + } + } + + if (info.node && info.node.tagName === 'BODY') { + if (info.diff.action === 'removeAttribute' && + ['class', 'spellcheck'].indexOf(info.diff.name) !== -1) { + 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; + } + } + + // 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; + } + + // no use trying to recover the cursor if it doesn't exist + if (!cursor.exists()) { 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 (!frame) { return; } + + 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; + } + } + 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 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 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'); } + }; + + // 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); + } + }; + + 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; + } + return stringify(hjson); + }; + + var realtimeOptions = { + // the websocket URL + websocketURL: Cryptpad.getWebsocketURL(), + + // the channel we will communicate over + channel: 'x',//secret.channel, + + // the nework used for the file store if it exists + network: Cryptpad.getNetwork(), + + // our public key + validateKey: undefined,//secret.keys.validateKey || undefined, + readOnly: readOnly, + + // Pass in encrypt and decrypt methods + crypto: undefined,//Crypto.createEncryptor(secret.keys), + + // really basic operational transform + transformFunction : JsonOT.validate, + + // 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; + } + } + }; + + var setHistory = function (bool, update) { + isHistoryMode = bool; + setEditable(!bool); + if (!bool && update) { + realtimeOptions.onRemote(); + } + }; + + realtimeOptions.onRemote = function () { + if (initializing) { return; } + if (isHistoryMode) { return; } + + var oldShjson = stringifyDOM(inner); + + var shjson = module.realtime.getUserDoc(); + + // remember where the cursor is + cursor.update(); + + // Update the user list (metadata) from the hyperjson + Metadata.update(shjson); + + var newInner = JSON.parse(shjson); + var newSInner; + if (newInner.length > 2) { + newSInner = stringify(newInner[2]); + } + + // build a dom from HJSON, diff, and patch the editor + applyHjson(shjson); + + 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); + } + } + } + } + + // 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); + }; + + 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(); + }; + + 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')); + }); + + Title.setToolbar(toolbar); + + var $rightside = toolbar.$rightside; + var $drawer = toolbar.$drawer; + + var editHash; + + if (!readOnly) { + editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); + } + + $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); + } + + /* 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); + } + + /* add an export button */ + var $export = Cryptpad.createButton('export', true, {}, exportFile); + $drawer.append($export); + + if (!readOnly) { + /* add an import button */ + var $import = Cryptpad.createButton('import', true, { + accept: 'text/html' + }, importFile); + $drawer.append($import); + } + + /* 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.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, + }); + } + + module.realtime = info.realtime; + + var shjson = module.realtime.getUserDoc(); + + var newPad = false; + if (shjson === '') { newPad = true; } + + if (!newPad) { + applyHjson(shjson); + + // Update the user list (metadata) from the hyperjson + Metadata.update(shjson); + + 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; + } + + Cryptpad.removeLoadingScreen(emitResize); + setEditable(!readOnly); + initializing = false; + + if (readOnly) { return; } + UserList.getLastName(toolbar.$userNameButton, newPad); + editor.focus(); + if (newPad) { + cursor.setToEnd(); + } else { + cursor.setToStart(); + } + }; + + 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); + } + }; + + realtimeOptions.onError = onConnectError; + + var onLocal = realtimeOptions.onLocal = function () { + if (initializing) { return; } + if (isHistoryMode) { return; } + if (readOnly) { return; } + + // stringify the json and send it into chainpad + var shjson = stringifyDOM(inner); + + 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 first = function () { + Ckeditor = window.CKEDITOR; + if (Ckeditor) { + // mobile configuration + Ckeditor.config.toolbarCanCollapse = true; + if (screen.height < 800) { + Ckeditor.config.toolbarStartupExpanded = false; + $('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no'); + } 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); + } + }; + + $(function () { + Cryptpad.addLoadingScreen(); + first(); + }); +}); diff --git a/www/pad2/outer.js b/www/pad2/outer.js new file mode 100644 index 000000000..5c7c11364 --- /dev/null +++ b/www/pad2/outer.js @@ -0,0 +1,13 @@ + +define([ + '/common/sframe-ctrl.js', + 'jquery' +], function (SFrameCtrl, $) { + console.log('xxx'); + $(function () { + console.log('go'); + SFrameCtrl.init($('#sbox-iframe')[0], function () { + console.log('\n\ndone\n\n'); + }); + }); +}); \ No newline at end of file From 35a55a15edbf1336190bcfa9a7864f3624767519 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Mon, 7 Aug 2017 17:23:28 +0200 Subject: [PATCH 002/121] wip --- www/common/sframe-boot.js | 9 ++- www/common/sframe-boot2.js | 1 - www/common/sframe-chainpad-netflux-outer.js | 47 ++++----------- www/common/sframe-channel.js | 46 ++++++++++++++ www/common/sframe-ctrl.js | 67 ++++++++++++++------- www/common/sframe-protocol.js | 5 ++ 6 files changed, 113 insertions(+), 62 deletions(-) create mode 100644 www/common/sframe-channel.js create mode 100644 www/common/sframe-protocol.js diff --git a/www/common/sframe-boot.js b/www/common/sframe-boot.js index 127505ef1..34b25167e 100644 --- a/www/common/sframe-boot.js +++ b/www/common/sframe-boot.js @@ -1,10 +1,9 @@ // Stage 0, this gets cached which means we can't change it. boot2-sframe.js is changable. // Note that this file is meant to be executed only inside of a sandbox iframe. window.addEventListener('message', function (msg) { - var data = msg.data; + var data = JSON.parse(msg.data); if (data.q !== 'INIT') { return; } - msg.source.postMessage({ txid: data.txid, res: 'OK' }, '*'); - if (data.requireConf) { require.config(data.requireConf); } + msg.source.postMessage({ txid: data.txid, content: 'OK' }, '*'); + if (data.content && data.content.requireConf) { require.config(data.content.requireConf); } require(['/common/sframe-boot2.js'], function () { }); -}); -console.log('boot'); \ No newline at end of file +}); \ No newline at end of file diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js index ac0c060e9..0c98fb5d7 100644 --- a/www/common/sframe-boot2.js +++ b/www/common/sframe-boot2.js @@ -22,6 +22,5 @@ console.log('boot2'); window.__defineGetter__('localStorage', function () { return mkFakeStore(); }); window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); }); - require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]); }); diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index 0da737c0f..c27be9021 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -27,45 +27,24 @@ define([ var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; - module.exports.start = function (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 || {}; + module.exports.start = function (conf) { + var websocketUrl = conf.websocketURL; + var userName = conf.userName; + 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 network = config.network; var lastKnownHash; - var userList = { - change : [], - onChange : function(newData) { - userList.change.forEach(function (el) { - el(newData); - }); - }, - users: [] - }; - - var onJoining = function(peer) { - if(peer.length !== 32) { return; } - var list = userList.users; - var index = list.indexOf(peer); - if(index === -1) { - userList.users.push(peer); - } - 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. @@ -73,10 +52,8 @@ define([ realtime.start(); - if(config.setMyID) { - config.setMyID({ - myID: wc.myID - }); + if(setMyID) { + setMyID({ myID: wc.myID }); } // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced if (!readOnly) { diff --git a/www/common/sframe-channel.js b/www/common/sframe-channel.js new file mode 100644 index 000000000..4291f5108 --- /dev/null +++ b/www/common/sframe-channel.js @@ -0,0 +1,46 @@ +// 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; + 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.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; +}); \ No newline at end of file diff --git a/www/common/sframe-ctrl.js b/www/common/sframe-ctrl.js index 536f2689e..ebae43985 100644 --- a/www/common/sframe-ctrl.js +++ b/www/common/sframe-ctrl.js @@ -1,4 +1,5 @@ -// This file provides the external API for launching the sandboxed iframe. +// 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) { @@ -11,40 +12,64 @@ define([ return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', ''); }; - var init = module.exports.init = function (frame, cb) { + module.exports.init = function (frame, cb) { if (iframe) { throw new Error('already initialized'); } var txid = mkTxid(); var intr = setInterval(function () { - frame.contentWindow.postMessage({ + frame.contentWindow.postMessage(JSON.stringify({ txid: txid, - requireConf: RequireConfig, + content: { requireConf: RequireConfig }, q: 'INIT' - }, '*'); + }), '*'); }); window.addEventListener('message', function (msg) { - console.log('recv'); - console.log(msg.origin); - var data = msg.data; - if (data.txid !== txid) { return; } - clearInterval(intr); - iframe = frame; - cb(); + 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); + } }); }; - var query = module.exports.query = function (msg, cb) { + + module.exports.query = function (q, content, cb) { if (!iframe) { throw new Error('not yet initialized'); } var txid = mkTxid(); - queries[txid] = { - txid: txid, - timeout: setTimeout(function () { - delete queries[txid]; - console.log("Error") - }) + 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 + }), '*'); }; - var registerHandler = module.exports.registerHandler = function (queryType, handler) { + + module.exports.registerHandler = function (queryType, handler) { if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } - handlers[queryType] = handler; + 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 new file mode 100644 index 000000000..56d32601a --- /dev/null +++ b/www/common/sframe-protocol.js @@ -0,0 +1,5 @@ +// This file defines all of the RPC calls +// The internal API is in sframe-channel.js +define({ + +}); \ No newline at end of file From 4b25ab80d6fce33fb2c95a98de26a91e5d1ae9eb Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Wed, 9 Aug 2017 14:45:39 +0200 Subject: [PATCH 003/121] wip --- bower.json | 3 +- www/common/metadata-manager.js | 0 www/common/sframe-boot.js | 2 +- www/common/sframe-boot2.js | 7 +- www/common/sframe-chainpad-netflux-inner.js | 441 ++----- www/common/sframe-chainpad-netflux-outer.js | 300 ++--- www/common/sframe-channel.js | 115 +- www/common/sframe-ctrl.js | 76 -- www/common/sframe-protocol.js | 32 +- www/pad2/main.js | 1139 ++++++++++--------- www/pad2/outer.js | 40 +- 11 files changed, 913 insertions(+), 1242 deletions(-) create mode 100644 www/common/metadata-manager.js delete mode 100644 www/common/sframe-ctrl.js 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 From c304071492377a0bd87727796bd0b834cf9c7ddd Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Wed, 9 Aug 2017 17:37:55 +0200 Subject: [PATCH 004/121] wip --- www/common/common-userlist2.js | 112 ++++++++++ www/common/metadata-manager.js | 17 ++ www/common/sframe-boot2.js | 9 +- www/common/sframe-chainpad-netflux-inner.js | 24 +- www/common/sframe-chainpad-netflux-outer.js | 24 +- www/common/sframe-channel.js | 168 +++++++------- www/common/sframe-protocol.js | 5 + www/pad2/main.js | 232 ++++++++++---------- www/pad2/outer.js | 9 +- 9 files changed, 360 insertions(+), 240 deletions(-) create mode 100644 www/common/common-userlist2.js diff --git a/www/common/common-userlist2.js b/www/common/common-userlist2.js new file mode 100644 index 000000000..e7f2449fa --- /dev/null +++ b/www/common/common-userlist2.js @@ -0,0 +1,112 @@ +define(function () { + var module = {}; + + module.create = function (info, onLocal, Cryptget, Cryptpad) { + var exp = {}; + + var userData = exp.userData = {}; + var userList = exp.userList = info.userList; + var myData = exp.myData = {}; + exp.myUserName = info.myID; + exp.myNetfluxId = info.myID; + + var network = Cryptpad.getNetwork(); + + var parsed = Cryptpad.parsePadUrl(window.location.href); + var appType = parsed ? parsed.type : undefined; + + var addToUserData = exp.addToUserData = function(data) { + var users = userList.users; + for (var attrname in data) { userData[attrname] = data[attrname]; } + + if (users && users.length) { + for (var userKey in userData) { + if (users.indexOf(userKey) === -1) { + delete userData[userKey]; + } + } + } + + if(userList && typeof userList.onChange === "function") { + userList.onChange(userData); + } + }; + + exp.getToolbarConfig = function () { + return { + data: userData, + list: userList, + userNetfluxId: exp.myNetfluxId + }; + }; + + var setName = exp.setName = function (newName, cb) { + if (typeof(newName) !== 'string') { return; } + var myUserNameTemp = newName.trim(); + if(myUserNameTemp.length > 32) { + myUserNameTemp = myUserNameTemp.substr(0, 32); + } + exp.myUserName = myUserNameTemp; + myData = {}; + myData[exp.myNetfluxId] = { + name: exp.myUserName, + uid: Cryptpad.getUid(), + avatar: Cryptpad.getAvatarUrl(), + profile: Cryptpad.getProfileUrl(), + curvePublic: Cryptpad.getProxy().curvePublic + }; + addToUserData(myData); + /*Cryptpad.setAttribute('username', exp.myUserName, function (err) { + if (err) { + console.log("Couldn't set username"); + console.error(err); + return; + } + if (typeof cb === "function") { cb(); } + });*/ + if (typeof cb === "function") { cb(); } + }; + + exp.getLastName = function ($changeNameButton, isNew) { + Cryptpad.getLastName(function (err, lastName) { + if (err) { + console.log("Could not get previous name"); + console.error(err); + return; + } + // Update the toolbar list: + // Add the current user in the metadata + if (typeof(lastName) === 'string') { + setName(lastName, onLocal); + } else { + myData[exp.myNetfluxId] = { + name: "", + uid: Cryptpad.getUid(), + avatar: Cryptpad.getAvatarUrl(), + profile: Cryptpad.getProfileUrl(), + curvePublic: Cryptpad.getProxy().curvePublic + }; + addToUserData(myData); + onLocal(); + $changeNameButton.click(); + } + if (isNew && appType) { + Cryptpad.selectTemplate(appType, info.realtime, Cryptget); + } + }); + }; + + Cryptpad.onDisplayNameChanged(function (newName) { + setName(newName, onLocal); + }); + + network.on('reconnect', function (uid) { + exp.myNetfluxId = uid; + exp.setName(exp.myUserName); + }); + + return exp; + }; + + return module; +}); diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index e69de29bb..9d26d5588 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -0,0 +1,17 @@ +define([], function () { + var metadataChange = function (ctx, newMeta) { + + }; + var getMetadata = function (ctx) { + + }; + var create = function (sframeChan, cpNfInner) { + var ctx = { + sframeChan: sframeChan, + personalMetadata: {} + }; + + }; + return { create: create }; + +}); \ No newline at end of file diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js index 94ec9d511..8285032d2 100644 --- a/www/common/sframe-boot2.js +++ b/www/common/sframe-boot2.js @@ -1,11 +1,8 @@ // 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', - '/common/sframe-channel.js' -], function (RequireConfig, SFrameChannel) { +define(['/common/requireconfig.js'], function (RequireConfig) { require.config(RequireConfig); -console.log('boot2'); + // most of CryptPad breaks if you don't support isArray if (!Array.isArray) { Array.isArray = function(arg) { // CRYPTPAD_SHIM @@ -23,7 +20,5 @@ 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 32e1beb0f..efce47a91 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -15,9 +15,8 @@ * along with this program. If not, see . */ define([ - '/common/sframe-channel.js', '/bower_components/chainpad/chainpad.dist.js', -], function (SFrameChannel) { +], function () { var ChainPad = window.ChainPad; var module = { exports: {} }; @@ -81,6 +80,7 @@ define([ var avgSyncMilliseconds = config.avgSyncMilliseconds; var logLevel = typeof(config.logLevel) !== 'undefined'? config.logLevel : 1; var readOnly = config.readOnly || false; + var sframeChan = config.sframeChan; config = undefined; var chainpad; @@ -88,14 +88,14 @@ define([ 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 () { + sframeChan.on('EV_RT_JOIN', userList.onJoin); + sframeChan.on('EV_RT_LEAVE', userList.onLeave); + sframeChan.on('EV_RT_DISCONNECT', function () { isReady = false; userList.onReset(); onConnectionChange({ state: false }); }); - SFrameChannel.on('EV_RT_CONNECT', function (content) { + sframeChan.on('EV_RT_CONNECT', function (content) { content.members.forEach(userList.onJoin); myID = content.myID; isReady = false; @@ -113,26 +113,26 @@ define([ logLevel: logLevel }); chainpad.onMessage(function(message, cb) { - SFrameChannel.query('Q_RT_MESSAGE', message, cb); + sframeChan.query('Q_RT_MESSAGE', message, cb); }); chainpad.onPatch(function () { onRemote({ realtime: chainpad }); }); onInit({ - myID: content.myID, + myID: myID, realtime: chainpad, userList: userList, readOnly: readOnly }); }); - SFrameChannel.on('Q_RT_MESSAGE', function (content, cb) { + sframeChan.on('Q_RT_MESSAGE', function (content, cb) { if (isReady) { onLocal(); // should be onBeforeMessage } chainpad.message(content); cb('OK'); }); - SFrameChannel.on('EV_RT_READY', function () { + sframeChan.on('EV_RT_READY', function () { if (isReady) { return; } isReady = true; chainpad.start(); @@ -141,7 +141,9 @@ define([ if (!readOnly) { userList.onJoin(myID); } onReady({ realtime: chainpad }); }); - return; + return { + getMyID: function () { return myID; } + }; }; 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 d28fbe631..078a920e7 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -14,11 +14,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -define([ - '/common/sframe-channel.js', -], function (SFrameChannel) { +define([], function () { var USE_HISTORY = true; - var module = { exports: {} }; var verbose = function (x) { console.log(x); }; verbose = function () {}; // comment out to enable verbose logging @@ -31,6 +28,7 @@ define([ var validateKey = conf.validateKey; var readOnly = conf.readOnly || false; var network = conf.network; + var sframeChan = conf.sframeChan; conf = undefined; var initializing = true; @@ -38,15 +36,15 @@ define([ var queue = []; var messageFromInner = function (m, cb) { queue.push([ m, cb ]); }; - SFrameChannel.on('Q_RT_MESSAGE', function (message, cb) { + sframeChan.on('Q_RT_MESSAGE', function (message, cb) { messageFromInner(message, cb); }); - var onReady = function(wc) { + var onReady = function () { // 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; } - SFrameChannel.event('EV_RT_READY', null); + sframeChan.event('EV_RT_READY', null); // we're fully synced initializing = false; }; @@ -122,7 +120,7 @@ define([ message = unBencode(message);//.slice(message.indexOf(':[') + 1); // pass the message into Chainpad - SFrameChannel.query('Q_RT_MESSAGE', message, function () { }); + sframeChan.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 @@ -134,14 +132,14 @@ define([ channel = wc.id; // Add the existing peers in the userList - SFrameChannel.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly }); + sframeChan.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', function (m) { SFrameChannel.event('EV_RT_JOIN', m); }); - wc.on('leave', function (m) { SFrameChannel.event('EV_RT_LEAVE', m); }); + wc.on('join', function (m) { sframeChan.event('EV_RT_JOIN', m); }); + wc.on('leave', function (m) { sframeChan.event('EV_RT_LEAVE', m); }); if (firstConnection) { // Sending a message... @@ -210,7 +208,7 @@ define([ network.on('disconnect', function (reason) { if (isIntentionallyLeaving) { return; } if (reason === "network.disconnect() called") { return; } - SFrameChannel.event('EV_RT_DISCONNECT'); + sframeChan.event('EV_RT_DISCONNECT'); }); network.on('reconnect', function (uid) { @@ -230,7 +228,7 @@ define([ return { start: function (config) { - SFrameChannel.whenReg('EV_RT_READY', function () { start(config); }); + config.sframeChan.whenReg('EV_RT_READY', function () { start(config); }); } }; }); diff --git a/www/common/sframe-channel.js b/www/common/sframe-channel.js index 793906c5a..12b390fa7 100644 --- a/www/common/sframe-channel.js +++ b/www/common/sframe-channel.js @@ -2,22 +2,92 @@ 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 create = function (ow, cb) { + var otherWindow; + var handlers = {}; + var queries = {}; + + // list of handlers which are registered from the other side... + var insideHandlers = []; + var callWhenRegistered = {}; + + var chan = {}; + + chan.query = function (q, content, cb) { + 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]; + console.log("Timeout making query " + q); + }, 30000); + queries[txid] = function (data, msg) { + clearTimeout(timeout); + delete queries[txid]; + cb(undefined, data.content, msg); + }; + otherWindow.postMessage(JSON.stringify({ + txid: txid, + content: content, + q: q + }), '*'); + }; + + var event = chan.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 }), '*'); + }; + + chan.on = function (queryType, handler) { + if (!otherWindow) { throw new Error('not yet initialized'); } + if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } + 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, + content: replyContent + }), '*'); + }, msg); + }; + event('EV_REGISTER_HANDLER', queryType); + }; + + chan.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); + }; + var intr; var txid; window.addEventListener('message', function (msg) { @@ -32,7 +102,7 @@ define([ } clearInterval(intr); otherWindow = ow; - cb(); + cb(chan); } else if (typeof(data.q) === 'string' && handlers[data.q]) { handlers[data.q](data, msg); } else if (typeof(data.q) === 'undefined' && queries[data.txid]) { @@ -48,7 +118,7 @@ define([ if (window !== window.top) { // we're in the sandbox otherWindow = ow; - cb(); + cb(chan); } else { require(['/common/requireconfig.js'], function (RequireConfig) { txid = mkTxid(); @@ -63,75 +133,5 @@ define([ } }; - module.exports.query = function (q, content, cb) { - 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]; - console.log("Timeout making query " + q); - }, 30000); - queries[txid] = function (data, msg) { - clearTimeout(timeout); - delete queries[txid]; - cb(undefined, data.content, msg); - }; - otherWindow.postMessage(JSON.stringify({ - txid: txid, - content: content, - q: q - }), '*'); - }; - - 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'); } - 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, - content: replyContent - }), '*'); - }, 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; + return { create: create }; }); diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index 503dc2983..b6ab3f7b8 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -4,6 +4,11 @@ // 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. +// +// WARNING: At this point, this protocol is still EXPERIMENTAL. This is not it's final form. +// We need to define protocol one piece at a time and then when we are satisfied that we +// fully understand the problem, we will define the *right* protocol and this file will be dynomited. +// 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. diff --git a/www/pad2/main.js b/www/pad2/main.js index bf92010e8..064153914 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -14,6 +14,7 @@ define([ '/common/cryptget.js', '/pad/links.js', '/bower_components/nthen/index.js', + '/common/sframe-channel.js', '/bower_components/file-saver/FileSaver.min.js', '/bower_components/diff-dom/diffDOM.js', @@ -21,8 +22,8 @@ define([ 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', '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, nThen) { +], function ($, Crypto, CpNfInner, Hyperjson, + Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links, nThen, SFrameChannel) { var saveAs = window.saveAs; var Messages = Cryptpad.Messages; var DiffDom = window.diffDOM; @@ -84,67 +85,48 @@ define([ Cryptpad.errorLoadingScreen(Messages.websocketError); }; - var andThen = function (editor) { - //var $iframe = $('#pad-iframe').contents(); - //var secret = Cryptpad.getSecrets(); - //var readOnly = secret.keys && !secret.keys.editKeyStr; - //if (!secret.keys) { - // secret.keys = secret.key; - //} - var readOnly = false; // TODO - - - 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.$; - - [ml, ml.parentElement].forEach(function (el) { - el.setAttribute('class', 'non-realtime'); - }); - - var documentBody = $html.find('iframe')[0].contentWindow.document.body; + var domFromHTML = function (html) { + return new DOMParser().parseFromString(html, 'text/html'); + }; - var inner = window.inner = documentBody; + var forbiddenTags = [ + 'SCRIPT', + 'IFRAME', + 'OBJECT', + 'APPLET', + 'VIDEO', + 'AUDIO' + ]; + + 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 cursor = module.cursor = Cursor(inner); + var getHTML = function (inner) { + return ('\n' + '\n' + inner.innerHTML); + }; - var setEditable = module.setEditable = function (bool) { - if (bool) { - $(inner).css({ - color: '#333', - }); - } - if (!readOnly || !bool) { - inner.setAttribute('contenteditable', bool); + 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(); + }; - // don't let the user edit until the pad is ready - setEditable(false); - - var forbiddenTags = [ - 'SCRIPT', - 'IFRAME', - 'OBJECT', - 'APPLET', - 'VIDEO', - 'AUDIO' - ]; - - var diffOptions = { + var mkDiffOptions = function (cursor, readOnly) { + return { preDiffApply: function (info) { /* Don't accept attributes that begin with 'on' @@ -262,12 +244,69 @@ define([ } } }; + }; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + + var andThen = function (editor, Ckeditor, sframeChan) { + //var $iframe = $('#pad-iframe').contents(); + //var secret = Cryptpad.getSecrets(); + //var readOnly = secret.keys && !secret.keys.editKeyStr; + //if (!secret.keys) { + // secret.keys = secret.key; + //} + var readOnly = false; // TODO + + + 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 = Ckeditor.instances.editor1.plugins.magicline.backdoor.that.line.$; + [ml, ml.parentElement].forEach(function (el) { + el.setAttribute('class', 'non-realtime'); + }); + + var documentBody = $html.find('iframe')[0].contentWindow.document.body; + + var inner = window.inner = documentBody; + + var cursor = module.cursor = Cursor(inner); + + 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 initializing = true; - var Title; - var UserList; - var Metadata; + //var Title; + //var UserList; + //var Metadata; var getHeadingText = function () { var text; @@ -280,14 +319,7 @@ define([ })) { return text; } }; - 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 DD = new DiffDom(mkDiffOptions(cursor, readOnly)); // apply patches, and try not to lose the cursor in the process! var applyHjson = function (shjson) { @@ -323,28 +355,12 @@ define([ }; var realtimeOptions = { - // the websocket URL - websocketURL: Cryptpad.getWebsocketURL(), - - // the channel we will communicate over - channel: 'x',//secret.channel, - - // the nework used for the file store if it exists - network: Cryptpad.getNetwork(), - - // our public key - validateKey: undefined,//secret.keys.validateKey || undefined, + sframeChan: sframeChan, readOnly: readOnly, - - // Pass in encrypt and decrypt methods - crypto: undefined,//Crypto.createEncryptor(secret.keys), - // really basic operational transform transformFunction : JsonOT.validate, - // cryptpad debug logging (default is 1) // logLevel: 0, - validateContent: function (content) { try { JSON.parse(content); @@ -440,16 +456,8 @@ define([ } }; - 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 html = getHTML(inner); var suggestion = Title.suggestTitle('cryptpad-document'); Cryptpad.prompt(Messages.exportPrompt, Cryptpad.fixFileName(suggestion) + '.html', function (filename) { @@ -477,7 +485,8 @@ define([ Metadata = Cryptpad.createMetadata(UserList, Title, null, Cryptpad); var configTb = { - displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'], + displayed: [ + 'title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'], userList: UserList.getToolbarConfig(), share: { secret: secret, @@ -648,14 +657,6 @@ define([ 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); @@ -685,7 +686,9 @@ define([ } }; - module.realtimeInput = realtimeInput.start(realtimeOptions); + var cpNfInner = CpNfInner.start(realtimeOptions); + + Cryptpad.onLogout(function () { setEditable(false); }); @@ -725,32 +728,17 @@ define([ }); }; - - 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 main = function () { var Ckeditor; var editor; + var sframeChan; nThen(function (waitFor) { ckEditorAvailable(waitFor(function (ck) { Ckeditor = ck; })); $(waitFor(function () { Cryptpad.addLoadingScreen(); })); + SFrameChannel.create(window.top, waitFor(function (sfc) { sframeChan = sfc; })); }).nThen(function (waitFor) { Ckeditor.config.toolbarCanCollapse = true; if (screen.height < 800) { @@ -770,7 +758,7 @@ define([ onConnectError(); } }); - andThen(editor); + andThen(editor, Ckeditor, sframeChan); }); }; main(); diff --git a/www/pad2/outer.js b/www/pad2/outer.js index 8e09a6ea0..e24a9a4b8 100644 --- a/www/pad2/outer.js +++ b/www/pad2/outer.js @@ -8,10 +8,12 @@ define([ '/bower_components/chainpad-crypto/crypto.js' ], function (SFrameChannel, $, CpNfOuter, nThen, Cryptpad, Crypto) { console.log('xxx'); + var sframeChan; nThen(function (waitFor) { $(waitFor()); }).nThen(function (waitFor) { - SFrameChannel.init($('#sbox-iframe')[0].contentWindow, waitFor(function () { + SFrameChannel.create($('#sbox-iframe')[0].contentWindow, waitFor(function (sfc) { + sframeChan = sfc; console.log('sframe initialized'); })); Cryptpad.ready(waitFor()); @@ -23,12 +25,13 @@ define([ //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({ + CpNfOuter.start({ + sframeChan: sframeChan, channel: secret.channel, network: Cryptpad.getNetwork(), validateKey: secret.keys.validateKey || undefined, From 0dde1d750740ddbf62d40a0922de3210e5d9bd72 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Thu, 10 Aug 2017 14:49:21 +0200 Subject: [PATCH 005/121] wip --- www/common/metadata-manager.js | 72 +++++++++++++++++---- www/common/sframe-chainpad-netflux-inner.js | 66 +++---------------- www/common/sframe-channel.js | 22 ++++--- www/pad2/main.js | 4 +- 4 files changed, 86 insertions(+), 78 deletions(-) diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 9d26d5588..822880a02 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -1,17 +1,67 @@ define([], function () { - var metadataChange = function (ctx, newMeta) { + var create = function (sframeChan) { + var personalMetadata = 'uninitialized'; + var myID = 'uninitialized'; + var members = []; + var metadataObj = 'unintialized'; + var dirty = true; + var changeHandlers = []; - }; - var getMetadata = function (ctx) { - - }; - var create = function (sframeChan, cpNfInner) { - var ctx = { - sframeChan: sframeChan, - personalMetadata: {} + var checkUpdate = function () { + if (!dirty) { return; } + if (metadataObj === 'uninitialized') { throw new Error(); } + if (myID === 'uninitialized') { throw new Error(); } + if (personalMetadata === 'uninitialized') { throw new Error(); } + var mdo = {}; + Object.keys(metadataObj).forEach(function (x) { + if (members.indexOf(x) === -1) { return; } + mdo[x] = metadataObj[x]; + }); + mdo[myID] = personalMetadata; + metadataObj = mdo; + dirty = false; + changeHandlers.forEach(function (f) { f(); }); + }; + var change = function () { + dirty = true; + setTimeout(checkUpdate); }; - }; - return { create: create }; + sframeChan.on('EV_USERDATA_UPDATE', function (ev) { + personalMetadata = ev; + change(); + }); + sframeChan.on('EV_RT_CONNECT', function (ev) { + myID = ev.myID; + members = ev.members; + change(); + }); + sframeChan.on('EV_RT_JOIN', function (ev) { + members.push(ev); + change(); + }); + sframeChan.on('EV_RT_LEAVE', function (ev) { + var idx = members.indexOf(ev); + if (idx === -1) { console.log('Error: ' + ev + ' not in members'); return; } + members.splice(idx, 1); + change(); + }); + sframeChan.on('EV_RT_DISCONNECT', function () { + members = []; + change(); + }); + return Object.freeze({ + metadataChange: function (meta) { + metadataObj = meta; + change(); + }, + getMetadata: function () { + checkUpdate(); + return metadataObj; + }, + onChange: function (f) { changeHandlers.push(f); } + }); + }; + return Object.freeze({ create: create }); }); \ No newline at end of file diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index efce47a91..d4bd31f0e 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -15,57 +15,15 @@ * along with this program. If not, see . */ define([ - '/bower_components/chainpad/chainpad.dist.js', -], function () { + '/common/metadata-manager.js', + '/bower_components/chainpad/chainpad.dist.js' +], function (MetadataMgr) { var ChainPad = window.ChainPad; var module = { exports: {} }; var verbose = function (x) { console.log(x); }; verbose = function () {}; // comment out to enable verbose logging - var mkUserList = function () { - var userList = Object.freeze({ - change : [], - onChange : function(newData) { - userList.change.forEach(function (el) { - el(newData); - }); - }, - users: [] - }); - - var onJoining = function (peer) { - if(peer.length !== 32) { return; } - var list = userList.users; - var index = list.indexOf(peer); - if(index === -1) { - userList.users.push(peer); - } - userList.onChange(); - }; - - // 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(); - }; - - var onReset = function () { - userList.users.forEach(onLeaving); - }; - - 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 () { }; @@ -84,15 +42,13 @@ define([ config = undefined; var chainpad; - var userList = mkUserList(); var myID; var isReady = false; - sframeChan.on('EV_RT_JOIN', userList.onJoin); - sframeChan.on('EV_RT_LEAVE', userList.onLeave); + var metadataMgr = MetadataMgr.create(sframeChan); + sframeChan.on('EV_RT_DISCONNECT', function () { isReady = false; - userList.onReset(); onConnectionChange({ state: false }); }); sframeChan.on('EV_RT_CONNECT', function (content) { @@ -121,7 +77,6 @@ define([ onInit({ myID: myID, realtime: chainpad, - userList: userList, readOnly: readOnly }); }); @@ -137,13 +92,12 @@ define([ 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 { - getMyID: function () { return myID; } - }; + return Object.freeze({ + getMyID: function () { return myID; }, + metadataMgr: metadataMgr + }); }; - return module.exports; + return Object.freeze(module.exports); }); \ No newline at end of file diff --git a/www/common/sframe-channel.js b/www/common/sframe-channel.js index 12b390fa7..3dd0a3d8d 100644 --- a/www/common/sframe-channel.js +++ b/www/common/sframe-channel.js @@ -51,21 +51,22 @@ define([ otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*'); }; - chan.on = function (queryType, handler) { + chan.on = function (queryType, handler, quiet) { if (!otherWindow) { throw new Error('not yet initialized'); } - if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } if (!SFrameProtocol[queryType]) { throw new Error('please only register handlers which are defined in sframe-protocol.js'); } - handlers[queryType] = function (data, msg) { + (handlers[queryType] = handlers[queryType] || []).push(function (data, msg) { handler(data.content, function (replyContent) { msg.source.postMessage(JSON.stringify({ txid: data.txid, content: replyContent }), '*'); }, msg); - }; - event('EV_REGISTER_HANDLER', queryType); + }); + if (!quiet) { + event('EV_REGISTER_HANDLER', queryType); + } }; chan.whenReg = function (queryType, handler) { @@ -80,13 +81,13 @@ define([ } }; - handlers['EV_REGISTER_HANDLER'] = function (data) { + (handlers['EV_REGISTER_HANDLER'] = handlers['EV_REGISTER_HANDLER'] || []).push(function (data) { if (callWhenRegistered[data.content]) { callWhenRegistered[data.content].forEach(function (f) { f(); }); delete callWhenRegistered[data.content]; } insideHandlers.push(data.content); - }; + }); var intr; var txid; @@ -104,9 +105,12 @@ define([ otherWindow = ow; cb(chan); } else if (typeof(data.q) === 'string' && handlers[data.q]) { - handlers[data.q](data, msg); + 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); + queries[data.txid](msg, msg); } else if (data.txid === txid) { // stray message from init return; diff --git a/www/pad2/main.js b/www/pad2/main.js index 064153914..675b05a25 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -660,10 +660,10 @@ define([ realtimeOptions.onConnectionChange = function (info) { setEditable(info.state); - toolbar.failed(); + //toolbar.failed(); TODO if (info.state) { initializing = true; - toolbar.reconnecting(info.myId); + //toolbar.reconnecting(info.myId); // TODO Cryptpad.findOKButton().click(); } else { Cryptpad.alert(Messages.common_connectionLost, undefined, true); From 1e56fa31c0173593f30c760bac32dd870b64620a Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Thu, 10 Aug 2017 18:31:44 +0200 Subject: [PATCH 006/121] yay, they talk and they don't fight --- www/common/metadata-manager.js | 43 ++++++++++++++------- www/common/sframe-chainpad-netflux-inner.js | 2 +- www/common/sframe-chainpad-netflux-outer.js | 4 +- www/common/sframe-channel.js | 36 ++++++++++++----- www/common/sframe-protocol.js | 2 +- www/pad2/main.js | 27 ++++++------- www/pad2/outer.js | 28 ++++++++++++++ 7 files changed, 99 insertions(+), 43 deletions(-) diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 822880a02..24d03df2e 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -1,24 +1,38 @@ define([], function () { + var UNINIT = 'uninitialized'; var create = function (sframeChan) { - var personalMetadata = 'uninitialized'; - var myID = 'uninitialized'; + var meta = UNINIT; + var myID = UNINIT; var members = []; - var metadataObj = 'unintialized'; + var metadataObj = UNINIT; var dirty = true; var changeHandlers = []; var checkUpdate = function () { if (!dirty) { return; } - if (metadataObj === 'uninitialized') { throw new Error(); } - if (myID === 'uninitialized') { throw new Error(); } - if (personalMetadata === 'uninitialized') { throw new Error(); } + if (meta === UNINIT) { throw new Error(); } + if (myID === UNINIT) { myID = meta.myID; } + if (metadataObj === UNINIT) { + metadataObj = { + defaultTitle: meta.doc.defaultTitle, + title: meta.doc.defaultTitle, + users: {} + }; + } var mdo = {}; - Object.keys(metadataObj).forEach(function (x) { + // We don't want to add our user data to the object multiple times. + var containsYou = false; + console.log(metadataObj); + Object.keys(metadataObj.users).forEach(function (x) { if (members.indexOf(x) === -1) { return; } - mdo[x] = metadataObj[x]; + mdo[x] = metadataObj.users[x]; + if (metadataObj.users[x].uid === meta.user.uid) { + console.log('document already contains you'); + containsYou = true; + } }); - mdo[myID] = personalMetadata; - metadataObj = mdo; + if (!containsYou) { mdo[myID] = meta.user; } + metadataObj.users = mdo; dirty = false; changeHandlers.forEach(function (f) { f(); }); }; @@ -27,8 +41,8 @@ define([], function () { setTimeout(checkUpdate); }; - sframeChan.on('EV_USERDATA_UPDATE', function (ev) { - personalMetadata = ev; + sframeChan.on('EV_METADATA_UPDATE', function (ev) { + meta = ev; change(); }); sframeChan.on('EV_RT_CONNECT', function (ev) { @@ -52,8 +66,9 @@ define([], function () { }); return Object.freeze({ - metadataChange: function (meta) { - metadataObj = meta; + updateMetadata: function (m) { + if (JSON.stringify(metadataObj) === JSON.stringify(m)) { return; } + metadataObj = m; change(); }, getMetadata: function () { diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index d4bd31f0e..e5b921859 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -52,7 +52,7 @@ define([ onConnectionChange({ state: false }); }); sframeChan.on('EV_RT_CONNECT', function (content) { - content.members.forEach(userList.onJoin); + //content.members.forEach(userList.onJoin); myID = content.myID; isReady = false; if (chainpad) { diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index 078a920e7..01b26d494 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -228,7 +228,9 @@ define([], function () { return { start: function (config) { - config.sframeChan.whenReg('EV_RT_READY', function () { start(config); }); + config.sframeChan.whenReg('EV_RT_READY', function () { + start(config); + }); } }; }); diff --git a/www/common/sframe-channel.js b/www/common/sframe-channel.js index 3dd0a3d8d..62250704a 100644 --- a/www/common/sframe-channel.js +++ b/www/common/sframe-channel.js @@ -18,6 +18,7 @@ define([ var chan = {}; + // Send a query. channel.query('Q_SOMETHING', { args: "whatever" }, function (reply) { ... }); chan.query = function (q, content, cb) { if (!otherWindow) { throw new Error('not yet initialized'); } if (!SFrameProtocol[q]) { @@ -40,6 +41,7 @@ define([ }), '*'); }; + // Fire an event. channel.event('EV_SOMETHING', { args: "whatever" }); var event = chan.event = function (e, content) { if (!otherWindow) { throw new Error('not yet initialized'); } if (!SFrameProtocol[e]) { @@ -51,13 +53,17 @@ define([ otherWindow.postMessage(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) { - if (!otherWindow) { throw new Error('not yet initialized'); } + if (!otherWindow && !quiet) { throw new Error('not yet initialized'); } if (!SFrameProtocol[queryType]) { throw new Error('please only register handlers which are defined in sframe-protocol.js'); } (handlers[queryType] = handlers[queryType] || []).push(function (data, msg) { handler(data.content, function (replyContent) { + if (queryType.indexOf('Q_') !== 0) { throw new Error("replies to events are invalid"); } msg.source.postMessage(JSON.stringify({ txid: data.txid, content: replyContent @@ -69,25 +75,35 @@ define([ } }; - chan.whenReg = function (queryType, handler) { + // 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) { 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'); } + var reg = always; if (insideHandlers.indexOf(queryType) > -1) { - handler(); + cb(); } else { - (callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(handler); + reg = true; + } + if (reg) { + (callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(cb); } }; - (handlers['EV_REGISTER_HANDLER'] = handlers['EV_REGISTER_HANDLER'] || []).push(function (data) { - if (callWhenRegistered[data.content]) { - callWhenRegistered[data.content].forEach(function (f) { f(); }); - delete callWhenRegistered[data.content]; + // 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(data.content); - }); + insideHandlers.push(content); + }, true); var intr; var txid; diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index b6ab3f7b8..ea7557fc7 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -34,5 +34,5 @@ define({ // 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 + 'EV_METADATA_UPDATE': true }); \ No newline at end of file diff --git a/www/pad2/main.js b/www/pad2/main.js index 675b05a25..3f527e72c 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -260,6 +260,7 @@ define([ // secret.keys = secret.key; //} var readOnly = false; // TODO + var cpNfInner; var $bar = $('#cke_1_toolbox'); @@ -339,7 +340,9 @@ define([ var stringifyDOM = module.stringifyDOM = function (dom) { var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); - + hjson[3] = { + metadata: cpNfInner.metadataMgr.getMetadata() + }; /*hjson[3] = { TODO users: UserList.userData, defaultTitle: Title.defaultTitle, @@ -380,9 +383,6 @@ define([ } }; - var meta; - var metaStr; - realtimeOptions.onRemote = function () { if (initializing) { return; } if (isHistoryMode) { return; } @@ -403,6 +403,10 @@ define([ newSInner = stringify(newInner[2]); } + if (newInner[3]) { + cpNfInner.metadataMgr.updateMetadata(newInner[3].metadata); + } + // build a dom from HJSON, diff, and patch the editor applyHjson(shjson); @@ -446,14 +450,6 @@ define([ if (newSInner && newSInner !== oldSInner) { Cryptpad.notify(); } - - var newMeta = newInner[3]; - var newMetaStr = JSON.stringify(newMeta); - if (newMetaStr !== metaStr) { - metaStr = newMetaStr; - meta = newMeta; - //meta[] HERE - } }; var exportFile = function () { @@ -473,7 +469,7 @@ define([ }; realtimeOptions.onInit = function (info) { - + console.log('onInit'); // TODO return; @@ -599,6 +595,7 @@ define([ // this should only ever get called once, when the chain syncs realtimeOptions.onReady = function (info) { + console.log('onReady'); if (!module.isMaximized) { module.isMaximized = true; $('iframe.cke_wysiwyg_frame').css('width', ''); @@ -686,9 +683,7 @@ define([ } }; - var cpNfInner = CpNfInner.start(realtimeOptions); - - + cpNfInner = CpNfInner.start(realtimeOptions); Cryptpad.onLogout(function () { setEditable(false); }); diff --git a/www/pad2/outer.js b/www/pad2/outer.js index e24a9a4b8..4d835bf03 100644 --- a/www/pad2/outer.js +++ b/www/pad2/outer.js @@ -18,6 +18,34 @@ define([ })); Cryptpad.ready(waitFor()); }).nThen(function (waitFor) { + var parsed = Cryptpad.parsePadUrl(window.location.href); + if (!parsed.type) { throw new Error(); } + var defaultTitle = Cryptpad.getDefaultName(parsed); + var updateMeta = function () { + console.log('EV_METADATA_UPDATE'); + var name; + nThen(function (waitFor) { + Cryptpad.getLastName(waitFor(function (n) { name = n })); + }).nThen(function (waitFor) { + sframeChan.event('EV_METADATA_UPDATE', { + doc: { + defaultTitle: defaultTitle, + type: parsed.type + }, + myID: Cryptpad.getNetwork().webChannels[0].myID, + user: { + name: name, + uid: Cryptpad.getUid(), + avatar: Cryptpad.getAvatarUrl(), + profile: Cryptpad.getProfileUrl(), + curvePublic: Cryptpad.getProxy().curvePublic + } + }); + }); + }; + Cryptpad.onDisplayNameChanged(updateMeta); + sframeChan.onReg('EV_METADATA_UPDATE', updateMeta); + Cryptpad.onError(function (info) { console.log('error'); console.log(info); From 33e73dd5e529dfea1fb9de7b91a873d95dd150ea Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Thu, 10 Aug 2017 21:40:34 +0200 Subject: [PATCH 007/121] fixed one last fight --- www/common/metadata-manager.js | 11 +++++------ www/pad2/main.js | 4 ++++ www/pad2/outer.js | 7 ++++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 24d03df2e..930a7c138 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -2,7 +2,6 @@ define([], function () { var UNINIT = 'uninitialized'; var create = function (sframeChan) { var meta = UNINIT; - var myID = UNINIT; var members = []; var metadataObj = UNINIT; var dirty = true; @@ -11,27 +10,27 @@ define([], function () { var checkUpdate = function () { if (!dirty) { return; } if (meta === UNINIT) { throw new Error(); } - if (myID === UNINIT) { myID = meta.myID; } if (metadataObj === UNINIT) { metadataObj = { defaultTitle: meta.doc.defaultTitle, title: meta.doc.defaultTitle, + type: meta.doc.type, users: {} }; } var mdo = {}; // We don't want to add our user data to the object multiple times. var containsYou = false; - console.log(metadataObj); + //console.log(metadataObj); Object.keys(metadataObj.users).forEach(function (x) { if (members.indexOf(x) === -1) { return; } mdo[x] = metadataObj.users[x]; if (metadataObj.users[x].uid === meta.user.uid) { - console.log('document already contains you'); + //console.log('document already contains you'); containsYou = true; } }); - if (!containsYou) { mdo[myID] = meta.user; } + if (!containsYou) { mdo[meta.user.netfluxId] = meta.user; } metadataObj.users = mdo; dirty = false; changeHandlers.forEach(function (f) { f(); }); @@ -46,7 +45,7 @@ define([], function () { change(); }); sframeChan.on('EV_RT_CONNECT', function (ev) { - myID = ev.myID; + meta.user.netfluxId = ev.myID; members = ev.members; change(); }); diff --git a/www/pad2/main.js b/www/pad2/main.js index 3f527e72c..62cc64922 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -622,6 +622,10 @@ define([ // Update the user list (metadata) from the hyperjson // XXX Metadata.update(shjson); + var parsed = JSON.parse(shjson); + if (parsed[3] && parsed[3].metadata) { + cpNfInner.metadataMgr.updateMetadata(parsed[3].metadata); + } if (!readOnly) { var shjson2 = stringifyDOM(inner); diff --git a/www/pad2/outer.js b/www/pad2/outer.js index 4d835bf03..2b88bb420 100644 --- a/www/pad2/outer.js +++ b/www/pad2/outer.js @@ -19,10 +19,11 @@ define([ Cryptpad.ready(waitFor()); }).nThen(function (waitFor) { var parsed = Cryptpad.parsePadUrl(window.location.href); + parsed.type = parsed.type.replace('pad2', 'pad'); if (!parsed.type) { throw new Error(); } var defaultTitle = Cryptpad.getDefaultName(parsed); var updateMeta = function () { - console.log('EV_METADATA_UPDATE'); + //console.log('EV_METADATA_UPDATE'); var name; nThen(function (waitFor) { Cryptpad.getLastName(waitFor(function (n) { name = n })); @@ -32,13 +33,13 @@ define([ defaultTitle: defaultTitle, type: parsed.type }, - myID: Cryptpad.getNetwork().webChannels[0].myID, user: { name: name, uid: Cryptpad.getUid(), avatar: Cryptpad.getAvatarUrl(), profile: Cryptpad.getProfileUrl(), - curvePublic: Cryptpad.getProxy().curvePublic + curvePublic: Cryptpad.getProxy().curvePublic, + netfluxId: Cryptpad.getNetwork().webChannels[0].myID, } }); }); From 4acd9957a928bfa8f3793d3a3a35a4001718298d Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Fri, 11 Aug 2017 10:40:57 +0200 Subject: [PATCH 008/121] Set the url if there is none --- bower.json | 2 +- www/common/sframe-chainpad-netflux-outer.js | 6 +++++- www/pad2/main.js | 2 +- www/pad2/outer.js | 12 ++++++++---- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/bower.json b/bower.json index bfc74af20..c680abdd6 100644 --- a/bower.json +++ b/bower.json @@ -40,7 +40,7 @@ "less": "^2.7.2", "bootstrap": "#v4.0.0-alpha.6", "diff-dom": "2.1.1", - "nthen": "^0.1.5" + "nthen": "^0.1.5", "open-sans-fontface": "^1.4.2" } } diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index 01b26d494..904cb9521 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -29,6 +29,7 @@ define([], function () { var readOnly = conf.readOnly || false; var network = conf.network; var sframeChan = conf.sframeChan; + var onConnect = conf.onConnect || function () { }; conf = undefined; var initializing = true; @@ -40,7 +41,7 @@ define([], function () { messageFromInner(message, cb); }); - var onReady = function () { + 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; } @@ -131,6 +132,9 @@ define([], function () { wcObject.wc = wc; channel = wc.id; + onConnect(wc); + onConnect = function () { }; + // Add the existing peers in the userList sframeChan.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly }); diff --git a/www/pad2/main.js b/www/pad2/main.js index 62cc64922..e4874a532 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -641,7 +641,7 @@ define([ } } } else { - Title.updateTitle(Cryptpad.initialName || Title.defaultTitle); + //Title.updateTitle(Cryptpad.initialName || Title.defaultTitle); documentBody.innerHTML = Messages.initialState; } diff --git a/www/pad2/outer.js b/www/pad2/outer.js index 2b88bb420..526035d3d 100644 --- a/www/pad2/outer.js +++ b/www/pad2/outer.js @@ -18,6 +18,10 @@ define([ })); Cryptpad.ready(waitFor()); }).nThen(function (waitFor) { + var secret = Cryptpad.getSecrets(); + var readOnly = secret.keys && !secret.keys.editKeyStr; + if (!secret.keys) { secret.keys = secret.key; } + var parsed = Cryptpad.parsePadUrl(window.location.href); parsed.type = parsed.type.replace('pad2', 'pad'); if (!parsed.type) { throw new Error(); } @@ -55,10 +59,6 @@ define([ } }); - var secret = Cryptpad.getSecrets(); - var readOnly = secret.keys && !secret.keys.editKeyStr; - if (!secret.keys) { secret.keys = secret.key; } - CpNfOuter.start({ sframeChan: sframeChan, channel: secret.channel, @@ -66,6 +66,10 @@ define([ validateKey: secret.keys.validateKey || undefined, readOnly: readOnly, crypto: Crypto.createEncryptor(secret.keys), + onConnect: function (wc) { + if (readOnly) { return; } + Cryptpad.replaceHash(Cryptpad.getEditHashFromKeys(wc.id, secret.keys)); + } }); }); }); \ No newline at end of file From 130b330ede16e1c01e19bd54e1ff99e9a7805d11 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 16 Aug 2017 10:04:50 +0200 Subject: [PATCH 009/121] refactor messaging --- www/common/common-messaging2.js | 692 ++++++++++++++++++++++++++++++++ 1 file changed, 692 insertions(+) create mode 100644 www/common/common-messaging2.js diff --git a/www/common/common-messaging2.js b/www/common/common-messaging2.js new file mode 100644 index 000000000..5f3d9af39 --- /dev/null +++ b/www/common/common-messaging2.js @@ -0,0 +1,692 @@ +define([ + 'jquery', + '/bower_components/chainpad-crypto/crypto.js', + '/common/curve.js', + '/common/common-hash.js', + '/common/common-realtime.js' +// '/bower_components/marked/marked.min.js' +], function ($, Crypto, Curve, Hash, Realtime) { + var Msg = { + inputs: [], + }; + + var Types = { + message: 'MSG', + update: 'UPDATE', + unfriend: 'UNFRIEND', + mapId: 'MAP_ID', + mapIdAck: 'MAP_ID_ACK' + }; + + // TODO + // - mute a channel (hide notifications or don't open it?) + + var ready = []; + var pending = {}; + var pendingRequests = []; + + var createData = Msg.createData = function (proxy, hash) { + return { + channel: hash || Hash.createChannelId(), + displayName: proxy['cryptpad.username'], + profile: proxy.profile && proxy.profile.view, + edPublic: proxy.edPublic, + curvePublic: proxy.curvePublic, + avatar: proxy.profile && proxy.profile.avatar + }; + }; + + var getFriend = function (proxy, pubkey) { + if (pubkey === proxy.curvePublic) { + var data = createData(proxy); + delete data.channel; + return data; + } + return proxy.friends ? proxy.friends[pubkey] : undefined; + }; + + var removeFromFriendList = function (proxy, realtime, curvePublic, cb) { + if (!proxy.friends) { return; } + var friends = proxy.friends; + delete friends[curvePublic]; + Realtime.whenRealtimeSyncs(realtime, cb); + }; + + var getFriendList = Msg.getFriendList = function (proxy) { + if (!proxy.friends) { proxy.friends = {}; } + return proxy.friends; + }; + + var eachFriend = function (friends, cb) { + Object.keys(friends).forEach(function (id) { + if (id === 'me') { return; } + cb(friends[id], id, friends); + }); + }; + + Msg.getFriendChannelsList = function (proxy) { + var list = []; + eachFriend(proxy, function (friend) { + list.push(friend.channel); + }); + return list; + }; + + var msgAlreadyKnown = function (channel, sig) { + return channel.messages.some(function (message) { + return message[0] === sig; + }); + }; + + var getMoreHistory = function (network, chan, hash, count) { + var msg = [ 'GET_HISTORY_RANGE', chan.id, { + from: hash, + count: count, + } + ]; + + network.sendto(network.historyKeeper, JSON.stringify(msg)).then(function () { + }, function (err) { + throw new Error(err); + }); + }; + + var getChannelMessagesSince = function (network, proxy, chan, data, keys) { + var cfg = { + validateKey: keys.validateKey, + owners: [proxy.edPublic, data.edPublic], + lastKnownHash: data.lastKnownHash + }; + var msg = ['GET_HISTORY', chan.id, cfg]; + network.sendto(network.historyKeeper, JSON.stringify(msg)) + .then($.noop, function (err) { + throw new Error(err); + }); + }; + + Msg.messenger = function (common) { + var messenger = {}; + + var DEBUG = function (label) { + console.log('event:' + label); + }; + + var channels = messenger.channels = {}; + + // declare common variables + var network = common.getNetwork(); + var proxy = common.getProxy(); + var realtime = common.getRealtime(); + Msg.hk = network.historyKeeper; + var friends = getFriendList(proxy); + + // Id message allows us to map a netfluxId with a public curve key + var onIdMessage = function (msg, sender) { + var channel; + var isId = Object.keys(channels).some(function (chanId) { + if (channels[chanId].userList.indexOf(sender) !== -1) { + channel = channels[chanId]; + return true; + } + }); + + if (!isId) { return; } + + var decryptedMsg = channel.encryptor.decrypt(msg); + + if (decryptedMsg === null) { + // console.error('unable to decrypt message'); + // console.error('potentially meant for yourself'); + + // message failed to parse, meaning somebody sent it to you but + // encrypted it with the wrong key, or you're sending a message to + // yourself in a different tab. + return; + } + + if (!decryptedMsg) { + console.error('decrypted message was falsey but not null'); + return; + } + + var parsed; + try { + parsed = JSON.parse(decryptedMsg); + } catch (e) { + console.error(decryptedMsg); + return; + } + if (parsed[0] !== Types.mapId && parsed[0] !== Types.mapIdAck) { return; } + + // check that the responding peer's encrypted netflux id matches + // the sender field. This is to prevent replay attacks. + if (parsed[2] !== sender || !parsed[1]) { return; } + channel.mapId[sender] = parsed[1]; + + channel.updateStatus(); + + if (parsed[0] !== Types.mapId) { return; } // Don't send your key if it's already an ACK + // Answer with your own key + var rMsg = [Types.mapIdAck, proxy.curvePublic, channel.wc.myID]; + var rMsgStr = JSON.stringify(rMsg); + var cryptMsg = channel.encryptor.encrypt(rMsgStr); + network.sendto(sender, cryptMsg); + }; + + var pushMsg = function (channel, cryptMsg) { + var msg = channel.encryptor.decrypt(cryptMsg); + + // TODO emit new message event or something + // extension point for other apps + console.log(msg); + + var sig = cryptMsg.slice(0, 64); + if (msgAlreadyKnown(channel, sig)) { return; } + + var parsedMsg = JSON.parse(msg); + if (parsedMsg[0] === Types.message) { + // TODO validate messages here + var res = { + type: parsedMsg[0], + sig: sig, + channel: parsedMsg[1], + time: parsedMsg[2], + text: parsedMsg[3], + }; + + channel.messages.push(res); + return true; + } + if (parsedMsg[0] === Types.update) { + if (parsedMsg[1] === proxy.curvePublic) { return; } + var newdata = parsedMsg[3]; + var data = getFriend(proxy, parsedMsg[1]); + var types = []; + Object.keys(newdata).forEach(function (k) { + if (data[k] !== newdata[k]) { + types.push(k); + data[k] = newdata[k]; + } + }); + channel.updateUI(types); + return; + } + if (parsedMsg[0] === Types.unfriend) { + removeFromFriendList(proxy, realtime, channel.friendEd, function () { + channel.wc.leave(Types.unfriend); + channel.removeUI(); + }); + return; + } + }; + + /* Broadcast a display name, profile, or avatar change to all contacts + */ + var updateMyData = function (proxy) { + var friends = getFriendList(proxy); + var mySyncData = friends.me; + var myData = createData(proxy); + if (!mySyncData || mySyncData.displayName !== myData.displayName + || mySyncData.profile !== myData.profile + || mySyncData.avatar !== myData.avatar) { + delete myData.channel; + Object.keys(channels).forEach(function (chan) { + var channel = channels[chan]; + var msg = [Types.update, myData.curvePublic, +new Date(), myData]; + var msgStr = JSON.stringify(msg); + var cryptMsg = channel.encryptor.encrypt(msgStr); + channel.wc.bcast(cryptMsg).then(function () { + channel.refresh(); + }, function (err) { + console.error(err); + }); + }); + friends.me = myData; + } + }; + + var onChannelReady = function (proxy, chanId) { + if (ready.indexOf(chanId) !== -1) { return; } + ready.push(chanId); + channels[chanId].updateStatus(); // c'est quoi? + var friends = getFriendList(proxy); + if (ready.length === Object.keys(friends).length) { + // All channels are ready + updateMyData(proxy); + } + return ready.length; + }; + + var onDirectMessage = function (common, msg, sender) { + if (sender !== Msg.hk) { return void onIdMessage(msg, sender); } + var parsed = JSON.parse(msg); + if ((parsed.validateKey || parsed.owners) && parsed.channel) { + return; + } + if (parsed.state && parsed.state === 1 && parsed.channel) { + if (channels[parsed.channel]) { + // parsed.channel is Ready + // TODO: call a function that shows that the channel is ready? (remove a spinner, ...) + // channel[parsed.channel].ready(); + channels[parsed.channel].ready = true; + onChannelReady(proxy, parsed.channel); + var updateTypes = channels[parsed.channel].updateOnReady; + if (updateTypes) { + channels[parsed.channel].updateUI(updateTypes); + } + } + return; + } + var chan = parsed[3]; + if (!chan || !channels[chan]) { return; } + pushMsg(channels[chan], parsed[4]); + channels[chan].refresh(); + }; + + var onMessage = function (common, msg, sender, chan) { + if (!channels[chan.id]) { return; } + + var realtime = common.getRealtime(); + var proxy = common.getProxy(); + + var isMessage = pushMsg(channels[chan.id], msg); + if (isMessage) { + // Don't notify for your own messages + if (channels[chan.id].wc.myID !== sender) { + channels[chan.id].notify(); + } + channels[chan.id].refresh(); + } + }; + + // listen for messages... + network.on('message', function(msg, sender) { + onDirectMessage(common, msg, sender); + }); + + // Refresh the active channel + // TODO extract into UI method + var refresh = function (/*curvePublic*/) { + return; + /* + if (messenger.active !== curvePublic) { return; } + var data = friends[curvePublic]; + if (!data) { return; } + var channel = channels[data.channel]; + if (!channel) { return; } + + var $chat = ui.getChannel(curvePublic); + + if (!$chat) { return; } + + // Add new messages + var messages = channel.messages; + var $messages = $chat.find('.messages'); + var msg, name; + var last = typeof(channel.lastDisplayed) === 'number'? channel.lastDisplayed: -1; + for (var i = last + 1; i 10) { + var lastKnownMsg = messages[messages.length - 11]; + channel.setLastMessageRead(lastKnownMsg.sig); + }*/ + }; + // Display a new channel + // TODO extract into UI method + //var display = function (curvePublic) { + //curvePublic = curvePublic; + /* + ui.hideInfo(); + var $chat = ui.getChannel(curvePublic); + if (!$chat) { + $chat = ui.createChat(curvePublic); + ui.createChatBox(proxy, $chat, curvePublic); + } + // Show the correct div + ui.hideChat(); + $chat.show(); + + // TODO set this attr per-messenger + messenger.setActive(curvePublic); + // TODO don't mark messages as read unless you have displayed them + + refresh(curvePublic);*/ + //}; + + // TODO take a callback + var remove = function (curvePublic) { + var data = getFriend(proxy, curvePublic); + var channel = channels[data.channel]; + var msg = [Types.unfriend, proxy.curvePublic, +new Date()]; + var msgStr = JSON.stringify(msg); + var cryptMsg = channel.encryptor.encrypt(msgStr); + channel.wc.bcast(cryptMsg).then(function () { + removeFromFriendList(common, curvePublic, function () { + channel.wc.leave(Types.unfriend); + channel.removeUI(); + }); + }, function (err) { + console.error(err); + }); + }; + + // Open the channels + var openFriendChannel = function (data, f) { + var keys = Curve.deriveKeys(data.curvePublic, proxy.curvePrivate); + var encryptor = Curve.createEncryptor(keys); + network.join(data.channel).then(function (chan) { + var channel = channels[data.channel] = { + sending: false, + friendEd: f, + keys: keys, + encryptor: encryptor, + messages: [], + unfriend: function () { + DEBUG('unfriend'); + remove(data.curvePublic); + }, + refresh: function () { + DEBUG('refresh'); + refresh(data.curvePublic); + }, + notify: function () { + //ui.notify(data.curvePublic); + DEBUG('notify'); + common.notify(); // HERE + }, + unnotify: function () { + DEBUG('unnotify'); + //ui.unnotify(data.curvePublic); + }, + removeUI: function () { + DEBUG('remove-ui'); + //ui.remove(data.curvePublic); + }, + updateUI: function (/*types*/) { + DEBUG('update-ui'); + //ui.update(data.curvePublic, types); + }, + updateStatus: function () { + DEBUG('update-status'); + //ui.updateStatus(data.curvePublic, + //channel.getStatus(data.curvePublic)); + }, + setLastMessageRead: function (hash) { + DEBUG('updateLastKnownHash'); + data.lastKnownHash = hash; + }, + getLastMessageRead: function () { + return data.lastKnownHash; + }, + isActive: function () { + return data.curvePublic === messenger.active; + }, + getMessagesSinceDisconnect: function () { + getChannelMessagesSince(network, proxy, chan, data, keys); + }, + wc: chan, + userList: [], + mapId: {}, + getStatus: function (curvePublic) { + return channel.userList.some(function (nId) { + return channel.mapId[nId] === curvePublic; + }); + }, + getPreviousMessages: function () { + var history = channel.messages; + if (!history || !history.length) { + // TODO ask for default history? + return; + } + + var oldestMessage = history[0]; + if (!oldestMessage) { + return; // nothing to fetch + } + + var messageHash = oldestMessage[0]; + getMoreHistory(network, chan, messageHash, 10); + }, + send: function (payload, cb) { + if (!network.webChannels.some(function (wc) { + if (wc.id === channel.wc.id) { return true; } + })) { + return void cb('NO_SUCH_CHANNEL'); + } + + var msg = [Types.message, proxy.curvePublic, +new Date(), payload]; + var msgStr = JSON.stringify(msg); + var cryptMsg = channel.encryptor.encrypt(msgStr); + + channel.wc.bcast(cryptMsg).then(function () { + pushMsg(channel, cryptMsg); + cb(); + }, function (err) { + cb(err); + }); + }, + }; + chan.on('message', function (msg, sender) { + onMessage(common, msg, sender, chan); + }); + + var onJoining = function (peer) { + if (peer === Msg.hk) { return; } + if (channel.userList.indexOf(peer) !== -1) { return; } + channel.userList.push(peer); + var msg = [Types.mapId, proxy.curvePublic, chan.myID]; + var msgStr = JSON.stringify(msg); + var cryptMsg = channel.encryptor.encrypt(msgStr); + network.sendto(peer, cryptMsg); + channel.updateStatus(); + }; + chan.members.forEach(function (peer) { + if (peer === Msg.hk) { return; } + if (channel.userList.indexOf(peer) !== -1) { return; } + channel.userList.push(peer); + }); + chan.on('join', onJoining); + chan.on('leave', function (peer) { + var i = channel.userList.indexOf(peer); + while (i !== -1) { + channel.userList.splice(i, 1); + i = channel.userList.indexOf(peer); + } + channel.updateStatus(); + }); + + getChannelMessagesSince(network, proxy, chan, data, keys); + }, function (err) { + console.error(err); + }); + }; + + messenger.getLatestMessages = function () { + Object.keys(channels).forEach(function (id) { + if (id === 'me') { return; } + var friend = channels[id]; + friend.getMessagesSinceDisconnect(); + friend.refresh(); + }); + }; + + messenger.cleanFriendChannels = function () { + Object.keys(channels).forEach(function (id) { + delete channels[id]; + }); + }; + + var openFriendChannels = messenger.openFriendChannels = function () { + eachFriend(friends, openFriendChannel); + }; + + //messenger.setEditable = ui.setEditable; + + openFriendChannels(); + + // TODO split loop innards into ui methods + var checkNewFriends = function () { + eachFriend(friends, function (friend, id) { + if (!channels[id]) { + openFriendChannel(friend, id); + } + }); + }; + + common.onDisplayNameChanged(function () { + checkNewFriends(); + updateMyData(proxy); + }); + + return messenger; + }; + + // Invitation + // FIXME there are too many functions with this name + var addToFriendList = Msg.addToFriendList = function (common, data, cb) { + var proxy = common.getProxy(); + var friends = getFriendList(proxy); + var pubKey = data.curvePublic; + + if (pubKey === proxy.curvePublic) { return void cb("E_MYKEY"); } + + friends[pubKey] = data; + + Realtime.whenRealtimeSyncs(common.getRealtime(), function () { + cb(); + common.pinPads([data.channel]); + }); + common.changeDisplayName(proxy[common.displayNameKey]); + }; + + /* Used to accept friend requests within apps other than /contacts/ */ + Msg.addDirectMessageHandler = function (common) { + var network = common.getNetwork(); + var proxy = common.getProxy(); + if (!network) { return void console.error('Network not ready'); } + network.on('message', function (message, sender) { + var msg; + if (sender === network.historyKeeper) { return; } + try { + var parsed = common.parsePadUrl(window.location.href); + if (!parsed.hashData) { return; } + var chan = parsed.hashData.channel; + // Decrypt + var keyStr = parsed.hashData.key; + var cryptor = Crypto.createEditCryptor(keyStr); + var key = cryptor.cryptKey; + var decryptMsg; + try { + decryptMsg = Crypto.decrypt(message, key); + } catch (e) { + // If we can't decrypt, it means it is not a friend request message + } + if (!decryptMsg) { return; } + // Parse + msg = JSON.parse(decryptMsg); + if (msg[1] !== parsed.hashData.channel) { return; } + var msgData = msg[2]; + var msgStr; + if (msg[0] === "FRIEND_REQ") { + msg = ["FRIEND_REQ_NOK", chan]; + var todo = function (yes) { + if (yes) { + pending[sender] = msgData; + msg = ["FRIEND_REQ_OK", chan, createData(common, msgData.channel)]; + } + msgStr = Crypto.encrypt(JSON.stringify(msg), key); + network.sendto(sender, msgStr); + }; + var existing = getFriend(proxy, msgData.curvePublic); + if (existing) { + todo(true); + return; + } + var confirmMsg = common.Messages._getKey('contacts_request', [ + common.fixHTML(msgData.displayName) + ]); + common.confirm(confirmMsg, todo, null, true); + return; + } + if (msg[0] === "FRIEND_REQ_OK") { + var idx = pendingRequests.indexOf(sender); + if (idx !== -1) { pendingRequests.splice(idx, 1); } + + // FIXME clarify this function's name + addToFriendList(common, msgData, function (err) { + if (err) { + return void common.log(common.Messages.contacts_addError); + } + common.log(common.Messages.contacts_added); + var msg = ["FRIEND_REQ_ACK", chan]; + var msgStr = Crypto.encrypt(JSON.stringify(msg), key); + network.sendto(sender, msgStr); + }); + return; + } + if (msg[0] === "FRIEND_REQ_NOK") { + var i = pendingRequests.indexOf(sender); + if (i !== -1) { pendingRequests.splice(i, 1); } + common.log(common.Messages.contacts_rejected); + common.changeDisplayName(proxy[common.displayNameKey]); + return; + } + if (msg[0] === "FRIEND_REQ_ACK") { + var data = pending[sender]; + if (!data) { return; } + addToFriendList(common, data, function (err) { + if (err) { + return void common.log(common.Messages.contacts_addError); + } + common.log(common.Messages.contacts_added); + }); + return; + } + // TODO: timeout ACK: warn the user + } catch (e) { + console.error("Cannot parse direct message", msg || message, "from", sender, e); + } + }); + }; + + Msg.getPending = function () { + return pendingRequests; + }; + + Msg.inviteFromUserlist = function (common, netfluxId) { + var network = common.getNetwork(); + var parsed = common.parsePadUrl(window.location.href); + if (!parsed.hashData) { return; } + // Message + var chan = parsed.hashData.channel; + var myData = createData(common); + var msg = ["FRIEND_REQ", chan, myData]; + // Encryption + var keyStr = parsed.hashData.key; + var cryptor = Crypto.createEditCryptor(keyStr); + var key = cryptor.cryptKey; + var msgStr = Crypto.encrypt(JSON.stringify(msg), key); + // Send encrypted message + if (pendingRequests.indexOf(netfluxId) === -1) { + pendingRequests.push(netfluxId); + var proxy = common.getProxy(); + // this redraws the userlist after a change has occurred + // TODO rename this function to reflect its purpose + common.changeDisplayName(proxy[common.displayNameKey]); + } + network.sendto(netfluxId, msgStr); + }; + + return Msg; +}); From 744fb407ae87631760578fe1f20b6b582a27c270 Mon Sep 17 00:00:00 2001 From: CatalinScr Date: Wed, 16 Aug 2017 15:53:10 +0300 Subject: [PATCH 010/121] Updated the what is page --- customize.dist/bkabout.jpg | Bin 17129 -> 67668 bytes customize.dist/bkwhat.jpg | Bin 0 -> 103964 bytes customize.dist/pages.js | 60 ++++++++++++------ .../src/less2/pages/page-login.less | 2 +- .../less2/pages/page-what-is-cryptpad.less | 38 ++++++++++- 5 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 customize.dist/bkwhat.jpg diff --git a/customize.dist/bkabout.jpg b/customize.dist/bkabout.jpg index 43de082685b0dd5ad25189e81a4585715d741b02..01a51c1769011b6a578e50af4f4a052a0c3538d3 100644 GIT binary patch literal 67668 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 literal 17129 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 diff --git a/customize.dist/bkwhat.jpg b/customize.dist/bkwhat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3549f7acb5fa66b36b632743e5e711ce58132e2c GIT binary patch literal 103964 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%0AsYL=#mbCp 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 Z71JN6@#5TEv$oZL1R2R;ZjE+9|Jf`*7%>0< literal 0 HcmV?d00001 diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 480538800..0691bd2f4 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -292,32 +292,54 @@ define([ Pages['/what-is-cryptpad.html'] = function () { return h('div#cp-main', [ infopageTopbar(), + h('div.container-fluid.cp-what-is',[ + h('div.container',[ + h('div.row',[ + h('div.col-12.text-center', h('h1', Msg.whatis_title)), + ]), + ]), + ]), h('div.container.cp-container', [ - h('center', h('h1', Msg.whatis_title)), - setHTML(h('h2'), Msg.whatis_collaboration), - setHTML(h('p'), Msg.whatis_collaboration_p1), - h('img', { src: '/customize/images/pad_screenshot.png?' + urlArgs }), - setHTML(h('p'), Msg.whatis_collaboration_p2), - setHTML(h('p'), Msg.whatis_collaboration_p3), - setHTML(h('h2'), Msg.whatis_zeroknowledge), - h('div.row', [ - h('div.col-md-4.align-self-center', [ - h('img#zeroknowledge', { src: '/customize/images/zeroknowledge_small.png?' + urlArgs }), + h('div.row.align-items-center', [ + h('div.col-12.col-sm-12.col-md-12.col-lg-6', [ + setHTML(h('h2'), Msg.whatis_collaboration), + setHTML(h('p'), Msg.whatis_collaboration_p1), + setHTML(h('p'), Msg.whatis_collaboration_p2), + setHTML(h('p'), Msg.whatis_collaboration_p3), + ]), + h('div.col-12.col-sm-12.col-md-12.col-lg-6', [ + h('img', { src: '/customize/images/pad_screenshot.png?' + urlArgs }), ]), - h('div.col-md-8', [ + ]), + h('div.row.align-items-center', [ + h('div.col-12.col-sm-12.col-md-12.col-lg-6.push-lg-6', [ + setHTML(h('h2'), Msg.whatis_zeroknowledge), setHTML(h('p'), Msg.whatis_zeroknowledge_p1), setHTML(h('p'), Msg.whatis_zeroknowledge_p2), setHTML(h('p'), Msg.whatis_zeroknowledge_p3), ]), + h('div.col-12.col-sm-12.col-md-12.col-lg-6.pull-lg-6', [ + h('img#zeroknowledge', { src: '/customize/images/zeroknowledge_small.png?' + urlArgs }), + ]), + ]), + h('div.row.align-items-center', [ + h('div.col-12.col-sm-12.col-md-12.col-lg-6', [ + setHTML(h('h2'), Msg.whatis_drive), + setHTML(h('p'), Msg.whatis_drive_p1), + setHTML(h('p'), Msg.whatis_drive_p2), + setHTML(h('p'), Msg.whatis_drive_p3), + ]), + h('div.col-12.col-sm-12.col-md-12.col-lg-6', [ + h('img', { src: '/customize/images/drive_screenshot.png?' + urlArgs }), + ]), + ]), + h('div.row.align-items-center', [ + h('div.col-12', [ + setHTML(h('h2.text-center'), Msg.whatis_business), + setHTML(h('p'), Msg.whatis_business_p1), + setHTML(h('p'), Msg.whatis_business_p2), + ]), ]), - setHTML(h('h2'), Msg.whatis_drive), - setHTML(h('p'), Msg.whatis_drive_p1), - h('img', { src: '/customize/images/drive_screenshot.png?' + urlArgs }), - setHTML(h('p'), Msg.whatis_drive_p2), - setHTML(h('p'), Msg.whatis_drive_p3), - setHTML(h('h2'), Msg.whatis_business), - setHTML(h('p'), Msg.whatis_business_p1), - setHTML(h('p'), Msg.whatis_business_p2), ]), infopageFooter(), ]); diff --git a/customize.dist/src/less2/pages/page-login.less b/customize.dist/src/less2/pages/page-login.less index cbde08893..050064348 100644 --- a/customize.dist/src/less2/pages/page-login.less +++ b/customize.dist/src/less2/pages/page-login.less @@ -73,6 +73,6 @@ } } .cp-container { - padding-top: 0; + padding-top: 3em; min-height: 66vh; } \ No newline at end of file diff --git a/customize.dist/src/less2/pages/page-what-is-cryptpad.less b/customize.dist/src/less2/pages/page-what-is-cryptpad.less index b57517a01..cf127e1ec 100644 --- a/customize.dist/src/less2/pages/page-what-is-cryptpad.less +++ b/customize.dist/src/less2/pages/page-what-is-cryptpad.less @@ -4,6 +4,40 @@ .infopages_main(); .infopages_topbar(); -img#zeroknowledge { - width: 100%; +.cp-what-is { + padding-top: 3em; + padding-bottom: 3em; + background-image: url(/customize/bkwhat.jpg); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + color: #fff; + h1 { + font-weight: 700; + } } +#cp-main { + background: #fff; +} +.cp-container { + padding-top: 3em; + padding-bottom: 3em; + h2 { + margin-top: 0; + font-weight: 700; + color: @cryptpad_header_col; + } + p { + color: @cryptpad_text_col + } + #zeroknowledge { + width: 65%; + } + .row { + margin-bottom: 1.5em; + } + img { + display: block; + margin: 0 auto; + } +} \ No newline at end of file From 4881f8d0308b63747cff90a7ef09edb28517ba9b Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Thu, 17 Aug 2017 12:12:40 +0200 Subject: [PATCH 011/121] Remove X-Frame-Options because it cannot work with a cross-domain iframe. --- config.example.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config.example.js b/config.example.js index 43438aea6..73444b94c 100644 --- a/config.example.js +++ b/config.example.js @@ -17,8 +17,7 @@ module.exports = { httpHeaders: { "X-XSS-Protection": "1; mode=block", - "X-Content-Type-Options": "nosniff", - 'X-Frame-Options': 'SAMEORIGIN', + "X-Content-Type-Options": "nosniff" }, contentSecurity: [ From 839c6df3bb35acdc521ab81e755db0d43fbb4890 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 17 Aug 2017 14:16:37 +0200 Subject: [PATCH 012/121] remove invalid loading screen tips. create a new one --- customize.dist/translations/messages.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 262f9890c..b857e4b73 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -685,13 +685,12 @@ define(function () { // Tips out.tips = {}; - out.tips.lag = "The green icon in the upper right shows the quality of your internet connection to the CryptPad server."; out.tips.shortcuts = "`ctrl+b`, `ctrl+i` and `ctrl+u` are quick shortcuts for bold, italic and underline."; out.tips.indent = "In numbered and bulleted lists, you can use tab or shift+tab to quickly increase or decrease indentation."; - out.tips.title = "You can set the title of your pad by clicking the top center."; out.tips.store = "Every time you visit a pad, if you're logged in it will be saved to your CryptDrive."; out.tips.marker = "You can highlight text in a pad using the \"marker\" item in the styles dropdown menu."; out.tips.driveUpload = "Registered users can upload encrypted files by dragging and dropping them into their CryptDrive."; + out.tips.filenames = "You can rename files in your CryptDrive, this name is just for you."; out.feedback_about = "If you're reading this, you were probably curious why CryptPad is requesting web pages when you perform certain actions"; out.feedback_privacy = "We care about your privacy, and at the same time we want CryptPad to be very easy to use. We use this file to figure out which UI features matter to our users, by requesting it along with a parameter specifying which action was taken."; From 48fe3ef8418343c86f8fc8f4e34fdfb3b18786b6 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 17 Aug 2017 14:25:24 +0200 Subject: [PATCH 013/121] add a tip about cryptdrive --- customize.dist/translations/messages.js | 1 + 1 file changed, 1 insertion(+) diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index b857e4b73..acab6951d 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -691,6 +691,7 @@ define(function () { out.tips.marker = "You can highlight text in a pad using the \"marker\" item in the styles dropdown menu."; out.tips.driveUpload = "Registered users can upload encrypted files by dragging and dropping them into their CryptDrive."; out.tips.filenames = "You can rename files in your CryptDrive, this name is just for you."; + out.tips.drive = "Logged in users can organize their files in their CryptDrive, accessible from the CryptPad icon at the top left of all pads."; out.feedback_about = "If you're reading this, you were probably curious why CryptPad is requesting web pages when you perform certain actions"; out.feedback_privacy = "We care about your privacy, and at the same time we want CryptPad to be very easy to use. We use this file to figure out which UI features matter to our users, by requesting it along with a parameter specifying which action was taken."; From 3b20dcd435166a22b4502b7f42c174b8c700223d Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 17 Aug 2017 14:34:22 +0200 Subject: [PATCH 014/121] add optional cache-busting to favicon --- www/common/notify.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/www/common/notify.js b/www/common/notify.js index caf9b16af..7ce087b63 100644 --- a/www/common/notify.js +++ b/www/common/notify.js @@ -1,4 +1,11 @@ (function () { + var Mod = function (ApiConfig) { + var requireConf; + if (ApiConfig && ApiConfig.requireConf) { + requireConf = ApiConfig.requireConf; + } + var urlArgs = typeof(requireConf.urlArgs) === 'string'? '?' + urlArgs: ''; + var Module = {}; var isSupported = Module.isSupported = function () { @@ -41,8 +48,8 @@ } }; - var DEFAULT_MAIN = '/customize/main-favicon.png'; - var DEFAULT_ALT = '/customize/alt-favicon.png'; + var DEFAULT_MAIN = '/customize/main-favicon.png' + urlArgs; + var DEFAULT_ALT = '/customize/alt-favicon.png' + urlArgs; var createFavicon = function () { console.log("creating favicon"); @@ -110,13 +117,13 @@ cancel: cancel, }; }; + return Module; + }; if (typeof(module) !== 'undefined' && module.exports) { module.exports = Module; } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { - define(function () { - return Module; - }); + define(['/api/config'], Mod); } else { window.Visible = Module; } From fb512c89230e942e4f578d5fae871be6f7483213 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 17 Aug 2017 14:55:44 +0200 Subject: [PATCH 015/121] Toolbar in pad2 --- www/common/common-inner.js | 8 + www/common/metadata-manager.js | 22 +- www/common/toolbar3.js | 1044 ++++++++++++++++++++++++++++++++ www/pad2/main.js | 19 +- 4 files changed, 1085 insertions(+), 8 deletions(-) create mode 100644 www/common/common-inner.js create mode 100644 www/common/toolbar3.js diff --git a/www/common/common-inner.js b/www/common/common-inner.js new file mode 100644 index 000000000..921d10e88 --- /dev/null +++ b/www/common/common-inner.js @@ -0,0 +1,8 @@ +define([ + 'jquery' +], function ($) { + var common = {}; + + + return common; +}); diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 930a7c138..8a87b7f0a 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -20,18 +20,21 @@ define([], function () { } var mdo = {}; // We don't want to add our user data to the object multiple times. - var containsYou = false; + //var containsYou = false; //console.log(metadataObj); + console.log(metadataObj.users); Object.keys(metadataObj.users).forEach(function (x) { if (members.indexOf(x) === -1) { return; } mdo[x] = metadataObj.users[x]; - if (metadataObj.users[x].uid === meta.user.uid) { + /*if (metadataObj.users[x].uid === meta.user.uid) { //console.log('document already contains you'); containsYou = true; - } + }*/ }); - if (!containsYou) { mdo[meta.user.netfluxId] = meta.user; } + //if (!containsYou) { mdo[meta.user.netfluxId] = meta.user; } + mdo[meta.user.netfluxId] = meta.user; metadataObj.users = mdo; + dirty = false; changeHandlers.forEach(function (f) { f(); }); }; @@ -74,8 +77,15 @@ define([], function () { checkUpdate(); return metadataObj; }, - onChange: function (f) { changeHandlers.push(f); } + onChange: function (f) { changeHandlers.push(f); }, + getNetfluxId: function () { + return meta && meta.user && meta.user.netfluxId; + }, + getUserlist: function () { + var list = members.slice().filter(function (m) { return m.length === 32; }); + return list; + } }); }; return Object.freeze({ create: create }); -}); \ No newline at end of file +}); diff --git a/www/common/toolbar3.js b/www/common/toolbar3.js new file mode 100644 index 000000000..7b5cd287a --- /dev/null +++ b/www/common/toolbar3.js @@ -0,0 +1,1044 @@ +define([ + 'jquery', + '/customize/application_config.js', + '/api/config', +], function ($, Config, ApiConfig) { + var Messages = {}; + var Cryptpad; + + var Bar = { + constants: {}, + }; + + var SPINNER_DISAPPEAR_TIME = 1000; + + // Toolbar parts + var TOOLBAR_CLS = Bar.constants.toolbar = 'cryptpad-toolbar'; + var TOP_CLS = Bar.constants.top = 'cryptpad-toolbar-top'; + var LEFTSIDE_CLS = Bar.constants.leftside = 'cryptpad-toolbar-leftside'; + var RIGHTSIDE_CLS = Bar.constants.rightside = 'cryptpad-toolbar-rightside'; + var DRAWER_CLS = Bar.constants.drawer = 'drawer-content'; + var HISTORY_CLS = Bar.constants.history = 'cryptpad-toolbar-history'; + + // Userlist + var USERLIST_CLS = Bar.constants.userlist = "cryptpad-dropdown-users"; + var EDITSHARE_CLS = Bar.constants.editShare = "cryptpad-dropdown-editShare"; + var VIEWSHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-viewShare"; + var SHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-share"; + + // Top parts + var USER_CLS = Bar.constants.userAdmin = "cryptpad-user"; + var SPINNER_CLS = Bar.constants.spinner = 'cryptpad-spinner'; + var LIMIT_CLS = Bar.constants.lag = 'cryptpad-limit'; + var TITLE_CLS = Bar.constants.title = "cryptpad-title"; + var NEWPAD_CLS = Bar.constants.newpad = "cryptpad-new"; + + // User admin menu + var USERADMIN_CLS = Bar.constants.user = 'cryptpad-user-dropdown'; + var USERNAME_CLS = Bar.constants.username = 'cryptpad-toolbar-username'; + var READONLY_CLS = Bar.constants.readonly = 'cryptpad-readonly'; + var USERBUTTON_CLS = Bar.constants.changeUsername = "cryptpad-change-username"; + + // Create the toolbar element + + var uid = function () { + return 'cryptpad-uid-' + String(Math.random()).substring(2); + }; + + var createRealtimeToolbar = function (config) { + if (!config.$container) { return; } + var $container = config.$container; + var $toolbar = $('
', { + 'class': TOOLBAR_CLS, + id: uid(), + }); + + // TODO iframe + // var parsed = Cryptpad.parsePadUrl(window.location.href); + var parsed = { type:'pad' }; + if (typeof parsed.type === "string") { + config.$container.parents('body').addClass('app-' + parsed.type); + } + + var $topContainer = $('
', {'class': TOP_CLS}); + $('', {'class': 'filler'}).appendTo($topContainer); + var $userContainer = $('', { + 'class': USER_CLS + }).appendTo($topContainer); + $('', {'class': LIMIT_CLS}).hide().appendTo($userContainer); + $('', {'class': NEWPAD_CLS + ' dropdown-bar'}).hide().appendTo($userContainer); + $('', {'class': USERADMIN_CLS + ' dropdown-bar'}).hide().appendTo($userContainer); + + $toolbar.append($topContainer) + .append($('
', {'class': LEFTSIDE_CLS})) + .append($('
', {'class': RIGHTSIDE_CLS})) + .append($('
', {'class': HISTORY_CLS})); + + // TODO + /* + var $rightside = $toolbar.find('.'+RIGHTSIDE_CLS); + if (!config.hideDrawer) { + var $drawerContent = $('
', { + 'class': DRAWER_CLS,// + ' dropdown-bar-content cryptpad-dropdown' + 'tabindex': 1 + }).appendTo($rightside).hide(); + var $drawer = Cryptpad.createButton('more', true).appendTo($rightside); + $drawer.click(function () { + $drawerContent.toggle(); + $drawer.removeClass('active'); + if ($drawerContent.is(':visible')) { + $drawer.addClass('active'); + $drawerContent.focus(); + } + }); + var onBlur = function (e) { + if (e.relatedTarget) { + if ($(e.relatedTarget).is('.drawer-button')) { return; } + if ($(e.relatedTarget).parents('.'+DRAWER_CLS).length) { + $(e.relatedTarget).blur(onBlur); + return; + } + } + $drawer.removeClass('active'); + $drawerContent.hide(); + }; + $drawerContent.blur(onBlur); + }*/ + + // The 'notitle' class removes the line added for the title with a small screen + if (!config.title || typeof config.title !== "object") { + $toolbar.addClass('notitle'); + } + + $container.prepend($toolbar); + + $container.on('drop dragover', function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + return $toolbar; + }; + + // Userlist elements + + var getOtherUsers = function(config) { + var userList = config.userList.getUserlist(); + var userData = config.userList.getMetadata().users; + + var i = 0; // duplicates counter + var list = []; + + // Display only one time each user (if he is connected in multiple tabs) + var uids = []; + userList.forEach(function(user) { + //if (user !== userNetfluxId) { + var data = userData[user] || {}; + var userId = data.uid; + if (!userId) { return; } + data.netfluxId = user; + if (uids.indexOf(userId) === -1) {// && (!myUid || userId !== myUid)) { + uids.push(userId); + list.push(data); + } else { i++; } + //} + }); + return { + list: list, + duplicates: i + }; + }; + var arrayIntersect = function(a, b) { + return $.grep(a, function(i) { + return $.inArray(i, b) > -1; + }); + }; + var updateDisplayName = function (toolbar, config) { + // Change username in useradmin dropdown + var name = Cryptpad.getDisplayName(); + if (config.displayed.indexOf('useradmin') !== -1) { + var $userAdminElement = toolbar.$userAdmin; + var $userElement = $userAdminElement.find('.' + USERNAME_CLS); + $userElement.show(); + if (config.readOnly === 1) { + $userElement.addClass(READONLY_CLS).text(Messages.readonly); + } + else { + if (!name) { + name = Messages.anonymous; + } + $userElement.removeClass(READONLY_CLS).text(name); + } + } + }; + var avatars = {}; + var updateUserList = function (toolbar, config) { + // Make sure the elements are displayed + var $userButtons = toolbar.userlist; + var $userlistContent = toolbar.userlistContent; + + var userList = config.userList.getUserlist(); + var userData = config.userList.getMetadata().users; +console.log(userList, userData); + var numberOfUsers = userList.length; + + // If we are using old pads (readonly unavailable), only editing users are in userList. + // With new pads, we also have readonly users in userList, so we have to intersect with + // the userData to have only the editing users. We can't use userData directly since it + // may contain data about users that have already left the channel. + userList = config.readOnly === -1 ? userList : arrayIntersect(userList, Object.keys(userData)); + + // Names of editing users + var others = getOtherUsers(config); + var editUsersNames = others.list; + var duplicates = others.duplicates; // Number of duplicates + + editUsersNames.sort(function (a, b) { + var na = a.name || Messages.anonymous; + var nb = b.name || Messages.anonymous; + return na.toLowerCase() > nb.toLowerCase(); + }); + + var numberOfEditUsers = userList.length - duplicates; + var numberOfViewUsers = numberOfUsers - userList.length; + + // Update the userlist + var $editUsers = $userlistContent.find('.' + USERLIST_CLS).html(''); + Cryptpad.clearTooltips(); + + var $editUsersList = $('
', {'class': 'userlist-others'}); + + // Editors + // TODO iframe enable friends + //var pendingFriends = Cryptpad.getPendingInvites(); + editUsersNames.forEach(function (data) { + var name = data.name || Messages.anonymous; + var $span = $('', {'class': 'avatar'}); + var $rightCol = $('', {'class': 'right-col'}); + var $nameSpan = $('', {'class': 'name'}).text(name).appendTo($rightCol); + //var proxy = Cryptpad.getProxy(); + //var isMe = data.curvePublic === proxy.curvePublic; + + /*if (Cryptpad.isLoggedIn() && data.curvePublic) { + if (isMe) { + $span.attr('title', Messages._getKey('userlist_thisIsYou', [ + name + ])); + $nameSpan.text(name); + } else if (!proxy.friends || !proxy.friends[data.curvePublic]) { + if (pendingFriends.indexOf(data.netfluxId) !== -1) { + $('', {'class': 'friend'}).text(Messages.userlist_pending) + .appendTo($rightCol); + } else { + $('', { + 'class': 'fa fa-user-plus friend', + 'title': Messages._getKey('userlist_addAsFriendTitle', [ + name + ]) + }).appendTo($rightCol).click(function (e) { + e.stopPropagation(); + Cryptpad.inviteFromUserlist(Cryptpad, data.netfluxId); + }); + } + } + }*/ + if (data.profile) { + $span.addClass('clickable'); + $span.click(function () { + window.open('/profile/#' + data.profile); + }); + } + if (data.avatar && avatars[data.avatar]) { + $span.append(avatars[data.avatar]); + $span.append($rightCol); + } else { + Cryptpad.displayAvatar($span, data.avatar, name, function ($img) { + if (data.avatar && $img) { + avatars[data.avatar] = $img[0].outerHTML; + } + $span.append($rightCol); + }); + } + $span.data('uid', data.uid); + $editUsersList.append($span); + }); + $editUsers.append($editUsersList); + + // Viewers + if (numberOfViewUsers > 0) { + var viewText = '
'; + var viewerText = numberOfViewUsers !== 1 ? Messages.viewers : Messages.viewer; + viewText += numberOfViewUsers + ' ' + viewerText + '
'; + $editUsers.append(viewText); + } + + // Update the buttons + var fa_editusers = ''; + var fa_viewusers = ''; + var $spansmall = $('').html(fa_editusers + ' ' + numberOfEditUsers + '   ' + fa_viewusers + ' ' + numberOfViewUsers); + $userButtons.find('.buttonTitle').html('').append($spansmall); + + updateDisplayName(toolbar, config); + }; + + var initUserList = function (toolbar, config) { + // TODO clean comments + if (config.userList) { /* && config.userList.list && config.userList.userNetfluxId) {*/ + //var userList = config.userList.list; + //userList.change.push + var metadataMgr = config.userList; + metadataMgr.onChange(function () { + var users = metadataMgr.getUserlist(); + if (users.indexOf(metadataMgr.getNetfluxId()) !== -1) {toolbar.connected = true;} + if (!toolbar.connected) { return; } + //if (config.userList.data) { + updateUserList(toolbar, config); + //} + }); + } + }; + + + // Create sub-elements + + var createUserList = function (toolbar, config) { + if (!config.userList) { /* || !config.userList.list || + !config.userList.data || !config.userList.userNetfluxId) {*/ + throw new Error("You must provide a `userList` object to display the userlist"); + } + var $content = $('
', {'class': 'userlist-drawer'}); + $content.on('drop dragover', function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + var $closeIcon = $('', {"class": "fa fa-window-close close"}).appendTo($content); + $('

').text(Messages.users).appendTo($content); + $('

', {'class': USERLIST_CLS}).appendTo($content); + + toolbar.userlistContent = $content; + + var $container = $('', {id: 'userButtons', title: Messages.userListButton}); + + var $button = $('