diff --git a/server.js b/server.js
index 351f0c695..1ee8b08e5 100644
--- a/server.js
+++ b/server.js
@@ -32,6 +32,9 @@ var setHeaders = (function () {
if (typeof(config.httpHeaders) !== 'object') { return function () {}; }
const headers = clone(config.httpHeaders);
+
+ headers['Access-Control-Allow-Origin'] = "*";
+
if (config.contentSecurity && false) {
headers['Content-Security-Policy'] = clone(config.contentSecurity);
if (!/;$/.test(headers['Content-Security-Policy'])) { headers['Content-Security-Policy'] += ';' }
@@ -149,6 +152,7 @@ httpServer.listen(config.httpPort,config.httpAddress,function(){
console.log('\n[%s] server available http://%s%s', new Date().toISOString(), hostName, ps);
});
+Http.createServer(app).listen(config.httpPort+1, config.httpAddress);
var wsConfig = { server: httpServer };
diff --git a/www/common/boot2.js b/www/common/boot2.js
index 928b57d48..b24ce38af 100644
--- a/www/common/boot2.js
+++ b/www/common/boot2.js
@@ -1,24 +1,8 @@
// This is stage 1, it can be changed but you must bump the version of the project.
-define([], function () {
- // fix up locations so that relative urls work.
- require.config({
- baseUrl: window.location.pathname,
- paths: {
- // jquery declares itself as literally "jquery" so it cannot be pulled by path :(
- "jquery": "/bower_components/jquery/dist/jquery.min",
- // json.sortify same
- "json.sortify": "/bower_components/json.sortify/dist/JSON.sortify",
- //"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf",
- //"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker"
- cm: '/bower_components/codemirror'
- },
- map: {
- '*': {
- 'css': '/bower_components/require-css/css.js',
- 'less': '/common/RequireLess.js',
- }
- }
- });
+define([
+ '/common/requireconfig.js'
+], function (RequireConfig) {
+ require.config(RequireConfig);
// most of CryptPad breaks if you don't support isArray
if (!Array.isArray) {
diff --git a/www/common/requireconfig.js b/www/common/requireconfig.js
new file mode 100644
index 000000000..2571b24e5
--- /dev/null
+++ b/www/common/requireconfig.js
@@ -0,0 +1,25 @@
+define([
+ '/api/config'
+], function (ApiConfig) {
+ var out = {
+ // fix up locations so that relative urls work.
+ baseUrl: window.location.pathname,
+ paths: {
+ // jquery declares itself as literally "jquery" so it cannot be pulled by path :(
+ "jquery": "/bower_components/jquery/dist/jquery.min",
+ // json.sortify same
+ "json.sortify": "/bower_components/json.sortify/dist/JSON.sortify",
+ //"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf",
+ //"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker"
+ cm: '/bower_components/codemirror'
+ },
+ map: {
+ '*': {
+ 'css': '/bower_components/require-css/css.js',
+ 'less': '/common/RequireLess.js',
+ }
+ }
+ };
+ Object.keys(ApiConfig.requireConf).forEach(function (k) { out[k] = ApiConfig.requireConf[k]; });
+ return out;
+});
diff --git a/www/common/sframe-boot.js b/www/common/sframe-boot.js
new file mode 100644
index 000000000..127505ef1
--- /dev/null
+++ b/www/common/sframe-boot.js
@@ -0,0 +1,10 @@
+// Stage 0, this gets cached which means we can't change it. boot2-sframe.js is changable.
+// Note that this file is meant to be executed only inside of a sandbox iframe.
+window.addEventListener('message', function (msg) {
+ var data = msg.data;
+ if (data.q !== 'INIT') { return; }
+ msg.source.postMessage({ txid: data.txid, res: 'OK' }, '*');
+ if (data.requireConf) { require.config(data.requireConf); }
+ require(['/common/sframe-boot2.js'], function () { });
+});
+console.log('boot');
\ No newline at end of file
diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js
new file mode 100644
index 000000000..ac0c060e9
--- /dev/null
+++ b/www/common/sframe-boot2.js
@@ -0,0 +1,27 @@
+// This is stage 1, it can be changed but you must bump the version of the project.
+// Note: This must only be loaded from inside of a sandbox-iframe.
+define([
+ '/common/requireconfig.js'
+], function (RequireConfig) {
+ require.config(RequireConfig);
+console.log('boot2');
+ // most of CryptPad breaks if you don't support isArray
+ if (!Array.isArray) {
+ Array.isArray = function(arg) { // CRYPTPAD_SHIM
+ return Object.prototype.toString.call(arg) === '[object Array]';
+ };
+ }
+
+ var mkFakeStore = function () {
+ var fakeStorage = {
+ getItem: function (k) { return fakeStorage[k]; },
+ setItem: function (k, v) { fakeStorage[k] = v; return v; }
+ };
+ return fakeStorage;
+ };
+ window.__defineGetter__('localStorage', function () { return mkFakeStore(); });
+ window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); });
+
+
+ require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]);
+});
diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js
new file mode 100644
index 000000000..746b1d733
--- /dev/null
+++ b/www/common/sframe-chainpad-netflux-inner.js
@@ -0,0 +1,414 @@
+/*
+ * Copyright 2014 XWiki SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+define([
+ '/bower_components/netflux-websocket/netflux-client.js',
+ '/bower_components/chainpad/chainpad.dist.js',
+], function (Netflux) {
+ var ChainPad = window.ChainPad;
+ var USE_HISTORY = true;
+ var module = { exports: {} };
+
+ var verbose = function (x) { console.log(x); };
+ verbose = function () {}; // comment out to enable verbose logging
+
+ var unBencode = function (str) { return str.replace(/^\d+:/, ''); };
+
+ module.exports.start = function (config) {
+ console.log(config);
+ var websocketUrl = config.websocketURL;
+ var userName = config.userName;
+ var channel = config.channel;
+ var Crypto = config.crypto;
+ var validateKey = config.validateKey;
+ var readOnly = config.readOnly || false;
+
+ // make sure configuration is defined
+ config = config || {};
+
+ var initializing = true;
+ var toReturn = {};
+ var messagesHistory = [];
+ var chainpadAdapter = {};
+ var realtime;
+ var network = config.network;
+ var lastKnownHash;
+
+ var userList = {
+ change : [],
+ onChange : function(newData) {
+ userList.change.forEach(function (el) {
+ el(newData);
+ });
+ },
+ users: []
+ };
+
+ var onJoining = function(peer) {
+ if(peer.length !== 32) { return; }
+ var list = userList.users;
+ var index = list.indexOf(peer);
+ if(index === -1) {
+ userList.users.push(peer);
+ }
+ userList.onChange();
+ };
+
+ var onReady = function(wc, network) {
+ // Trigger onReady only if not ready yet. This is important because the history keeper sends a direct
+ // message through "network" when it is synced, and it triggers onReady for each channel joined.
+ if (!initializing) { return; }
+
+ realtime.start();
+
+ if(config.setMyID) {
+ config.setMyID({
+ myID: wc.myID
+ });
+ }
+ // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced
+ if (!readOnly) {
+ onJoining(wc.myID);
+ }
+
+ // we're fully synced
+ initializing = false;
+
+ if (config.onReady) {
+ config.onReady({
+ realtime: realtime,
+ network: network,
+ userList: userList,
+ myId: wc.myID,
+ leave: wc.leave
+ });
+ }
+ };
+
+ var onMessage = function(peer, msg, wc, network, direct) {
+ // unpack the history keeper from the webchannel
+ var hk = network.historyKeeper;
+
+ // Old server
+ if(wc && (msg === 0 || msg === '0')) {
+ onReady(wc, network);
+ return;
+ }
+ if (direct && peer !== hk) {
+ return;
+ }
+ if (direct) {
+ var parsed = JSON.parse(msg);
+ if (parsed.validateKey && parsed.channel) {
+ if (parsed.channel === wc.id && !validateKey) {
+ validateKey = parsed.validateKey;
+ }
+ // We have to return even if it is not the current channel:
+ // we don't want to continue with other channels messages here
+ return;
+ }
+ if (parsed.state && parsed.state === 1 && parsed.channel) {
+ if (parsed.channel === wc.id) {
+ onReady(wc, network);
+ }
+ // We have to return even if it is not the current channel:
+ // we don't want to continue with other channels messages here
+ return;
+ }
+ }
+ // The history keeper is different for each channel :
+ // no need to check if the message is related to the current channel
+ if (peer === hk){
+ // if the peer is the 'history keeper', extract their message
+ var parsed1 = JSON.parse(msg);
+ msg = parsed1[4];
+ // Check that this is a message for us
+ if (parsed1[3] !== wc.id) { return; }
+ }
+
+ lastKnownHash = msg.slice(0,64);
+ var message = chainpadAdapter.msgIn(peer, msg);
+
+ verbose(message);
+
+ if (!initializing) {
+ if (config.onLocal) {
+ config.onLocal();
+ }
+ }
+
+ // slice off the bencoded header
+ // Why are we getting bencoded stuff to begin with?
+ // FIXME this shouldn't be necessary
+ message = unBencode(message);//.slice(message.indexOf(':[') + 1);
+
+ // pass the message into Chainpad
+ realtime.message(message);
+ };
+
+ // update UI components to show that one of the other peers has left
+ var onLeaving = function(peer) {
+ var list = userList.users;
+ var index = list.indexOf(peer);
+ if(index !== -1) {
+ userList.users.splice(index, 1);
+ }
+ userList.onChange();
+ };
+
+ // shim between chainpad and netflux
+ chainpadAdapter = {
+ msgIn : function(peerId, msg) {
+ msg = msg.replace(/^cp\|/, '');
+ try {
+ var decryptedMsg = Crypto.decrypt(msg, validateKey);
+ messagesHistory.push(decryptedMsg);
+ return decryptedMsg;
+ } catch (err) {
+ console.error(err);
+ return msg;
+ }
+ },
+ msgOut : function(msg) {
+ if (readOnly) { return; }
+ try {
+ var cmsg = Crypto.encrypt(msg);
+ if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; }
+ return cmsg;
+ } catch (err) {
+ console.log(msg);
+ throw err;
+ }
+ }
+ };
+
+ var createRealtime = function() {
+ return ChainPad.create({
+ userName: userName,
+ initialState: config.initialState,
+ transformFunction: config.transformFunction,
+ validateContent: config.validateContent,
+ avgSyncMilliseconds: config.avgSyncMilliseconds,
+ logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1
+ });
+ };
+
+ // We use an object to store the webchannel so that we don't have to push new handlers to chainpad
+ // and remove the old ones when reconnecting and keeping the same 'realtime' object
+ // See realtime.onMessage below: we call wc.bcast(...) but wc may change
+ var wcObject = {};
+ var onOpen = function(wc, network, initialize) {
+ wcObject.wc = wc;
+ channel = wc.id;
+
+ // Add the existing peers in the userList
+ wc.members.forEach(onJoining);
+
+ // Add the handlers to the WebChannel
+ wc.on('message', function (msg, sender) { //Channel msg
+ onMessage(sender, msg, wc, network);
+ });
+ wc.on('join', onJoining);
+ wc.on('leave', onLeaving);
+
+ if (initialize) {
+ toReturn.realtime = realtime = createRealtime();
+
+ realtime._patch = realtime.patch;
+ realtime.patch = function (patch, x, y) {
+ if (initializing) {
+ console.error("attempted to change the content before chainpad was synced");
+ }
+ return realtime._patch(patch, x, y);
+ };
+ realtime._change = realtime.change;
+ realtime.change = function (offset, count, chars) {
+ if (initializing) {
+ console.error("attempted to change the content before chainpad was synced");
+ }
+ return realtime._change(offset, count, chars);
+ };
+
+ if (config.onInit) {
+ config.onInit({
+ myID: wc.myID,
+ realtime: realtime,
+ getLag: network.getLag,
+ userList: userList,
+ network: network,
+ channel: channel
+ });
+ }
+
+ // Sending a message...
+ realtime.onMessage(function(message, cb) {
+ // Filter messages sent by Chainpad to make it compatible with Netflux
+ message = chainpadAdapter.msgOut(message);
+ if(message) {
+ // Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we
+ // want to keep the same chainpad (realtime) object
+ wcObject.wc.bcast(message).then(function() {
+ cb();
+ }, function(err) {
+ // The message has not been sent, display the error.
+ console.error(err);
+ });
+ }
+ });
+
+ realtime.onPatch(function () {
+ if (config.onRemote) {
+ config.onRemote({
+ realtime: realtime
+ });
+ }
+ });
+ }
+
+ // Get the channel history
+ if(USE_HISTORY) {
+ var hk;
+
+ wc.members.forEach(function (p) {
+ if (p.length === 16) { hk = p; }
+ });
+ network.historyKeeper = hk;
+
+ var msg = ['GET_HISTORY', wc.id];
+ // Add the validateKey if we are the channel creator and we have a validateKey
+ msg.push(validateKey);
+ msg.push(lastKnownHash);
+ if (hk) { network.sendto(hk, JSON.stringify(msg)); }
+ }
+ else {
+ onReady(wc, network);
+ }
+ };
+
+ // Set a flag to avoid calling onAbort or onConnectionChange when the user is leaving the page
+ var isIntentionallyLeaving = false;
+ window.addEventListener("beforeunload", function () {
+ isIntentionallyLeaving = true;
+ });
+
+ var findChannelById = function(webChannels, channelId) {
+ var webChannel;
+
+ // Array.some terminates once a truthy value is returned
+ // best case is faster than forEach, though webchannel arrays seem
+ // to consistently have a length of 1
+ webChannels.some(function(chan) {
+ if(chan.id === channelId) { webChannel = chan; return true;}
+ });
+ return webChannel;
+ };
+
+ var onConnectError = function (err) {
+ if (config.onError) {
+ config.onError({
+ error: err.type
+ });
+ }
+ };
+
+ var joinSession = function (endPoint, cb) {
+ // a websocket URL has been provided
+ // connect to it with Netflux.
+ if (typeof(endPoint) === 'string') {
+ Netflux.connect(endPoint).then(cb, onConnectError);
+ } else if (typeof(endPoint.then) === 'function') {
+ // a netflux network promise was provided
+ // connect to it and use a channel
+ endPoint.then(cb, onConnectError);
+ } else {
+ // assume it's a network and try to connect.
+ cb(endPoint);
+ }
+ };
+
+ var firstConnection = true;
+ /* Connect to the Netflux network, or fall back to a WebSocket
+ in theory this lets us connect to more netflux channels using only
+ one network. */
+ var connectTo = function (network) {
+ // join the netflux network, promise to handle opening of the channel
+ network.join(channel || null).then(function(wc) {
+ onOpen(wc, network, firstConnection);
+ firstConnection = false;
+ }, function(error) {
+ console.error(error);
+ });
+ };
+
+ joinSession(network || websocketUrl, function (network) {
+ // pass messages that come out of netflux into our local handler
+ if (firstConnection) {
+ toReturn.network = network;
+
+ network.on('disconnect', function (reason) {
+ if (isIntentionallyLeaving) { return; }
+ if (reason === "network.disconnect() called") { return; }
+ if (config.onConnectionChange) {
+ config.onConnectionChange({
+ state: false
+ });
+ return;
+ }
+ if (config.onAbort) {
+ config.onAbort({
+ reason: reason
+ });
+ }
+ });
+
+ network.on('reconnect', function (uid) {
+ if (config.onConnectionChange) {
+ config.onConnectionChange({
+ state: true,
+ myId: uid
+ });
+ var afterReconnecting = function () {
+ initializing = true;
+ userList.users=[];
+ joinSession(network, connectTo);
+ };
+ if (config.beforeReconnecting) {
+ config.beforeReconnecting(function (newKey, newContent) {
+ channel = newKey;
+ config.initialState = newContent;
+ afterReconnecting();
+ });
+ return;
+ }
+ afterReconnecting();
+ }
+ });
+
+ network.on('message', function (msg, sender) { // Direct message
+ var wchan = findChannelById(network.webChannels, channel);
+ if(wchan) {
+ onMessage(sender, msg, wchan, network, true);
+ }
+ });
+ }
+
+ connectTo(network);
+ }, onConnectError);
+
+ return toReturn;
+ };
+ return module.exports;
+});
diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js
new file mode 100644
index 000000000..0da737c0f
--- /dev/null
+++ b/www/common/sframe-chainpad-netflux-outer.js
@@ -0,0 +1,413 @@
+/*
+ * Copyright 2014 XWiki SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+define([
+ '/bower_components/netflux-websocket/netflux-client.js',
+ '/bower_components/chainpad/chainpad.dist.js',
+], function (Netflux) {
+ var ChainPad = window.ChainPad;
+ var USE_HISTORY = true;
+ var module = { exports: {} };
+
+ var verbose = function (x) { console.log(x); };
+ verbose = function () {}; // comment out to enable verbose logging
+
+ var unBencode = function (str) { return str.replace(/^\d+:/, ''); };
+
+ module.exports.start = function (config) {
+ var websocketUrl = config.websocketURL;
+ var userName = config.userName;
+ var channel = config.channel;
+ var Crypto = config.crypto;
+ var validateKey = config.validateKey;
+ var readOnly = config.readOnly || false;
+
+ // make sure configuration is defined
+ config = config || {};
+
+ var initializing = true;
+ var toReturn = {};
+ var messagesHistory = [];
+ var chainpadAdapter = {};
+ var realtime;
+ var network = config.network;
+ var lastKnownHash;
+
+ var userList = {
+ change : [],
+ onChange : function(newData) {
+ userList.change.forEach(function (el) {
+ el(newData);
+ });
+ },
+ users: []
+ };
+
+ var onJoining = function(peer) {
+ if(peer.length !== 32) { return; }
+ var list = userList.users;
+ var index = list.indexOf(peer);
+ if(index === -1) {
+ userList.users.push(peer);
+ }
+ userList.onChange();
+ };
+
+ var onReady = function(wc, network) {
+ // Trigger onReady only if not ready yet. This is important because the history keeper sends a direct
+ // message through "network" when it is synced, and it triggers onReady for each channel joined.
+ if (!initializing) { return; }
+
+ realtime.start();
+
+ if(config.setMyID) {
+ config.setMyID({
+ myID: wc.myID
+ });
+ }
+ // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced
+ if (!readOnly) {
+ onJoining(wc.myID);
+ }
+
+ // we're fully synced
+ initializing = false;
+
+ if (config.onReady) {
+ config.onReady({
+ realtime: realtime,
+ network: network,
+ userList: userList,
+ myId: wc.myID,
+ leave: wc.leave
+ });
+ }
+ };
+
+ var onMessage = function(peer, msg, wc, network, direct) {
+ // unpack the history keeper from the webchannel
+ var hk = network.historyKeeper;
+
+ // Old server
+ if(wc && (msg === 0 || msg === '0')) {
+ onReady(wc, network);
+ return;
+ }
+ if (direct && peer !== hk) {
+ return;
+ }
+ if (direct) {
+ var parsed = JSON.parse(msg);
+ if (parsed.validateKey && parsed.channel) {
+ if (parsed.channel === wc.id && !validateKey) {
+ validateKey = parsed.validateKey;
+ }
+ // We have to return even if it is not the current channel:
+ // we don't want to continue with other channels messages here
+ return;
+ }
+ if (parsed.state && parsed.state === 1 && parsed.channel) {
+ if (parsed.channel === wc.id) {
+ onReady(wc, network);
+ }
+ // We have to return even if it is not the current channel:
+ // we don't want to continue with other channels messages here
+ return;
+ }
+ }
+ // The history keeper is different for each channel :
+ // no need to check if the message is related to the current channel
+ if (peer === hk){
+ // if the peer is the 'history keeper', extract their message
+ var parsed1 = JSON.parse(msg);
+ msg = parsed1[4];
+ // Check that this is a message for us
+ if (parsed1[3] !== wc.id) { return; }
+ }
+
+ lastKnownHash = msg.slice(0,64);
+ var message = chainpadAdapter.msgIn(peer, msg);
+
+ verbose(message);
+
+ if (!initializing) {
+ if (config.onLocal) {
+ config.onLocal();
+ }
+ }
+
+ // slice off the bencoded header
+ // Why are we getting bencoded stuff to begin with?
+ // FIXME this shouldn't be necessary
+ message = unBencode(message);//.slice(message.indexOf(':[') + 1);
+
+ // pass the message into Chainpad
+ realtime.message(message);
+ };
+
+ // update UI components to show that one of the other peers has left
+ var onLeaving = function(peer) {
+ var list = userList.users;
+ var index = list.indexOf(peer);
+ if(index !== -1) {
+ userList.users.splice(index, 1);
+ }
+ userList.onChange();
+ };
+
+ // shim between chainpad and netflux
+ chainpadAdapter = {
+ msgIn : function(peerId, msg) {
+ msg = msg.replace(/^cp\|/, '');
+ try {
+ var decryptedMsg = Crypto.decrypt(msg, validateKey);
+ messagesHistory.push(decryptedMsg);
+ return decryptedMsg;
+ } catch (err) {
+ console.error(err);
+ return msg;
+ }
+ },
+ msgOut : function(msg) {
+ if (readOnly) { return; }
+ try {
+ var cmsg = Crypto.encrypt(msg);
+ if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; }
+ return cmsg;
+ } catch (err) {
+ console.log(msg);
+ throw err;
+ }
+ }
+ };
+
+ var createRealtime = function() {
+ return ChainPad.create({
+ userName: userName,
+ initialState: config.initialState,
+ transformFunction: config.transformFunction,
+ validateContent: config.validateContent,
+ avgSyncMilliseconds: config.avgSyncMilliseconds,
+ logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1
+ });
+ };
+
+ // We use an object to store the webchannel so that we don't have to push new handlers to chainpad
+ // and remove the old ones when reconnecting and keeping the same 'realtime' object
+ // See realtime.onMessage below: we call wc.bcast(...) but wc may change
+ var wcObject = {};
+ var onOpen = function(wc, network, initialize) {
+ wcObject.wc = wc;
+ channel = wc.id;
+
+ // Add the existing peers in the userList
+ wc.members.forEach(onJoining);
+
+ // Add the handlers to the WebChannel
+ wc.on('message', function (msg, sender) { //Channel msg
+ onMessage(sender, msg, wc, network);
+ });
+ wc.on('join', onJoining);
+ wc.on('leave', onLeaving);
+
+ if (initialize) {
+ toReturn.realtime = realtime = createRealtime();
+
+ realtime._patch = realtime.patch;
+ realtime.patch = function (patch, x, y) {
+ if (initializing) {
+ console.error("attempted to change the content before chainpad was synced");
+ }
+ return realtime._patch(patch, x, y);
+ };
+ realtime._change = realtime.change;
+ realtime.change = function (offset, count, chars) {
+ if (initializing) {
+ console.error("attempted to change the content before chainpad was synced");
+ }
+ return realtime._change(offset, count, chars);
+ };
+
+ if (config.onInit) {
+ config.onInit({
+ myID: wc.myID,
+ realtime: realtime,
+ getLag: network.getLag,
+ userList: userList,
+ network: network,
+ channel: channel
+ });
+ }
+
+ // Sending a message...
+ realtime.onMessage(function(message, cb) {
+ // Filter messages sent by Chainpad to make it compatible with Netflux
+ message = chainpadAdapter.msgOut(message);
+ if(message) {
+ // Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we
+ // want to keep the same chainpad (realtime) object
+ wcObject.wc.bcast(message).then(function() {
+ cb();
+ }, function(err) {
+ // The message has not been sent, display the error.
+ console.error(err);
+ });
+ }
+ });
+
+ realtime.onPatch(function () {
+ if (config.onRemote) {
+ config.onRemote({
+ realtime: realtime
+ });
+ }
+ });
+ }
+
+ // Get the channel history
+ if(USE_HISTORY) {
+ var hk;
+
+ wc.members.forEach(function (p) {
+ if (p.length === 16) { hk = p; }
+ });
+ network.historyKeeper = hk;
+
+ var msg = ['GET_HISTORY', wc.id];
+ // Add the validateKey if we are the channel creator and we have a validateKey
+ msg.push(validateKey);
+ msg.push(lastKnownHash);
+ if (hk) { network.sendto(hk, JSON.stringify(msg)); }
+ }
+ else {
+ onReady(wc, network);
+ }
+ };
+
+ // Set a flag to avoid calling onAbort or onConnectionChange when the user is leaving the page
+ var isIntentionallyLeaving = false;
+ window.addEventListener("beforeunload", function () {
+ isIntentionallyLeaving = true;
+ });
+
+ var findChannelById = function(webChannels, channelId) {
+ var webChannel;
+
+ // Array.some terminates once a truthy value is returned
+ // best case is faster than forEach, though webchannel arrays seem
+ // to consistently have a length of 1
+ webChannels.some(function(chan) {
+ if(chan.id === channelId) { webChannel = chan; return true;}
+ });
+ return webChannel;
+ };
+
+ var onConnectError = function (err) {
+ if (config.onError) {
+ config.onError({
+ error: err.type
+ });
+ }
+ };
+
+ var joinSession = function (endPoint, cb) {
+ // a websocket URL has been provided
+ // connect to it with Netflux.
+ if (typeof(endPoint) === 'string') {
+ Netflux.connect(endPoint).then(cb, onConnectError);
+ } else if (typeof(endPoint.then) === 'function') {
+ // a netflux network promise was provided
+ // connect to it and use a channel
+ endPoint.then(cb, onConnectError);
+ } else {
+ // assume it's a network and try to connect.
+ cb(endPoint);
+ }
+ };
+
+ var firstConnection = true;
+ /* Connect to the Netflux network, or fall back to a WebSocket
+ in theory this lets us connect to more netflux channels using only
+ one network. */
+ var connectTo = function (network) {
+ // join the netflux network, promise to handle opening of the channel
+ network.join(channel || null).then(function(wc) {
+ onOpen(wc, network, firstConnection);
+ firstConnection = false;
+ }, function(error) {
+ console.error(error);
+ });
+ };
+
+ joinSession(network || websocketUrl, function (network) {
+ // pass messages that come out of netflux into our local handler
+ if (firstConnection) {
+ toReturn.network = network;
+
+ network.on('disconnect', function (reason) {
+ if (isIntentionallyLeaving) { return; }
+ if (reason === "network.disconnect() called") { return; }
+ if (config.onConnectionChange) {
+ config.onConnectionChange({
+ state: false
+ });
+ return;
+ }
+ if (config.onAbort) {
+ config.onAbort({
+ reason: reason
+ });
+ }
+ });
+
+ network.on('reconnect', function (uid) {
+ if (config.onConnectionChange) {
+ config.onConnectionChange({
+ state: true,
+ myId: uid
+ });
+ var afterReconnecting = function () {
+ initializing = true;
+ userList.users=[];
+ joinSession(network, connectTo);
+ };
+ if (config.beforeReconnecting) {
+ config.beforeReconnecting(function (newKey, newContent) {
+ channel = newKey;
+ config.initialState = newContent;
+ afterReconnecting();
+ });
+ return;
+ }
+ afterReconnecting();
+ }
+ });
+
+ network.on('message', function (msg, sender) { // Direct message
+ var wchan = findChannelById(network.webChannels, channel);
+ if(wchan) {
+ onMessage(sender, msg, wchan, network, true);
+ }
+ });
+ }
+
+ connectTo(network);
+ }, onConnectError);
+
+ return toReturn;
+ };
+ return module.exports;
+});
diff --git a/www/common/sframe-ctrl.js b/www/common/sframe-ctrl.js
new file mode 100644
index 000000000..536f2689e
--- /dev/null
+++ b/www/common/sframe-ctrl.js
@@ -0,0 +1,51 @@
+// This file provides the external API for launching the sandboxed iframe.
+define([
+ '/common/requireconfig.js'
+], function (RequireConfig) {
+ var iframe;
+ var handlers = {};
+ var queries = {};
+ var module = { exports: {} };
+
+ var mkTxid = function () {
+ return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', '');
+ };
+
+ var init = module.exports.init = function (frame, cb) {
+ if (iframe) { throw new Error('already initialized'); }
+ var txid = mkTxid();
+ var intr = setInterval(function () {
+ frame.contentWindow.postMessage({
+ txid: txid,
+ requireConf: RequireConfig,
+ q: 'INIT'
+ }, '*');
+ });
+ window.addEventListener('message', function (msg) {
+ console.log('recv');
+ console.log(msg.origin);
+ var data = msg.data;
+ if (data.txid !== txid) { return; }
+ clearInterval(intr);
+ iframe = frame;
+ cb();
+ });
+ };
+ var query = module.exports.query = function (msg, cb) {
+ if (!iframe) { throw new Error('not yet initialized'); }
+ var txid = mkTxid();
+ queries[txid] = {
+ txid: txid,
+ timeout: setTimeout(function () {
+ delete queries[txid];
+ console.log("Error")
+ })
+ };
+ };
+ var registerHandler = module.exports.registerHandler = function (queryType, handler) {
+ if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); }
+ handlers[queryType] = handler;
+ };
+
+ return module.exports;
+});
diff --git a/www/common/sframe-noscriptfix.js b/www/common/sframe-noscriptfix.js
new file mode 100644
index 000000000..f5e6fb1c0
--- /dev/null
+++ b/www/common/sframe-noscriptfix.js
@@ -0,0 +1,3 @@
+// Fix for noscript bugs when caching iframe content.
+// Caution, this file will get cached, you must change the name if you change it.
+document.getElementById('sbox-iframe').setAttribute('src', 'http://localhost:3001/pad2/inner.html?cb=' + (+new Date()));
diff --git a/www/pad2/index.html b/www/pad2/index.html
new file mode 100644
index 000000000..0a2a3b922
--- /dev/null
+++ b/www/pad2/index.html
@@ -0,0 +1,31 @@
+
+
+
+ CryptPad
+
+
+
+
+
+
+
+
+
diff --git a/www/pad2/inner.html b/www/pad2/inner.html
new file mode 100644
index 000000000..2e528c49b
--- /dev/null
+++ b/www/pad2/inner.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/www/pad2/links.js b/www/pad2/links.js
new file mode 100644
index 000000000..6a53a4bc7
--- /dev/null
+++ b/www/pad2/links.js
@@ -0,0 +1,60 @@
+define(['/common/cryptpad-common.js'], function (Cryptpad) {
+ // Adds a context menu entry to open the selected link in a new tab.
+ // See https://github.com/xwiki-contrib/application-ckeditor/commit/755d193497bf23ed874d874b4ae92fbee887fc10
+ var Messages = Cryptpad.Messages;
+ return {
+ addSupportForOpeningLinksInNewTab : function (Ckeditor) {
+ // Returns the DOM element of the active (currently focused) link. It has also support for linked image widgets.
+ // @return {CKEDITOR.dom.element}
+ var getActiveLink = function(editor) {
+ var anchor = Ckeditor.plugins.link.getSelectedLink(editor),
+ // We need to do some special checking against widgets availability.
+ activeWidget = editor.widgets && editor.widgets.focused;
+ // If default way of getting links didn't return anything useful..
+ if (!anchor && activeWidget && activeWidget.name === 'image' && activeWidget.parts.link) {
+ // Since CKEditor 4.4.0 image widgets may be linked.
+ anchor = activeWidget.parts.link;
+ }
+ return anchor;
+ };
+
+ return function(event) {
+ var editor = event.editor;
+ if (!Ckeditor.plugins.link) {
+ return;
+ }
+ editor.addCommand( 'openLink', {
+ exec: function(editor) {
+ var anchor = getActiveLink(editor);
+ if (anchor) {
+ var href = anchor.getAttribute('href');
+ if (href) {
+ window.open(href);
+ }
+ }
+ }
+ });
+ if (typeof editor.addMenuItem === 'function') {
+ editor.addMenuItem('openLink', {
+ label: Messages.openLinkInNewTab,
+ command: 'openLink',
+ group: 'link',
+ order: -1
+ });
+ }
+ if (editor.contextMenu) {
+ editor.contextMenu.addListener(function(startElement) {
+ if (startElement) {
+ var anchor = getActiveLink(editor);
+ if (anchor && anchor.getAttribute('href')) {
+ return {openLink: Ckeditor.TRISTATE_OFF};
+ }
+ }
+ });
+ editor.contextMenu._.panelDefinition.css.push('.cke_button__openLink_icon {' +
+ Ckeditor.skin.getIconStyle('link') + '}');
+ }
+ };
+ }
+ };
+});
diff --git a/www/pad2/main.js b/www/pad2/main.js
new file mode 100644
index 000000000..ce8db2010
--- /dev/null
+++ b/www/pad2/main.js
@@ -0,0 +1,758 @@
+console.log('one');
+define([
+ 'jquery',
+ '/bower_components/chainpad-crypto/crypto.js',
+ '/common/sframe-chainpad-netflux-inner.js',
+ '/bower_components/hyperjson/hyperjson.js',
+ '/common/toolbar2.js',
+ '/common/cursor.js',
+ '/bower_components/chainpad-json-validator/json-ot.js',
+ '/common/TypingTests.js',
+ 'json.sortify',
+ '/bower_components/textpatcher/TextPatcher.js',
+ '/common/cryptpad-common.js',
+ '/common/cryptget.js',
+ '/pad/links.js',
+
+ '/bower_components/file-saver/FileSaver.min.js',
+ '/bower_components/diff-dom/diffDOM.js',
+
+ 'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
+ 'less!/customize/src/less/cryptpad.less',
+ 'less!/customize/src/less/toolbar.less'
+], function ($, Crypto, realtimeInput, Hyperjson,
+ Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links) {
+ var saveAs = window.saveAs;
+ var Messages = Cryptpad.Messages;
+
+ console.log('two');
+
+ var Ckeditor; // to be initialized later...
+ var DiffDom = window.diffDOM;
+
+ var stringify = function (obj) {
+ return JSONSortify(obj);
+ };
+
+ window.Toolbar = Toolbar;
+ window.Hyperjson = Hyperjson;
+
+ var slice = function (coll) {
+ return Array.prototype.slice.call(coll);
+ };
+
+ var removeListeners = function (root) {
+ slice(root.attributes).map(function (attr) {
+ if (/^on/.test(attr.name)) {
+ root.attributes.removeNamedItem(attr.name);
+ }
+ });
+ slice(root.children).forEach(removeListeners);
+ };
+
+ var hjsonToDom = function (H) {
+ var dom = Hyperjson.toDOM(H);
+ removeListeners(dom);
+ return dom;
+ };
+
+ var module = window.REALTIME_MODULE = window.APP = {
+ Hyperjson: Hyperjson,
+ TextPatcher: TextPatcher,
+ logFights: true,
+ fights: [],
+ Cryptpad: Cryptpad,
+ Cursor: Cursor,
+ };
+
+ var emitResize = module.emitResize = function () {
+ var evt = window.document.createEvent('UIEvents');
+ evt.initUIEvent('resize', true, false, window, 0);
+ window.dispatchEvent(evt);
+ };
+
+ var toolbar;
+
+ var isNotMagicLine = function (el) {
+ return !(el && typeof(el.getAttribute) === 'function' &&
+ el.getAttribute('class') &&
+ el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1);
+ };
+
+ /* catch `type="_moz"` before it goes over the wire */
+ var brFilter = function (hj) {
+ if (hj[1].type === '_moz') { hj[1].type = undefined; }
+ return hj;
+ };
+
+ var onConnectError = function () {
+ Cryptpad.errorLoadingScreen(Messages.websocketError);
+ };
+
+ var andThen = function (Ckeditor) {
+ //var $iframe = $('#pad-iframe').contents();
+ //var secret = Cryptpad.getSecrets();
+ //var readOnly = secret.keys && !secret.keys.editKeyStr;
+ //if (!secret.keys) {
+ // secret.keys = secret.key;
+ //}
+ var readOnly = false; // TODO
+
+ var editor = window.editor = Ckeditor.replace('editor1', {
+ customConfig: '/customize/ckeditor-config.js',
+ });
+
+ editor.on('instanceReady', Links.addSupportForOpeningLinksInNewTab(Ckeditor));
+ editor.on('instanceReady', function () {
+ var $bar = $('#cke_1_toolbox');
+
+ var $html = $bar.closest('html');
+ var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]');
+ if ($faLink.length) {
+ $html.find('iframe').contents().find('head').append($faLink.clone());
+ }
+ var isHistoryMode = false;
+
+ if (readOnly) {
+ $('#cke_1_toolbox > .cke_toolbox_main').hide();
+ }
+
+ /* add a class to the magicline plugin so we can pick it out more easily */
+
+ var ml = window.CKEDITOR.instances.editor1.plugins.magicline.backdoor.that.line.$;
+
+ [ml, ml.parentElement].forEach(function (el) {
+ el.setAttribute('class', 'non-realtime');
+ });
+
+ var documentBody = document.body;
+
+ var inner = window.inner = documentBody;
+
+ // hide all content until the realtime doc is ready
+ $(inner).css({
+ color: '#fff',
+ });
+
+ var cursor = module.cursor = Cursor(inner);
+
+ var setEditable = module.setEditable = function (bool) {
+ if (bool) {
+ $(inner).css({
+ color: '#333',
+ });
+ }
+ if (!readOnly || !bool) {
+ inner.setAttribute('contenteditable', bool);
+ }
+ };
+
+ // don't let the user edit until the pad is ready
+ setEditable(false);
+
+ var forbiddenTags = [
+ 'SCRIPT',
+ 'IFRAME',
+ 'OBJECT',
+ 'APPLET',
+ 'VIDEO',
+ 'AUDIO'
+ ];
+
+ var diffOptions = {
+ preDiffApply: function (info) {
+ /*
+ Don't accept attributes that begin with 'on'
+ these are probably listeners, and we don't want to
+ send scripts over the wire.
+ */
+ if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) {
+ if (info.diff.name === 'href') {
+ // console.log(info.diff);
+ //var href = info.diff.newValue;
+
+ // TODO normalize HTML entities
+ if (/javascript *: */.test(info.diff.newValue)) {
+ // TODO remove javascript: links
+ }
+ }
+
+ if (/^on/.test(info.diff.name)) {
+ console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name);
+ return true;
+ }
+ }
+ /*
+ Also reject any elements which would insert any one of
+ our forbidden tag types: script, iframe, object,
+ applet, video, or audio
+ */
+ if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) {
+ if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) {
+ console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName);
+ return true;
+ } else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) {
+ console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName);
+ return true;
+ }
+ }
+
+ if (info.node && info.node.tagName === 'BODY') {
+ if (info.diff.action === 'removeAttribute' &&
+ ['class', 'spellcheck'].indexOf(info.diff.name) !== -1) {
+ return true;
+ }
+ }
+
+ /* DiffDOM will filter out magicline plugin elements
+ in practice this will make it impossible to use it
+ while someone else is typing, which could be annoying.
+
+ we should check when such an element is going to be
+ removed, and prevent that from happening. */
+ if (info.node && info.node.tagName === 'SPAN' &&
+ info.node.getAttribute('contentEditable') === "false") {
+ // it seems to be a magicline plugin element...
+ if (info.diff.action === 'removeElement') {
+ // and you're about to remove it...
+ // this probably isn't what you want
+
+ /*
+ I have never seen this in the console, but the
+ magic line is still getting removed on remote
+ edits. This suggests that it's getting removed
+ by something other than diffDom.
+ */
+ console.log("preventing removal of the magic line!");
+
+ // return true to prevent diff application
+ return true;
+ }
+ }
+
+ // Do not change the contenteditable value in view mode
+ if (readOnly && info.node && info.node.tagName === 'BODY' &&
+ info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') {
+ return true;
+ }
+
+ // no use trying to recover the cursor if it doesn't exist
+ if (!cursor.exists()) { return; }
+
+ /* frame is either 0, 1, 2, or 3, depending on which
+ cursor frames were affected: none, first, last, or both
+ */
+ var frame = info.frame = cursor.inNode(info.node);
+
+ if (!frame) { return; }
+
+ if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
+ var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);
+
+ if (frame & 1) {
+ // push cursor start if necessary
+ if (pushes.commonStart < cursor.Range.start.offset) {
+ cursor.Range.start.offset += pushes.delta;
+ }
+ }
+ if (frame & 2) {
+ // push cursor end if necessary
+ if (pushes.commonStart < cursor.Range.end.offset) {
+ cursor.Range.end.offset += pushes.delta;
+ }
+ }
+ }
+ },
+ postDiffApply: function (info) {
+ if (info.frame) {
+ if (info.node) {
+ if (info.frame & 1) { cursor.fixStart(info.node); }
+ if (info.frame & 2) { cursor.fixEnd(info.node); }
+ } else { console.error("info.node did not exist"); }
+
+ var sel = cursor.makeSelection();
+ var range = cursor.makeRange();
+
+ cursor.fixSelection(sel, range);
+ }
+ }
+ };
+
+ var initializing = true;
+
+ var Title;
+ var UserList;
+ var Metadata;
+
+ var getHeadingText = function () {
+ var text;
+ if (['h1', 'h2', 'h3'].some(function (t) {
+ var $header = $(inner).find(t + ':first-of-type');
+ if ($header.length && $header.text()) {
+ text = $header.text();
+ return true;
+ }
+ })) { return text; }
+ };
+
+ var DD = new DiffDom(diffOptions);
+
+ var openLink = function (e) {
+ var el = e.currentTarget;
+ if (!el || el.nodeName !== 'A') { return; }
+ var href = el.getAttribute('href');
+ if (href) { window.open(href, '_blank'); }
+ };
+
+ // apply patches, and try not to lose the cursor in the process!
+ var applyHjson = function (shjson) {
+ var userDocStateDom = hjsonToDom(JSON.parse(shjson));
+
+ if (!readOnly && !initializing) {
+ userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
+ }
+ var patch = (DD).diff(inner, userDocStateDom);
+ (DD).apply(inner, patch);
+ if (readOnly) {
+ var $links = $(inner).find('a');
+ // off so that we don't end up with multiple identical handlers
+ $links.off('click', openLink).on('click', openLink);
+ }
+ };
+
+ var stringifyDOM = module.stringifyDOM = function (dom) {
+ var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
+ hjson[3] = {
+ metadata: {
+ users: UserList.userData,
+ defaultTitle: Title.defaultTitle,
+ type: 'pad'
+ }
+ };
+ if (!initializing) {
+ hjson[3].metadata.title = Title.title;
+ } else if (Cryptpad.initialName && !hjson[3].metadata.title) {
+ hjson[3].metadata.title = Cryptpad.initialName;
+ }
+ return stringify(hjson);
+ };
+
+ var realtimeOptions = {
+ // the websocket URL
+ websocketURL: Cryptpad.getWebsocketURL(),
+
+ // the channel we will communicate over
+ channel: 'x',//secret.channel,
+
+ // the nework used for the file store if it exists
+ network: Cryptpad.getNetwork(),
+
+ // our public key
+ validateKey: undefined,//secret.keys.validateKey || undefined,
+ readOnly: readOnly,
+
+ // Pass in encrypt and decrypt methods
+ crypto: undefined,//Crypto.createEncryptor(secret.keys),
+
+ // really basic operational transform
+ transformFunction : JsonOT.validate,
+
+ // cryptpad debug logging (default is 1)
+ // logLevel: 0,
+
+ validateContent: function (content) {
+ try {
+ JSON.parse(content);
+ return true;
+ } catch (e) {
+ console.log("Failed to parse, rejecting patch");
+ return false;
+ }
+ }
+ };
+
+ var setHistory = function (bool, update) {
+ isHistoryMode = bool;
+ setEditable(!bool);
+ if (!bool && update) {
+ realtimeOptions.onRemote();
+ }
+ };
+
+ realtimeOptions.onRemote = function () {
+ if (initializing) { return; }
+ if (isHistoryMode) { return; }
+
+ var oldShjson = stringifyDOM(inner);
+
+ var shjson = module.realtime.getUserDoc();
+
+ // remember where the cursor is
+ cursor.update();
+
+ // Update the user list (metadata) from the hyperjson
+ Metadata.update(shjson);
+
+ var newInner = JSON.parse(shjson);
+ var newSInner;
+ if (newInner.length > 2) {
+ newSInner = stringify(newInner[2]);
+ }
+
+ // build a dom from HJSON, diff, and patch the editor
+ applyHjson(shjson);
+
+ if (!readOnly) {
+ var shjson2 = stringifyDOM(inner);
+ if (shjson2 !== shjson) {
+ console.error("shjson2 !== shjson");
+ module.patchText(shjson2);
+
+ /* pushing back over the wire is necessary, but it can
+ result in a feedback loop, which we call a browser
+ fight */
+ if (module.logFights) {
+ // what changed?
+ var op = TextPatcher.diff(shjson, shjson2);
+ // log the changes
+ TextPatcher.log(shjson, op);
+ var sop = JSON.stringify(TextPatcher.format(shjson, op));
+
+ var index = module.fights.indexOf(sop);
+ if (index === -1) {
+ module.fights.push(sop);
+ console.log("Found a new type of browser disagreement");
+ console.log("You can inspect the list in your " +
+ "console at `REALTIME_MODULE.fights`");
+ console.log(module.fights);
+ } else {
+ console.log("Encountered a known browser disagreement: " +
+ "available at `REALTIME_MODULE.fights[%s]`", index);
+ }
+ }
+ }
+ }
+
+ // Notify only when the content has changed, not when someone has joined/left
+ var oldSInner = stringify(JSON.parse(oldShjson)[2]);
+ if (newSInner && newSInner !== oldSInner) {
+ Cryptpad.notify();
+ }
+ };
+
+ var getHTML = function () {
+ return ('\n' + '\n' + inner.innerHTML);
+ };
+
+ var domFromHTML = function (html) {
+ return new DOMParser().parseFromString(html, 'text/html');
+ };
+
+ var exportFile = function () {
+ var html = getHTML();
+ var suggestion = Title.suggestTitle('cryptpad-document');
+ Cryptpad.prompt(Messages.exportPrompt,
+ Cryptpad.fixFileName(suggestion) + '.html', function (filename) {
+ if (!(typeof(filename) === 'string' && filename)) { return; }
+ var blob = new Blob([html], {type: "text/html;charset=utf-8"});
+ saveAs(blob, filename);
+ });
+ };
+ var importFile = function (content) {
+ var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body));
+ applyHjson(shjson);
+ realtimeOptions.onLocal();
+ };
+
+ realtimeOptions.onInit = function (info) {
+ UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad);
+
+ var titleCfg = { getHeadingText: getHeadingText };
+ Title = Cryptpad.createTitle(titleCfg, realtimeOptions.onLocal, Cryptpad);
+
+ Metadata = Cryptpad.createMetadata(UserList, Title, null, Cryptpad);
+
+ var configTb = {
+ displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'],
+ userList: UserList.getToolbarConfig(),
+ share: {
+ secret: secret,
+ channel: info.channel
+ },
+ title: Title.getTitleConfig(),
+ common: Cryptpad,
+ readOnly: readOnly,
+ ifrw: window,
+ realtime: info.realtime,
+ network: info.network,
+ $container: $bar,
+ $contentContainer: $('#cke_1_contents'),
+ };
+ toolbar = info.realtime.toolbar = Toolbar.create(configTb);
+
+ var src = 'less!/customize/src/less/toolbar.less';
+ require([
+ src
+ ], function () {
+ var $html = $bar.closest('html');
+ $html
+ .find('head style[data-original-src="' + src.replace(/less!/, '') + '"]')
+ .appendTo($html.find('head'));
+ });
+
+ Title.setToolbar(toolbar);
+
+ var $rightside = toolbar.$rightside;
+ var $drawer = toolbar.$drawer;
+
+ var editHash;
+
+ if (!readOnly) {
+ editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
+ }
+
+ $bar.find('#cke_1_toolbar_collapser').hide();
+ if (!readOnly) {
+ // Expand / collapse the toolbar
+ var $collapse = Cryptpad.createButton(null, true);
+ $collapse.removeClass('fa-question');
+ var updateIcon = function () {
+ $collapse.removeClass('fa-caret-down').removeClass('fa-caret-up');
+ var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible');
+ if (isCollapsed) {
+ if (!initializing) { Cryptpad.feedback('HIDETOOLBAR_PAD'); }
+ $collapse.addClass('fa-caret-down');
+ }
+ else {
+ if (!initializing) { Cryptpad.feedback('SHOWTOOLBAR_PAD'); }
+ $collapse.addClass('fa-caret-up');
+ }
+ };
+ updateIcon();
+ $collapse.click(function () {
+ $(window).trigger('resize');
+ $iframe.find('.cke_toolbox_main').toggle();
+ $(window).trigger('cryptpad-ck-toolbar');
+ updateIcon();
+ });
+ $rightside.append($collapse);
+ }
+
+ /* add a history button */
+ var histConfig = {
+ onLocal: realtimeOptions.onLocal,
+ onRemote: realtimeOptions.onRemote,
+ setHistory: setHistory,
+ applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); },
+ $toolbar: $bar
+ };
+ var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
+ $drawer.append($hist);
+
+ /* save as template */
+ if (!Cryptpad.isTemplate(window.location.href)) {
+ var templateObj = {
+ rt: info.realtime,
+ Crypt: Cryptget,
+ getTitle: function () { return document.title; }
+ };
+ var $templateButton = Cryptpad.createButton('template', true, templateObj);
+ $rightside.append($templateButton);
+ }
+
+ /* add an export button */
+ var $export = Cryptpad.createButton('export', true, {}, exportFile);
+ $drawer.append($export);
+
+ if (!readOnly) {
+ /* add an import button */
+ var $import = Cryptpad.createButton('import', true, {
+ accept: 'text/html'
+ }, importFile);
+ $drawer.append($import);
+ }
+
+ /* add a forget button */
+ var forgetCb = function (err) {
+ if (err) { return; }
+ setEditable(false);
+ };
+ var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb);
+ $rightside.append($forgetPad);
+
+ // set the hash
+ if (!readOnly) { Cryptpad.replaceHash(editHash); }
+ };
+
+ // this should only ever get called once, when the chain syncs
+ realtimeOptions.onReady = function (info) {
+ if (!module.isMaximized) {
+ module.isMaximized = true;
+ $iframe.find('iframe.cke_wysiwyg_frame').css('width', '');
+ $iframe.find('iframe.cke_wysiwyg_frame').css('height', '');
+ }
+ $iframe.find('body').addClass('app-pad');
+
+ if (module.realtime !== info.realtime) {
+ module.patchText = TextPatcher.create({
+ realtime: info.realtime,
+ //logging: true,
+ });
+ }
+
+ module.realtime = info.realtime;
+
+ var shjson = module.realtime.getUserDoc();
+
+ var newPad = false;
+ if (shjson === '') { newPad = true; }
+
+ if (!newPad) {
+ applyHjson(shjson);
+
+ // Update the user list (metadata) from the hyperjson
+ Metadata.update(shjson);
+
+ if (!readOnly) {
+ var shjson2 = stringifyDOM(inner);
+ var hjson2 = JSON.parse(shjson2).slice(0,-1);
+ var hjson = JSON.parse(shjson).slice(0,-1);
+ if (stringify(hjson2) !== stringify(hjson)) {
+ console.log('err');
+ console.error("shjson2 !== shjson");
+ Cryptpad.errorLoadingScreen(Messages.wrongApp);
+ throw new Error();
+ }
+ }
+ } else {
+ Title.updateTitle(Cryptpad.initialName || Title.defaultTitle);
+ documentBody.innerHTML = Messages.initialState;
+ }
+
+ Cryptpad.removeLoadingScreen(emitResize);
+ setEditable(!readOnly);
+ initializing = false;
+
+ if (readOnly) { return; }
+ UserList.getLastName(toolbar.$userNameButton, newPad);
+ editor.focus();
+ if (newPad) {
+ cursor.setToEnd();
+ } else {
+ cursor.setToStart();
+ }
+ };
+
+ realtimeOptions.onAbort = function () {
+ console.log("Aborting the session!");
+ // stop the user from continuing to edit
+ setEditable(false);
+ toolbar.failed();
+ Cryptpad.alert(Messages.common_connectionLost, undefined, true);
+ };
+
+ realtimeOptions.onConnectionChange = function (info) {
+ setEditable(info.state);
+ toolbar.failed();
+ if (info.state) {
+ initializing = true;
+ toolbar.reconnecting(info.myId);
+ Cryptpad.findOKButton().click();
+ } else {
+ Cryptpad.alert(Messages.common_connectionLost, undefined, true);
+ }
+ };
+
+ realtimeOptions.onError = onConnectError;
+
+ var onLocal = realtimeOptions.onLocal = function () {
+ if (initializing) { return; }
+ if (isHistoryMode) { return; }
+ if (readOnly) { return; }
+
+ // stringify the json and send it into chainpad
+ var shjson = stringifyDOM(inner);
+
+ module.patchText(shjson);
+ if (module.realtime.getUserDoc() !== shjson) {
+ console.error("realtime.getUserDoc() !== shjson");
+ }
+ };
+
+ module.realtimeInput = realtimeInput.start(realtimeOptions);
+
+ Cryptpad.onLogout(function () { setEditable(false); });
+
+ /* hitting enter makes a new line, but places the cursor inside
+ of the instead of the
. This makes it such that you
+ cannot type until you click, which is rather unnacceptable.
+ If the cursor is ever inside such a , you probably want
+ to push it out to the parent element, which ought to be a
+ paragraph tag. This needs to be done on keydown, otherwise
+ the first such keypress will not be inserted into the P. */
+ inner.addEventListener('keydown', cursor.brFix);
+
+ editor.on('change', onLocal);
+
+ // export the typing tests to the window.
+ // call like `test = easyTest()`
+ // terminate the test like `test.cancel()`
+ window.easyTest = function () {
+ cursor.update();
+ var start = cursor.Range.start;
+ var test = TypingTest.testInput(inner, start.el, start.offset, onLocal);
+ onLocal();
+ return test;
+ };
+
+ $bar.find('.cke_button').click(function () {
+ var e = this;
+ var classString = e.getAttribute('class');
+ var classes = classString.split(' ').filter(function (c) {
+ return /cke_button__/.test(c);
+ });
+
+ var id = classes[0];
+ if (typeof(id) === 'string') {
+ Cryptpad.feedback(id.toUpperCase());
+ }
+ });
+ });
+ };
+
+ var interval = 100;
+ var second = function (Ckeditor) {
+ //Cryptpad.ready(function () {
+ andThen(Ckeditor);
+ //Cryptpad.reportAppUsage();
+ //});
+ Cryptpad.onError(function (info) {
+ if (info && info.type === "store") {
+ onConnectError();
+ }
+ });
+ };
+
+ var first = function () {
+ Ckeditor = window.CKEDITOR;
+ if (Ckeditor) {
+ // mobile configuration
+ Ckeditor.config.toolbarCanCollapse = true;
+ if (screen.height < 800) {
+ Ckeditor.config.toolbarStartupExpanded = false;
+ $('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no');
+ } else {
+ $('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes');
+ }
+ second(Ckeditor);
+ } else {
+ //console.log("Ckeditor was not defined. Trying again in %sms",interval);
+ setTimeout(first, interval);
+ }
+ };
+
+ $(function () {
+ Cryptpad.addLoadingScreen();
+ first();
+ });
+});
diff --git a/www/pad2/outer.js b/www/pad2/outer.js
new file mode 100644
index 000000000..5c7c11364
--- /dev/null
+++ b/www/pad2/outer.js
@@ -0,0 +1,13 @@
+
+define([
+ '/common/sframe-ctrl.js',
+ 'jquery'
+], function (SFrameCtrl, $) {
+ console.log('xxx');
+ $(function () {
+ console.log('go');
+ SFrameCtrl.init($('#sbox-iframe')[0], function () {
+ console.log('\n\ndone\n\n');
+ });
+ });
+});
\ No newline at end of file