major wip
parent
548bce479a
commit
65dfd99171
@ -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;
|
||||
});
|
@ -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');
|
@ -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')]);
|
||||
});
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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;
|
||||
});
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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;
|
||||
});
|
@ -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;
|
||||
});
|
@ -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()));
|
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="cp pad">
|
||||
<head>
|
||||
<title>CryptPad</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<script async data-bootload="/pad2/outer.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
#sbox-iframe {
|
||||
position:fixed;
|
||||
top:0px;
|
||||
left:0px;
|
||||
bottom:0px;
|
||||
right:0px;
|
||||
width:100%;
|
||||
height:100%;
|
||||
border:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe id="sbox-iframe"></iframe><script src="/common/sframe-noscriptfix.js"></script>
|
||||
|
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
|
||||
<script async data-bootload="/pad2/main.js" data-main="/common/sframe-boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script src="/bower_components/ckeditor/ckeditor.js"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
}
|
||||
#cke_1_top {
|
||||
overflow: visible;
|
||||
padding: 0px;
|
||||
display: flex;
|
||||
}
|
||||
#cke_1_toolbox {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
background-color: #c1e7ff;
|
||||
}
|
||||
#cke_1_toolbox .cke_toolbar {
|
||||
height: 28px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
#cke_1_top .cryptpad-toolbar {
|
||||
padding: 0;
|
||||
display: block;
|
||||
}
|
||||
.cke_wysiwyg_frame {
|
||||
min-width: 60%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app-pad">
|
||||
<textarea style="display:none" id="editor1" name="editor1"></textarea>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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') + '}');
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
@ -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 ('<!DOCTYPE html>\n' + '<html>\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 <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', 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();
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue