From 65dfd99171e0f449a4c574fa6a9f0d43aae30bd6 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Mon, 7 Aug 2017 16:27:57 +0200 Subject: [PATCH] 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