Replace the Netflux old client (netflux.js) by the Netflux2 client.

Move the WebRTC peer-to-peer use case in /padrtc, which still uses the old
Netflux client
Use es6-promises.min.js to solve a issue with some browser and the new
Netflux client
pull/1/head
Yann Flory 9 years ago
parent cf9f60bd57
commit 0b3d6e15b8

@ -101,9 +101,8 @@ const handleMessage = function (ctx, user, msg) {
if (cmd === 'MSG') { if (cmd === 'MSG') {
if (obj === HISTORY_KEEPER_ID) { if (obj === HISTORY_KEEPER_ID) {
let parsed; let parsed;
try { parsed = JSON.parse(json[2]); } catch (err) { return; } try { parsed = JSON.parse(json[2]); } catch (err) { console.error(err); return; }
if (parsed[0] === 'GET_HISTORY') { if (parsed[0] === 'GET_HISTORY') {
console.log('getHistory ' + parsed[1]);
getHistory(ctx, parsed[1], function (msg) { getHistory(ctx, parsed[1], function (msg) {
sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)]); sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)]);
}); });
@ -168,6 +167,7 @@ let run = module.exports.run = function (storage, socketServer) {
}); });
}, 5000); }, 5000);
socketServer.on('connection', function(socket) { socketServer.on('connection', function(socket) {
if(socket.upgradeReq.url !== '/cryptpad_websocket') { return; }
let conn = socket.upgradeReq.connection; let conn = socket.upgradeReq.connection;
let user = { let user = {
addr: conn.remoteAddress + '|' + conn.remotePort, addr: conn.remoteAddress + '|' + conn.remotePort,

@ -10,6 +10,7 @@ var run = module.exports.run = function(server) {
socket.on('message', (data) => { socket.on('message', (data) => {
try { try {
let msg = JSON.parse(data) let msg = JSON.parse(data)
console.log(msg)
if (msg.hasOwnProperty('key')) { if (msg.hasOwnProperty('key')) {
for (let master of server.clients) { for (let master of server.clients) {
if (master.key === msg.key) { if (master.key === msg.key) {

@ -127,7 +127,7 @@
document.getElementById('buttons').setAttribute('style', ''); document.getElementById('buttons').setAttribute('style', '');
document.getElementById('create-pad').setAttribute('href', '/pad/'); document.getElementById('create-pad').setAttribute('href', '/pad/');
if(Config.webrtcURL !== '') { if(Config.webrtcURL !== '') {
document.getElementById('create-rtcpad').setAttribute('href', '/pad/?webrtc=1'); document.getElementById('create-rtcpad').setAttribute('href', '/padrtc/');
} }
document.getElementById('create-sheet').setAttribute('href', '/sheet/#' + Crypto.genKey()); document.getElementById('create-sheet').setAttribute('href', '/sheet/#' + Crypto.genKey());
document.getElementById('create-code').setAttribute('href', '/code/#' + Crypto.genKey()); document.getElementById('create-code').setAttribute('href', '/code/#' + Crypto.genKey());

File diff suppressed because one or more lines are too long

@ -0,0 +1,225 @@
/*global: WebSocket */
define(() => {
'use strict';
const MAX_LAG_BEFORE_PING = 15000;
const MAX_LAG_BEFORE_DISCONNECT = 30000;
const PING_CYCLE = 5000;
const REQUEST_TIMEOUT = 5000;
const now = () => new Date().getTime();
const networkSendTo = (ctx, peerId, content) => {
const seq = ctx.seq++;
ctx.ws.send(JSON.stringify([seq, 'MSG', peerId, content]));
return new Promise((res, rej) => {
ctx.requests[seq] = { reject: rej, resolve: res, time: now() };
});
};
const channelBcast = (ctx, chanId, content) => {
const chan = ctx.channels[chanId];
if (!chan) { throw new Error("no such channel " + chanId); }
const seq = ctx.seq++;
ctx.ws.send(JSON.stringify([seq, 'MSG', chanId, content]));
return new Promise((res, rej) => {
ctx.requests[seq] = { reject: rej, resolve: res, time: now() };
});
};
const channelLeave = (ctx, chanId, reason) => {
const chan = ctx.channels[chanId];
if (!chan) { throw new Error("no such channel " + chanId); }
delete ctx.channels[chanId];
ctx.ws.send(JSON.stringify([ctx.seq++, 'LEAVE', chanId, reason]));
};
const makeEventHandlers = (ctx, mappings) => {
return (name, handler) => {
const handlers = mappings[name];
if (!handlers) { throw new Error("no such event " + name); }
handlers.push(handler);
};
};
const mkChannel = (ctx, id) => {
const internal = {
onMessage: [],
onJoin: [],
onLeave: [],
members: [],
jSeq: ctx.seq++
};
const chan = {
_: internal,
id: id,
members: internal.members,
bcast: (msg) => channelBcast(ctx, chan.id, msg),
leave: (reason) => channelLeave(ctx, chan.id, reason),
on: makeEventHandlers(ctx, { message:
internal.onMessage, join: internal.onJoin, leave: internal.onLeave })
};
ctx.requests[internal.jSeq] = chan;
ctx.ws.send(JSON.stringify([internal.jSeq, 'JOIN', id]));
return new Promise((res, rej) => {
chan._.resolve = res;
chan._.reject = rej;
})
};
const mkNetwork = (ctx) => {
const network = {
webChannels: ctx.channels,
getLag: () => (ctx.lag),
sendto: (peerId, content) => (networkSendTo(ctx, peerId, content)),
join: (chanId) => (mkChannel(ctx, chanId)),
on: makeEventHandlers(ctx, { message: ctx.onMessage, disconnect: ctx.onDisconnect })
};
network.__defineGetter__("webChannels", () => {
return Object.keys(ctx.channels).map((k) => (ctx.channels[k]));
});
return network;
};
const onMessage = (ctx, evt) => {
let msg;
try { msg = JSON.parse(evt.data); } catch (e) { console.log(e.stack); return; }
if (msg[0] !== 0) {
const req = ctx.requests[msg[0]];
if (!req) {
console.log("error: " + JSON.stringify(msg));
return;
}
delete ctx.requests[msg[0]];
if (msg[1] === 'ACK') {
if (req.ping) { // ACK of a PING
ctx.lag = now() - Number(req.ping);
return;
}
req.resolve();
} else if (msg[1] === 'JACK') {
if (req._) {
// Channel join request...
if (!msg[2]) { throw new Error("wrong type of ACK for channel join"); }
req.id = msg[2];
ctx.channels[req.id] = req;
return;
}
req.resolve();
} else if (msg[1] === 'ERROR') {
req.reject({ type: msg[2], message: msg[3] });
} else {
req.reject({ type: 'UNKNOWN', message: JSON.stringify(msg) });
}
return;
}
if (msg[2] === 'IDENT') {
ctx.uid = msg[3];
setInterval(() => {
if (now() - ctx.timeOfLastMessage < MAX_LAG_BEFORE_PING) { return; }
let seq = ctx.seq++;
let currentDate = now();
ctx.requests[seq] = {time: now(), ping: currentDate};
ctx.ws.send(JSON.stringify([seq, 'PING', currentDate]));
if (now() - ctx.timeOfLastMessage > MAX_LAG_BEFORE_DISCONNECT) {
ctx.ws.close();
}
}, PING_CYCLE);
return;
} else if (!ctx.uid) {
// extranious message, waiting for an ident.
return;
}
if (msg[2] === 'PING') {
msg[1] = 'PONG';
ctx.ws.send(JSON.stringify(msg));
return;
}
if (msg[2] === 'MSG') {
let handlers;
if (msg[3] === ctx.uid) {
handlers = ctx.onMessage;
} else {
const chan = ctx.channels[msg[3]];
if (!chan) {
console.log("message to non-existant chan " + JSON.stringify(msg));
return;
}
handlers = chan._.onMessage;
}
handlers.forEach((h) => {
try { h(msg[4], msg[1]); } catch (e) { console.log(e.stack); }
});
}
if (msg[2] === 'LEAVE') {
const chan = ctx.channels[msg[3]];
if (!chan) {
console.log("leaving non-existant chan " + JSON.stringify(msg));
return;
}
chan._.onLeave.forEach((h) => {
try { h(msg[1], msg[4]); } catch (e) { console.log(e.stack); }
});
}
if (msg[2] === 'JOIN') {
const chan = ctx.channels[msg[3]];
if (!chan) {
console.log("ERROR: join to non-existant chan " + JSON.stringify(msg));
return;
}
// have we yet fully joined the chan?
const synced = (chan._.members.indexOf(ctx.uid) !== -1);
chan._.members.push(msg[1]);
if (!synced && msg[1] === ctx.uid) {
// sync the channel join event
chan.myID = ctx.uid;
chan._.resolve(chan);
}
if (synced) {
chan._.onJoin.forEach((h) => {
try { h(msg[1]); } catch (e) { console.log(e.stack); }
});
}
}
};
const connect = (websocketURL) => {
let ctx = {
ws: new WebSocket(websocketURL),
seq: 1,
lag: 0,
uid: null,
network: null,
channels: {},
onMessage: [],
onDisconnect: [],
requests: {}
};
setInterval(() => {
for (let id in ctx.requests) {
const req = ctx.requests[id];
if (now() - req.time > REQUEST_TIMEOUT) {
delete ctx.requests[id];
req.reject({ type: 'TIMEOUT', message: 'waited ' + now() - req.time + 'ms' });
}
}
}, 5000);
ctx.network = mkNetwork(ctx);
ctx.ws.onmessage = (msg) => (onMessage(ctx, msg));
ctx.ws.onclose = (evt) => {
ctx.onDisconnect.forEach((h) => {
try { h(evt.reason); } catch (e) { console.log(e.stack); }
});
};
return new Promise((resolve, reject) => {
ctx.ws.onopen = () => resolve(ctx.network);
});
};
return { connect: connect };
});

@ -1342,6 +1342,7 @@ return /******/ (function(modules) { // webpackBootstrap
if (msg[0] !== 0 && msg[1] !== 'ACK') { if (msg[0] !== 0 && msg[1] !== 'ACK') {
return; return;
} }
if (msg[2] === 'IDENT' && msg[1] === '') { if (msg[2] === 'IDENT' && msg[1] === '') {
socket.uid = msg[3]; socket.uid = msg[3];
webChannel.myID = msg[3]; webChannel.myID = msg[3];
@ -1401,7 +1402,7 @@ return /******/ (function(modules) { // webpackBootstrap
// Trigger onJoining() when another user is joining the channel // Trigger onJoining() when another user is joining the channel
// Register the user in the list of peers in the channel // Register the user in the list of peers in the channel
if (webChannel.peers.length === 0 && msg[1].length === 16) { if (webChannel.peers.length === 0 && msg[1].length === 16) {
// We've just catched the history keeper // We've just catched the history keeper (16 characters length name)
history_keeper = msg[1]; history_keeper = msg[1];
webChannel.hc = history_keeper; webChannel.hc = history_keeper;
} }

@ -17,10 +17,11 @@
window.Reflect = { has: (x,y) => { return (y in x); } }; window.Reflect = { has: (x,y) => { return (y in x); } };
define([ define([
'/common/messages.js', '/common/messages.js',
'/common/netflux.js', '/common/netflux-client.js',
'/common/crypto.js', '/common/crypto.js',
'/common/toolbar.js', '/common/toolbar.js',
'/_socket/text-patcher.js', '/_socket/text-patcher.js',
'/common/es6-promise.min.js',
'/common/chainpad.js', '/common/chainpad.js',
'/bower_components/jquery/dist/jquery.min.js', '/bower_components/jquery/dist/jquery.min.js',
], function (Messages, Netflux, Crypto, Toolbar, TextPatcher) { ], function (Messages, Netflux, Crypto, Toolbar, TextPatcher) {
@ -75,7 +76,6 @@ define([
function (config) function (config)
{ {
var websocketUrl = config.websocketURL; var websocketUrl = config.websocketURL;
var webrtcUrl = config.webrtcURL;
var userName = config.userName; var userName = config.userName;
var channel = config.channel; var channel = config.channel;
var chanKey = config.cryptKey; var chanKey = config.cryptKey;
@ -122,25 +122,20 @@ define([
content.length + ':' + content; content.length + ':' + content;
}; };
var onPeerMessage = function(toId, type, wc) {
if(type === 6) {
messagesHistory.forEach(function(msg) {
wc.sendTo(toId, '1:y'+msg);
});
wc.sendTo(toId, '0');
}
};
var whoami = new RegExp(userName.replace(/[\/\+]/g, function (c) { var whoami = new RegExp(userName.replace(/[\/\+]/g, function (c) {
return '\\' +c; return '\\' +c;
})); }));
var onMessage = function(peer, msg, wc) { var onMessage = function(peer, msg, wc, network) {
if(msg === 0 || msg === '0') { var hc = (wc && wc.history_keeper) ? wc.history_keeper : null;
onReady(wc); if(wc && (msg === 0 || msg === '0')) {
onReady(wc, network);
return; return;
} }
else if (peer === hc){
msg = JSON.parse(msg)[4];
}
var message = chainpadAdapter.msgIn(peer, msg); var message = chainpadAdapter.msgIn(peer, msg);
verbose(message); verbose(message);
@ -176,8 +171,10 @@ define([
users: [] users: []
}; };
var onJoining = function(peer) { var onJoining = function(peer) {
if(peer.length !== 32) { return; }
var list = userList.users; var list = userList.users;
if(list.indexOf(peer) === -1) { var index = list.indexOf(peer);
if(index === -1) {
userList.users.push(peer); userList.users.push(peer);
} }
userList.onChange(); userList.onChange();
@ -216,7 +213,7 @@ define([
if(parsed.content[0] === 4) { // PING message from Chainpad if(parsed.content[0] === 4) { // PING message from Chainpad
parsed.content[0] = 5; parsed.content[0] = 5;
onMessage('', '1:y'+mkMessage(parsed.user, parsed.channelId, parsed.content)); onMessage('', '1:y'+mkMessage(parsed.user, parsed.channelId, parsed.content));
wc.sendPing(); // wc.sendPing();
return; return;
} }
return Crypto.encrypt(msg, cryptKey); return Crypto.encrypt(msg, cryptKey);
@ -227,20 +224,6 @@ define([
key: '' key: ''
}; };
var rtc = true;
if(!getParameterByName("webrtc") || !webrtcUrl) {
rtc = false;
options.signaling = websocketUrl;
options.topology = 'StarTopologyService';
options.protocol = 'WebSocketProtocolService';
options.connector = 'WebSocketService';
options.openWebChannel = true;
}
else {
options.signaling = webrtcUrl;
}
var createRealtime = function(chan) { var createRealtime = function(chan) {
return ChainPad.create(userName, return ChainPad.create(userName,
passwd, passwd,
@ -251,12 +234,12 @@ define([
}); });
}; };
var onReady = function(wc) { var onReady = function(wc, network) {
if(config.onInit) { if(config.onInit) {
config.onInit({ config.onInit({
myID: wc.myID, myID: wc.myID,
realtime: realtime, realtime: realtime,
webChannel: wc, getLag: network.getLag,
userList: userList userList: userList
}); });
} }
@ -274,18 +257,21 @@ define([
} }
} }
var onOpen = function(wc) { var onOpen = function(wc, network) {
channel = wc.id; channel = wc.id;
window.location.hash = channel + '|' + chanKey; window.location.hash = channel + '|' + chanKey;
// Add the existing peers in the userList
wc.members.forEach(onJoining);
// Add the handlers to the WebChannel // Add the handlers to the WebChannel
wc.onmessage = function(peer, msg) { // On receiving message wc.on('message', function (msg, sender) { //Channel msg
onMessage(peer, msg, wc); onMessage(sender, msg, wc, network);
}; });
wc.onJoining = onJoining; // On user joining the session wc.on('join', onJoining);
wc.onLeaving = onLeaving; // On user leaving the session wc.on('leave', onLeaving);
wc.onPeerMessage = function(peerId, type) {
onPeerMessage(peerId, type, wc);
};
if(config.setMyID) { if(config.setMyID) {
config.setMyID({ config.setMyID({
myID: wc.myID myID: wc.myID
@ -299,7 +285,7 @@ define([
// Filter messages sent by Chainpad to make it compatible with Netflux // Filter messages sent by Chainpad to make it compatible with Netflux
message = chainpadAdapter.msgOut(message, wc); message = chainpadAdapter.msgOut(message, wc);
if(message) { if(message) {
wc.send(message).then(function() { wc.bcast(message).then(function() {
// Send the message back to Chainpad once it is sent to the recipients. // Send the message back to Chainpad once it is sent to the recipients.
onMessage(wc.myID, message); onMessage(wc.myID, message);
}, function(err) { }, function(err) {
@ -311,17 +297,11 @@ define([
// Get the channel history // Get the channel history
var hc; var hc;
if(rtc) { wc.members.forEach(function (p) {
wc.channels.forEach(function (c) { if(!hc) { hc = c; } }); if (p.length === 16) { hc = p; }
if(hc) { });
wc.getHistory(hc.peerID); wc.history_keeper = hc;
} if (hc) { network.sendto(hc, JSON.stringify(['GET_HISTORY', wc.id])); }
}
else {
// TODO : Improve WebSocket service to use the latest Netflux's API
wc.peers.forEach(function (p) { if (!hc || p.linkQuality > hc.linkQuality) { hc = p; } });
hc.send(JSON.stringify(['GET_HISTORY', wc.id]));
}
toReturn.patchText = TextPatcher.create({ toReturn.patchText = TextPatcher.create({
@ -331,58 +311,30 @@ define([
realtime.start(); realtime.start();
}; };
var createRTCChannel = function () { var findChannelById = function(webChannels, channelId) {
// Check if the WebRTC channel exists and create it if necessary var webChannel;
var webchannel = Netflux.create(); webChannels.forEach(function(chan) {
webchannel.openForJoining(options).then(function(data) { if(chan.id == channelId) { webChannel = chan; return;}
onOpen(webchannel); });
onReady(webchannel); return webChannel;
}, function(error) { }
warn(error);
});
};
var joinChannel = function() { // Connect to the WebSocket channel
// Connect to the WebSocket/WebRTC channel Netflux.connect(websocketUrl).then(function(network) {
Netflux.join(channel, options).then(function(wc) { network.on('message', function (msg, sender) { // Direct message
onOpen(wc); var wchan = findChannelById(network.webChannels, channel);
}, function(error) { if(wchan) {
if(rtc && error.code === 1008) {// Unexisting RTC channel onMessage(sender, msg, wchan, network);
createRTCChannel();
} }
else { warn(error); }
}); });
}; network.join(channel || null).then(function(wc) {
joinChannel(); onOpen(wc, network);
}, function(error) {
var checkConnection = function(wc) { console.error(error);
if(wc.channels && wc.channels.size > 0) { })
var channels = Array.from(wc.channels); }, function(error) {
var channel = channels[0]; warn(error);
});
var socketChecker = setInterval(function () {
if (channel.checkSocket(realtime)) {
warn("Socket disconnected!");
recoverableErrorCount += 1;
if (recoverableErrorCount >= MAX_RECOVERABLE_ERRORS) {
warn("Giving up!");
realtime.abort();
try { channel.close(); } catch (e) { warn(e); }
if (config.onAbort) {
config.onAbort({
socket: channel
});
}
if (socketChecker) { clearInterval(socketChecker); }
}
} else {
// it's working as expected, continue
}
}, 200);
}
};
return toReturn; return toReturn;
}; };

@ -132,7 +132,7 @@ define([
userList.forEach(function(user) { userList.forEach(function(user) {
if(user !== myUserName) { if(user !== myUserName) {
var data = (userData) ? (userData[user] || null) : null; var data = (userData) ? (userData[user] || null) : null;
var userName = (data) ? data.name : null; var userName = (data) ? data.name : user;
if(userName) { if(userName) {
if(i === 0) list = ' : '; if(i === 0) list = ' : ';
list += userName + ', '; list += userName + ', ';
@ -170,9 +170,9 @@ define([
return $container.find('#'+id)[0]; return $container.find('#'+id)[0];
}; };
var checkLag = function (webChannel, lagElement) { var checkLag = function (getLag, lagElement) {
if(typeof webChannel.getLag !== "function") { return; } if(typeof getLag !== "function") { return; }
var lag = webChannel.getLag(); var lag = getLag();
var lagMsg = Messages.lag + ' '; var lagMsg = Messages.lag + ' ';
if(lag) { if(lag) {
var lagSec = lag/1000; var lagSec = lag/1000;
@ -214,7 +214,7 @@ define([
localStorage['CryptPad_RECENTPADS'] = JSON.stringify(out); localStorage['CryptPad_RECENTPADS'] = JSON.stringify(out);
}; };
var create = function ($container, myUserName, realtime, webChannel, userList, config) { var create = function ($container, myUserName, realtime, getLag, userList, config) {
var toolbar = createRealtimeToolbar($container); var toolbar = createRealtimeToolbar($container);
createEscape(toolbar.find('.rtwysiwyg-toolbar-leftside')); createEscape(toolbar.find('.rtwysiwyg-toolbar-leftside'));
var userListElement = createUserList(toolbar.find('.rtwysiwyg-toolbar-leftside')); var userListElement = createUserList(toolbar.find('.rtwysiwyg-toolbar-leftside'));
@ -223,7 +223,7 @@ define([
var userData = config.userData; var userData = config.userData;
var changeNameID = config.changeNameID; var changeNameID = config.changeNameID;
// Check if the suer is allowed to change his name // Check if the user is allowed to change his name
if(changeNameID) { if(changeNameID) {
// Create the button and update the element containing the user list // Create the button and update the element containing the user list
userListElement = createChangeName($container, userListElement, changeNameID); userListElement = createChangeName($container, userListElement, changeNameID);
@ -253,7 +253,7 @@ define([
setInterval(function () { setInterval(function () {
if (!connected) { return; } if (!connected) { return; }
checkLag(webChannel, lagElement); checkLag(getLag, lagElement);
}, 3000); }, 3000);
return { return {

@ -91,53 +91,25 @@ define([
var diffOptions = { var diffOptions = {
preDiffApply: function (info) { preDiffApply: function (info) {
/* DiffDOM will filter out magicline plugin elements /* Don't remove local instances of the magicline plugin */
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' && if (info.node && info.node.tagName === 'SPAN' &&
info.node.contentEditable === false) { info.node.getAttribute('contentEditable') === 'false') {
// it seems to be a magicline plugin element... return true;
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; } 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); var frame = info.frame = cursor.inNode(info.node);
if (!frame) { return; } if (!frame) { return; }
if (typeof info.diff.oldValue === 'string' &&
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') { typeof info.diff.newValue === 'string') {
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue); var pushes = cursor.pushDelta(info.diff.oldValue,
info.diff.newValue);
if (frame & 1) { if (frame & 1) {
// push cursor start if necessary
if (pushes.commonStart < cursor.Range.start.offset) { if (pushes.commonStart < cursor.Range.start.offset) {
cursor.Range.start.offset += pushes.delta; cursor.Range.start.offset += pushes.delta;
} }
} }
if (frame & 2) { if (frame & 2) {
// push cursor end if necessary
if (pushes.commonStart < cursor.Range.end.offset) { if (pushes.commonStart < cursor.Range.end.offset) {
cursor.Range.end.offset += pushes.delta; cursor.Range.end.offset += pushes.delta;
} }
@ -149,7 +121,7 @@ define([
if (info.node) { if (info.node) {
if (info.frame & 1) { cursor.fixStart(info.node); } if (info.frame & 1) { cursor.fixStart(info.node); }
if (info.frame & 2) { cursor.fixEnd(info.node); } if (info.frame & 2) { cursor.fixEnd(info.node); }
} else { console.error("info.node did not exist"); } } else { console.log("info.node did not exist"); }
var sel = cursor.makeSelection(); var sel = cursor.makeSelection();
var range = cursor.makeRange(); var range = cursor.makeRange();
@ -220,9 +192,8 @@ define([
// provide initialstate... // provide initialstate...
initialState: JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)), initialState: JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)),
// the websocket URL (deprecated?) // the websocket URL
websocketURL: Config.websocketURL, websocketURL: Config.websocketURL,
webrtcURL: Config.webrtcURL,
// our username // our username
userName: userName, userName: userName,
@ -286,7 +257,7 @@ define([
userData: userList, userData: userList,
changeNameID: 'cryptpad-changeName' changeNameID: 'cryptpad-changeName'
}; };
toolbar = info.realtime.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.webChannel, info.userList, config); toolbar = info.realtime.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, info.userList, config);
createChangeName('cryptpad-changeName', $bar); createChangeName('cryptpad-changeName', $bar);
/* TODO handle disconnects and such*/ /* TODO handle disconnects and such*/
}; };
@ -357,14 +328,6 @@ define([
inner.addEventListener('keydown', cursor.brFix); inner.addEventListener('keydown', cursor.brFix);
editor.on('change', propogate); editor.on('change', propogate);
// editor.on('change', function () {
// var hjson = Convert.core.hyperjson.fromDOM(inner);
// if(myData !== {}) {
// hjson[hjson.length] = {metadata: userList};
// }
// $textarea.val(JSON.stringify(hjson));
// rti.bumpSharejs();
// });
}); });
}; };

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script data-main="main" src="/bower_components/requirejs/require.js"></script>
<style>
html, body {
margin: 0px;
padding: 0px;
}
#pad-iframe {
position:fixed;
top:0px;
left:0px;
bottom:0px;
right:0px;
width:100%;
height:100%;
border:none;
margin:0;
padding:0;
overflow:hidden;
}
#feedback {
display: none;
position: fixed;
top: 0px;
right: 0px;
border: 0px;
height: 100vh;
width: 30vw;
background-color: #222;
color: #ccc;
}
#debug {
height: 20px;
position: absolute;
right: 0px;
top: 70px;
}
#debug button {
visibility: hidden;
}
#debug:hover button {
visibility: visible;
}
</style>
</head>
<body>
<iframe id="pad-iframe" src="inner.html"></iframe>
<div id="debug"><button>DEBUG</button></div>
<textarea id="feedback"></textarea>
<script>
require(['/bower_components/jquery/dist/jquery.min.js'], function() {
var $ = window.$;
$('#debug').on('click', function() {
if($('#feedback').is(':visible')) {
$('#pad-iframe').css({
'width' : '100%'
});
$('#debug').css({
'right' : '0%'
});
}
else {
$('#pad-iframe').css({
'width' : '70%'
});
$('#debug').css({
'right' : '30%'
});
}
$('#feedback').toggle();
});
});
</script>
</body>
</html>

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
<script src="/bower_components/ckeditor/ckeditor.js"></script>
</head>
<body>
<textarea style="display:none" id="editor1" name="editor1"></textarea>
</body>
</html>

@ -0,0 +1,356 @@
define([
'/api/config?cb=' + Math.random().toString(16).substring(2),
'/common/messages.js',
'/common/crypto.js',
'/padrtc/realtime-input.js',
'/common/hyperjson.js',
'/common/hyperscript.js',
'/common/toolbar.js',
'/common/cursor.js',
'/common/json-ot.js',
'/bower_components/diff-dom/diffDOM.js',
'/bower_components/jquery/dist/jquery.min.js',
'/customize/pad.js'
], function (Config, Messages, Crypto, realtimeInput, Hyperjson, Hyperscript, Toolbar, Cursor, JsonOT) {
var $ = window.jQuery;
var ifrw = $('#pad-iframe')[0].contentWindow;
var Ckeditor; // to be initialized later...
var DiffDom = window.diffDOM;
window.Toolbar = Toolbar;
window.Hyperjson = Hyperjson;
var hjsonToDom = function (H) {
return Hyperjson.callOn(H, Hyperscript);
};
var module = window.REALTIME_MODULE = {
localChangeInProgress: 0
};
var userName = Crypto.rand64(8),
toolbar;
var isNotMagicLine = function (el) {
// factor as:
// return !(el.tagName === 'SPAN' && el.contentEditable === 'false');
var filter = (el.tagName === 'SPAN' && el.contentEditable === 'false');
if (filter) {
console.log("[hyperjson.serializer] prevented an element" +
"from being serialized:", el);
return false;
}
return true;
};
var andThen = function (Ckeditor) {
// $(window).on('hashchange', function() {
// window.location.reload();
// });
var key;
var channel = '';
if (window.location.href.indexOf('#') === -1) {
key = Crypto.genKey();
// window.location.href = window.location.href + '#' + Crypto.genKey();
// return;
}
else {
var hash = window.location.hash.substring(1);
var sep = hash.indexOf('|');
channel = hash.substr(0,sep);
key = hash.substr(sep+1);
}
var fixThings = false;
// var key = Crypto.parseKey(window.location.hash.substring(1));
var editor = window.editor = Ckeditor.replace('editor1', {
// https://dev.ckeditor.com/ticket/10907
needsBrFiller: fixThings,
needsNbspFiller: fixThings,
removeButtons: 'Source,Maximize',
// magicline plugin inserts html crap into the document which is not part of the
// document itself and causes problems when it's sent across the wire and reflected back
removePlugins: 'resize'
});
editor.on('instanceReady', function (Ckeditor) {
editor.execCommand('maximize');
var documentBody = ifrw.$('iframe')[0].contentDocument.body;
documentBody.innerHTML = Messages.initialState;
var inner = window.inner = documentBody;
var cursor = window.cursor = Cursor(inner);
var setEditable = function (bool) {
inner.setAttribute('contenteditable',
(typeof (bool) !== 'undefined'? bool : true));
};
// don't let the user edit until the pad is ready
setEditable(false);
var diffOptions = {
preDiffApply: function (info) {
/* Don't remove local instances of the magicline plugin */
if (info.node && info.node.tagName === 'SPAN' &&
info.node.getAttribute('contentEditable') === 'false') {
return true;
}
if (!cursor.exists()) { return; }
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) {
if (pushes.commonStart < cursor.Range.start.offset) {
cursor.Range.start.offset += pushes.delta;
}
}
if (frame & 2) {
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.log("info.node did not exist"); }
var sel = cursor.makeSelection();
var range = cursor.makeRange();
cursor.fixSelection(sel, range);
}
}
};
var now = function () { return new Date().getTime(); };
var initializing = true;
var userList = {}; // List of pretty name of all users (mapped with their server ID)
var toolbarList; // List of users still connected to the channel (server IDs)
var addToUserList = function(data) {
for (var attrname in data) { userList[attrname] = data[attrname]; }
if(toolbarList && typeof toolbarList.onChange === "function") {
toolbarList.onChange(userList);
}
};
var myData = {};
var myUserName = ''; // My "pretty name"
var myID; // My server ID
var setMyID = function(info) {
myID = info.myID || null;
myUserName = myID;
};
var createChangeName = function(id, $container) {
var buttonElmt = $container.find('#'+id)[0];
buttonElmt.addEventListener("click", function() {
var newName = prompt("Change your name :", myUserName)
if (newName && newName.trim()) {
var myUserNameTemp = newName.trim();
if(newName.trim().length > 32) {
myUserNameTemp = myUserNameTemp.substr(0, 32);
}
myUserName = myUserNameTemp;
myData[myID] = {
name: myUserName
};
addToUserList(myData);
editor.fire( 'change' );
}
});
};
var DD = new DiffDom(diffOptions);
// apply patches, and try not to lose the cursor in the process!
var applyHjson = function (shjson) {
// var hjson = JSON.parse(shjson);
// var peerUserList = hjson[hjson.length-1];
// if(peerUserList.metadata) {
// var userData = peerUserList.metadata;
// addToUserList(userData);
// delete hjson[hjson.length-1];
// }
var userDocStateDom = hjsonToDom(JSON.parse(shjson));
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
var patch = (DD).diff(inner, userDocStateDom);
(DD).apply(inner, patch);
};
var realtimeOptions = {
// provide initialstate...
initialState: JSON.stringify(Hyperjson.fromDOM(inner, isNotMagicLine)),
// the websocket URL (deprecated?)
websocketURL: Config.websocketURL,
webrtcURL: Config.webrtcURL,
// our username
userName: userName,
// the channel we will communicate over
channel: channel,
// our encryption key
cryptKey: key,
// configuration :D
doc: inner,
setMyID: setMyID,
// really basic operational transform
transformFunction : JsonOT.validate
// pass in websocket/netflux object TODO
};
var onRemote = realtimeOptions.onRemote = function (info) {
if (initializing) { return; }
var shjson = info.realtime.getUserDoc();
// remember where the cursor is
cursor.update();
// Extract the user list (metadata) from the hyperjson
var hjson = JSON.parse(shjson);
var peerUserList = hjson[hjson.length-1];
if(peerUserList.metadata) {
var userData = peerUserList.metadata;
// Update the local user data
userList = userData;
// Send the new data to the toolbar
if(toolbarList && typeof toolbarList.onChange === "function") {
toolbarList.onChange(userList);
}
hjson.pop();
}
// build a dom from HJSON, diff, and patch the editor
applyHjson(shjson);
// Build a new stringified Chainpad hyperjson without metadata to compare with the one build from the dom
shjson = JSON.stringify(hjson);
var hjson2 = Hyperjson.fromDOM(inner);
var shjson2 = JSON.stringify(hjson2);
if (shjson2 !== shjson) {
console.error("shjson2 !== shjson");
module.realtimeInput.patchText(shjson2);
}
};
var onInit = realtimeOptions.onInit = function (info) {
var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox');
toolbarList = info.userList;
var config = {
userData: userList,
changeNameID: 'cryptpad-changeName'
};
toolbar = info.realtime.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.webChannel, info.userList, config);
createChangeName('cryptpad-changeName', $bar);
/* TODO handle disconnects and such*/
};
var onReady = realtimeOptions.onReady = function (info) {
console.log("Unlocking editor");
initializing = false;
setEditable(true);
var shjson = info.realtime.getUserDoc();
applyHjson(shjson);
};
var onAbort = realtimeOptions.onAbort = function (info) {
console.log("Aborting the session!");
// stop the user from continuing to edit
setEditable(false);
// TODO inform them that the session was torn down
toolbar.failed();
};
var rti = module.realtimeInput = realtimeInput.start(realtimeOptions);
/* catch `type="_moz"` before it goes over the wire */
var brFilter = function (hj) {
if (hj[1].type === '_moz') { hj[1].type = undefined; }
return hj;
};
// $textarea.val(JSON.stringify(Convert.dom.to.hjson(inner)));
/* It's incredibly important that you assign 'rti.onLocal'
It's used inside of realtimeInput to make sure that all changes
make it into chainpad.
It's being assigned this way because it can't be passed in, and
and can't be easily returned from realtime input without making
the code less extensible.
*/
var propogate = rti.onLocal = function () {
/* if the problem were a matter of external patches being
applied while a local patch were in progress, then we would
expect to be able to check and find
'module.localChangeInProgress' with a non-zero value while
we were applying a remote change.
*/
var hjson = Hyperjson.fromDOM(inner, isNotMagicLine, brFilter);
if(Object.keys(myData).length > 0) {
hjson[hjson.length] = {metadata: userList};
}
var shjson = JSON.stringify(hjson);
if (!rti.patchText(shjson)) {
return;
}
rti.onEvent(shjson);
};
/* hitting enter makes a new line, but places the cursor inside
of the <br> instead of the <p>. This makes it such that you
cannot type until you click, which is rather unnacceptable.
If the cursor is ever inside such a <br>, 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', propogate);
// editor.on('change', function () {
// var hjson = Convert.core.hyperjson.fromDOM(inner);
// if(myData !== {}) {
// hjson[hjson.length] = {metadata: userList};
// }
// $textarea.val(JSON.stringify(hjson));
// rti.bumpSharejs();
// });
});
};
var interval = 100;
var first = function () {
Ckeditor = ifrw.CKEDITOR;
if (Ckeditor) {
andThen(Ckeditor);
} else {
console.log("Ckeditor was not defined. Trying again in %sms",interval);
setTimeout(first, interval);
}
};
$(first);
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,397 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
window.Reflect = { has: (x,y) => { return (y in x); } };
define([
'/common/messages.js',
'/padrtc/netflux.js',
'/common/crypto.js',
'/common/toolbar.js',
'/_socket/text-patcher.js',
'/common/es6-promise.min.js',
'/common/chainpad.js',
'/bower_components/jquery/dist/jquery.min.js',
], function (Messages, Netflux, Crypto, Toolbar, TextPatcher) {
var $ = window.jQuery;
var ChainPad = window.ChainPad;
var PARANOIA = true;
var module = { exports: {} };
/**
* If an error is encountered but it is recoverable, do not immediately fail
* but if it keeps firing errors over and over, do fail.
*/
var MAX_RECOVERABLE_ERRORS = 15;
var debug = function (x) { console.log(x); },
warn = function (x) { console.error(x); },
verbose = function (x) { console.log(x); };
verbose = function () {}; // comment out to enable verbose logging
// ------------------ Trapping Keyboard Events ---------------------- //
var bindEvents = function (element, events, callback, unbind) {
for (var i = 0; i < events.length; i++) {
var e = events[i];
if (element.addEventListener) {
if (unbind) {
element.removeEventListener(e, callback, false);
} else {
element.addEventListener(e, callback, false);
}
} else {
if (unbind) {
element.detachEvent('on' + e, callback);
} else {
element.attachEvent('on' + e, callback);
}
}
}
};
var getParameterByName = function (name, url) {
if (!url) { url = window.location.href; }
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) { return null; }
if (!results[2]) { return ''; }
return decodeURIComponent(results[2].replace(/\+/g, " "));
};
var start = module.exports.start =
function (config)
{
var websocketUrl = config.websocketURL;
var webrtcUrl = config.webrtcURL;
var userName = config.userName;
var channel = config.channel;
var chanKey = config.cryptKey;
var cryptKey = Crypto.parseKey(chanKey).cryptKey;
var passwd = 'y';
// make sure configuration is defined
config = config || {};
var doc = config.doc || null;
var allMessages = [];
var initializing = true;
var recoverableErrorCount = 0;
var toReturn = {};
var messagesHistory = [];
var chainpadAdapter = {};
var realtime;
// define this in case it gets called before the rest of our stuff is ready.
var onEvent = toReturn.onEvent = function (newText) { };
var parseMessage = function (msg) {
var res ={};
// two or more? use a for
['pass','user','channelId','content'].forEach(function(attr){
var len=msg.slice(0,msg.indexOf(':')),
// taking an offset lets us slice out the prop
// and saves us one string copy
o=len.length+1,
prop=res[attr]=msg.slice(o,Number(len)+o);
// slice off the property and its descriptor
msg = msg.slice(prop.length+o);
});
// content is the only attribute that's not a string
res.content=JSON.parse(res.content);
return res;
};
var mkMessage = function (user, chan, content) {
content = JSON.stringify(content);
return user.length + ':' + user +
chan.length + ':' + chan +
content.length + ':' + content;
};
var onPeerMessage = function(toId, type, wc) {
if(type === 6) {
messagesHistory.forEach(function(msg) {
wc.sendTo(toId, '1:y'+msg);
});
wc.sendTo(toId, '0');
}
};
var whoami = new RegExp(userName.replace(/[\/\+]/g, function (c) {
return '\\' +c;
}));
var onMessage = function(peer, msg, wc) {
if(msg === 0 || msg === '0') {
onReady(wc);
return;
}
var message = chainpadAdapter.msgIn(peer, msg);
verbose(message);
allMessages.push(message);
// if (!initializing) {
// if (toReturn.onLocal) {
// toReturn.onLocal();
// }
// }
realtime.message(message);
if (/\[5,/.test(message)) { verbose("pong"); }
if (!initializing) {
if (/\[2,/.test(message)) {
//verbose("Got a patch");
if (whoami.test(message)) {
//verbose("Received own message");
} else {
//verbose("Received remote message");
// obviously this is only going to get called if
if (config.onRemote) {
config.onRemote({
realtime: realtime
});
}
}
}
}
};
var userList = {
onChange : function() {},
users: []
};
var onJoining = function(peer) {
var list = userList.users;
if(list.indexOf(peer) === -1) {
userList.users.push(peer);
}
userList.onChange();
};
var onLeaving = function(peer) {
var list = userList.users;
var index = list.indexOf(peer);
if(index !== -1) {
userList.users.splice(index, 1);
}
userList.onChange();
};
chainpadAdapter = {
msgIn : function(peerId, msg) {
var parsed = parseMessage(msg);
// Remove the password from the message
var passLen = msg.substring(0,msg.indexOf(':'));
var message = msg.substring(passLen.length+1 + Number(passLen));
try {
var decryptedMsg = Crypto.decrypt(message, cryptKey);
messagesHistory.push(decryptedMsg);
return decryptedMsg;
} catch (err) {
return message;
}
},
msgOut : function(msg, wc) {
var parsed = parseMessage(msg);
if(parsed.content[0] === 0) { // We're registering : send a REGISTER_ACK to Chainpad
onMessage('', '1:y'+mkMessage('', channel, [1,0]));
return;
}
if(parsed.content[0] === 4) { // PING message from Chainpad
parsed.content[0] = 5;
onMessage('', '1:y'+mkMessage(parsed.user, parsed.channelId, parsed.content));
wc.sendPing();
return;
}
return Crypto.encrypt(msg, cryptKey);
}
};
var options = {};
var rtc = true;
if(channel.trim().length > 0) {
options.key = channel;
}
if(!webrtcUrl) {
rtc = false;
options.signaling = websocketUrl;
options.topology = 'StarTopologyService';
options.protocol = 'WebSocketProtocolService';
options.connector = 'WebSocketService';
options.openWebChannel = true;
}
else {
options.signaling = webrtcUrl;
}
var createRealtime = function(chan) {
return ChainPad.create(userName,
passwd,
channel,
config.initialState || {},
{
transformFunction: config.transformFunction
});
};
var onReady = function(wc) {
if(config.onInit) {
config.onInit({
myID: wc.myID,
realtime: realtime,
getLag: wc.getLag,
userList: userList
});
}
// Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced
onJoining(wc.myID);
// we're fully synced
initializing = false;
// execute an onReady callback if one was supplied
if (config.onReady) {
config.onReady({
realtime: realtime
});
}
}
var onOpen = function(wc) {
channel = wc.id;
window.location.hash = channel + '|' + chanKey;
// Add the handlers to the WebChannel
wc.onmessage = function(peer, msg) { // On receiving message
onMessage(peer, msg, wc);
};
wc.onJoining = onJoining; // On user joining the session
wc.onLeaving = onLeaving; // On user leaving the session
wc.onPeerMessage = function(peerId, type) {
onPeerMessage(peerId, type, wc);
};
if(config.setMyID) {
config.setMyID({
myID: wc.myID
});
}
// Open a Chainpad session
realtime = createRealtime();
// Sending a message...
realtime.onMessage(function(message) {
// Filter messages sent by Chainpad to make it compatible with Netflux
message = chainpadAdapter.msgOut(message, wc);
if(message) {
wc.send(message).then(function() {
// Send the message back to Chainpad once it is sent to the recipients.
onMessage(wc.myID, message);
}, function(err) {
// The message has not been sent, display the error.
console.error(err);
});
}
});
// Get the channel history
var hc;
if(rtc) {
wc.channels.forEach(function (c) { if(!hc) { hc = c; } });
if(hc) {
wc.getHistory(hc.peerID);
}
}
else {
// TODO : Improve WebSocket service to use the latest Netflux's API
wc.peers.forEach(function (p) { if (!hc || p.linkQuality > hc.linkQuality) { hc = p; } });
hc.send(JSON.stringify(['GET_HISTORY', wc.id]));
}
toReturn.patchText = TextPatcher.create({
realtime: realtime
});
realtime.start();
};
var createRTCChannel = function () {
// Check if the WebRTC channel exists and create it if necessary
var webchannel = Netflux.create();
webchannel.openForJoining(options).then(function(data) {
console.log(data);
webchannel.id = data.key
onOpen(webchannel);
onReady(webchannel);
}, function(error) {
warn(error);
});
};
var joinChannel = function() {
// Connect to the WebSocket/WebRTC channel
Netflux.join(channel, options).then(function(wc) {
if(channel.trim().length > 0) {
wc.id = channel
}
onOpen(wc);
}, function(error) {
if(rtc && error.code === 1008) {// Unexisting RTC channel
createRTCChannel();
}
else { warn(error); }
});
};
joinChannel();
var checkConnection = function(wc) {
if(wc.channels && wc.channels.size > 0) {
var channels = Array.from(wc.channels);
var channel = channels[0];
var socketChecker = setInterval(function () {
if (channel.checkSocket(realtime)) {
warn("Socket disconnected!");
recoverableErrorCount += 1;
if (recoverableErrorCount >= MAX_RECOVERABLE_ERRORS) {
warn("Giving up!");
realtime.abort();
try { channel.close(); } catch (e) { warn(e); }
if (config.onAbort) {
config.onAbort({
socket: channel
});
}
if (socketChecker) { clearInterval(socketChecker); }
}
} else {
// it's working as expected, continue
}
}, 200);
}
};
return toReturn;
};
return module.exports;
});
Loading…
Cancel
Save