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

. This makes it such that you + cannot type until you click, which is rather unnacceptable. + If the cursor is ever inside such a
, you probably want + to push it out to the parent element, which ought to be a + paragraph tag. This needs to be done on keydown, otherwise + the first such keypress will not be inserted into the P. */ + inner.addEventListener('keydown', cursor.brFix); + + editor.on('change', onLocal); + + // export the typing tests to the window. + // call like `test = easyTest()` + // terminate the test like `test.cancel()` + window.easyTest = function () { + cursor.update(); + var start = cursor.Range.start; + var test = TypingTest.testInput(inner, start.el, start.offset, onLocal); + onLocal(); + return test; + }; + + $bar.find('.cke_button').click(function () { + var e = this; + var classString = e.getAttribute('class'); + var classes = classString.split(' ').filter(function (c) { + return /cke_button__/.test(c); + }); + + var id = classes[0]; + if (typeof(id) === 'string') { + Cryptpad.feedback(id.toUpperCase()); + } + }); + }); + }; + + var interval = 100; + var second = function (Ckeditor) { + //Cryptpad.ready(function () { + andThen(Ckeditor); + //Cryptpad.reportAppUsage(); + //}); + Cryptpad.onError(function (info) { + if (info && info.type === "store") { + onConnectError(); + } + }); + }; + + var first = function () { + Ckeditor = window.CKEDITOR; + if (Ckeditor) { + // mobile configuration + Ckeditor.config.toolbarCanCollapse = true; + if (screen.height < 800) { + Ckeditor.config.toolbarStartupExpanded = false; + $('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no'); + } else { + $('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes'); + } + second(Ckeditor); + } else { + //console.log("Ckeditor was not defined. Trying again in %sms",interval); + setTimeout(first, interval); + } + }; + + $(function () { + Cryptpad.addLoadingScreen(); + first(); + }); +}); diff --git a/www/pad2/outer.js b/www/pad2/outer.js new file mode 100644 index 000000000..5c7c11364 --- /dev/null +++ b/www/pad2/outer.js @@ -0,0 +1,13 @@ + +define([ + '/common/sframe-ctrl.js', + 'jquery' +], function (SFrameCtrl, $) { + console.log('xxx'); + $(function () { + console.log('go'); + SFrameCtrl.init($('#sbox-iframe')[0], function () { + console.log('\n\ndone\n\n'); + }); + }); +}); \ No newline at end of file From 35a55a15edbf1336190bcfa9a7864f3624767519 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Mon, 7 Aug 2017 17:23:28 +0200 Subject: [PATCH 02/63] wip --- www/common/sframe-boot.js | 9 ++- www/common/sframe-boot2.js | 1 - www/common/sframe-chainpad-netflux-outer.js | 47 ++++----------- www/common/sframe-channel.js | 46 ++++++++++++++ www/common/sframe-ctrl.js | 67 ++++++++++++++------- www/common/sframe-protocol.js | 5 ++ 6 files changed, 113 insertions(+), 62 deletions(-) create mode 100644 www/common/sframe-channel.js create mode 100644 www/common/sframe-protocol.js diff --git a/www/common/sframe-boot.js b/www/common/sframe-boot.js index 127505ef1..34b25167e 100644 --- a/www/common/sframe-boot.js +++ b/www/common/sframe-boot.js @@ -1,10 +1,9 @@ // Stage 0, this gets cached which means we can't change it. boot2-sframe.js is changable. // Note that this file is meant to be executed only inside of a sandbox iframe. window.addEventListener('message', function (msg) { - var data = msg.data; + var data = JSON.parse(msg.data); if (data.q !== 'INIT') { return; } - msg.source.postMessage({ txid: data.txid, res: 'OK' }, '*'); - if (data.requireConf) { require.config(data.requireConf); } + msg.source.postMessage({ txid: data.txid, content: 'OK' }, '*'); + if (data.content && data.content.requireConf) { require.config(data.content.requireConf); } require(['/common/sframe-boot2.js'], function () { }); -}); -console.log('boot'); \ No newline at end of file +}); \ No newline at end of file diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js index ac0c060e9..0c98fb5d7 100644 --- a/www/common/sframe-boot2.js +++ b/www/common/sframe-boot2.js @@ -22,6 +22,5 @@ console.log('boot2'); window.__defineGetter__('localStorage', function () { return mkFakeStore(); }); window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); }); - require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]); }); diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index 0da737c0f..c27be9021 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -27,45 +27,24 @@ define([ var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; - module.exports.start = function (config) { - var websocketUrl = config.websocketURL; - var userName = config.userName; - var channel = config.channel; - var Crypto = config.crypto; - var validateKey = config.validateKey; - var readOnly = config.readOnly || false; - - // make sure configuration is defined - config = config || {}; + module.exports.start = function (conf) { + var websocketUrl = conf.websocketURL; + var userName = conf.userName; + var channel = conf.channel; + var Crypto = conf.crypto; + var validateKey = conf.validateKey; + var readOnly = conf.readOnly || false; + var websocketURL = conf.websocketURL; + var network = conf.network; + conf = undefined; var initializing = true; var toReturn = {}; var messagesHistory = []; var chainpadAdapter = {}; var realtime; - var network = config.network; var lastKnownHash; - var userList = { - change : [], - onChange : function(newData) { - userList.change.forEach(function (el) { - el(newData); - }); - }, - users: [] - }; - - var onJoining = function(peer) { - if(peer.length !== 32) { return; } - var list = userList.users; - var index = list.indexOf(peer); - if(index === -1) { - userList.users.push(peer); - } - userList.onChange(); - }; - var onReady = function(wc, network) { // Trigger onReady only if not ready yet. This is important because the history keeper sends a direct // message through "network" when it is synced, and it triggers onReady for each channel joined. @@ -73,10 +52,8 @@ define([ realtime.start(); - if(config.setMyID) { - config.setMyID({ - myID: wc.myID - }); + if(setMyID) { + setMyID({ myID: wc.myID }); } // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced if (!readOnly) { diff --git a/www/common/sframe-channel.js b/www/common/sframe-channel.js new file mode 100644 index 000000000..4291f5108 --- /dev/null +++ b/www/common/sframe-channel.js @@ -0,0 +1,46 @@ +// This file provides the internal API for talking from inside of the sandbox iframe +// The external API is in sframe-ctrl.js +define([], function () { + var iframe; + var handlers = {}; + var queries = {}; + var module = { exports: {} }; + + var mkTxid = function () { + return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', ''); + }; + + module.exports.query = function (q, content, cb) { + if (!iframe) { throw new Error('not yet initialized'); } + var txid = mkTxid(); + var timeout = setTimeout(function () { + delete queries[txid]; + cb("Timeout making query " + q); + }); + queries[txid] = function (data, msg) { + clearTimeout(timeout); + delete queries[txid]; + cb(undefined, data.content, msg); + }; + iframe.contentWindow.postMessage(JSON.stringify({ + txid: txid, + content: content, + q: q + }), '*'); + }; + + module.exports.registerHandler = function (queryType, handler) { + if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } + handlers[queryType] = function (msg) { + var data = JSON.parse(msg.data); + handler(data.content, function (replyContent) { + msg.source.postMessage(JSON.stringify({ + txid: data.txid, + content: replyContent + }), '*'); + }, msg); + }; + }; + + return module.exports; +}); \ No newline at end of file diff --git a/www/common/sframe-ctrl.js b/www/common/sframe-ctrl.js index 536f2689e..ebae43985 100644 --- a/www/common/sframe-ctrl.js +++ b/www/common/sframe-ctrl.js @@ -1,4 +1,5 @@ -// This file provides the external API for launching the sandboxed iframe. +// This file provides the external API for launching and talking to the sandboxed iframe. +// The internal API is in sframe-channel.js define([ '/common/requireconfig.js' ], function (RequireConfig) { @@ -11,40 +12,64 @@ define([ return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', ''); }; - var init = module.exports.init = function (frame, cb) { + module.exports.init = function (frame, cb) { if (iframe) { throw new Error('already initialized'); } var txid = mkTxid(); var intr = setInterval(function () { - frame.contentWindow.postMessage({ + frame.contentWindow.postMessage(JSON.stringify({ txid: txid, - requireConf: RequireConfig, + content: { requireConf: RequireConfig }, q: 'INIT' - }, '*'); + }), '*'); }); window.addEventListener('message', function (msg) { - console.log('recv'); - console.log(msg.origin); - var data = msg.data; - if (data.txid !== txid) { return; } - clearInterval(intr); - iframe = frame; - cb(); + var data = JSON.parse(msg.data); + if (!iframe) { + if (data.txid !== txid) { return; } + clearInterval(intr); + iframe = frame; + cb(); + } else if (typeof(data.q) === 'string' && handlers[data.q]) { + handlers[data.q](data, msg); + } else if (typeof(data.q) === 'undefined' && queries[data.txid]) { + queries[data.txid](data, msg); + } else { + console.log("Unhandled message"); + console.log(msg); + } }); }; - var query = module.exports.query = function (msg, cb) { + + module.exports.query = function (q, content, cb) { if (!iframe) { throw new Error('not yet initialized'); } var txid = mkTxid(); - queries[txid] = { - txid: txid, - timeout: setTimeout(function () { - delete queries[txid]; - console.log("Error") - }) + var timeout = setTimeout(function () { + delete queries[txid]; + cb("Timeout making query " + q); + }); + queries[txid] = function (data, msg) { + clearTimeout(timeout); + delete queries[txid]; + cb(undefined, data.content, msg); }; + iframe.contentWindow.postMessage(JSON.stringify({ + txid: txid, + content: content, + q: q + }), '*'); }; - var registerHandler = module.exports.registerHandler = function (queryType, handler) { + + module.exports.registerHandler = function (queryType, handler) { if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } - handlers[queryType] = handler; + handlers[queryType] = function (msg) { + var data = JSON.parse(msg.data); + handler(data.content, function (replyContent) { + msg.source.postMessage(JSON.stringify({ + txid: data.txid, + content: replyContent + }), '*'); + }, msg); + }; }; return module.exports; diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js new file mode 100644 index 000000000..56d32601a --- /dev/null +++ b/www/common/sframe-protocol.js @@ -0,0 +1,5 @@ +// This file defines all of the RPC calls +// The internal API is in sframe-channel.js +define({ + +}); \ No newline at end of file From 4b25ab80d6fce33fb2c95a98de26a91e5d1ae9eb Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Wed, 9 Aug 2017 14:45:39 +0200 Subject: [PATCH 03/63] wip --- bower.json | 3 +- www/common/metadata-manager.js | 0 www/common/sframe-boot.js | 2 +- www/common/sframe-boot2.js | 7 +- www/common/sframe-chainpad-netflux-inner.js | 441 ++----- www/common/sframe-chainpad-netflux-outer.js | 300 ++--- www/common/sframe-channel.js | 115 +- www/common/sframe-ctrl.js | 76 -- www/common/sframe-protocol.js | 32 +- www/pad2/main.js | 1139 ++++++++++--------- www/pad2/outer.js | 40 +- 11 files changed, 913 insertions(+), 1242 deletions(-) create mode 100644 www/common/metadata-manager.js delete mode 100644 www/common/sframe-ctrl.js diff --git a/bower.json b/bower.json index b04ff3a6b..9e6ee5978 100644 --- a/bower.json +++ b/bower.json @@ -39,6 +39,7 @@ "require-css": "0.1.10", "less": "^2.7.2", "bootstrap": "#v4.0.0-alpha.6", - "diff-dom": "2.1.1" + "diff-dom": "2.1.1", + "nthen": "^0.1.5" } } diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js new file mode 100644 index 000000000..e69de29bb diff --git a/www/common/sframe-boot.js b/www/common/sframe-boot.js index 34b25167e..190055682 100644 --- a/www/common/sframe-boot.js +++ b/www/common/sframe-boot.js @@ -3,7 +3,7 @@ window.addEventListener('message', function (msg) { var data = JSON.parse(msg.data); if (data.q !== 'INIT') { return; } - msg.source.postMessage({ txid: data.txid, content: 'OK' }, '*'); + msg.source.postMessage(JSON.stringify({ txid: data.txid, content: 'OK' }), '*'); if (data.content && data.content.requireConf) { require.config(data.content.requireConf); } require(['/common/sframe-boot2.js'], function () { }); }); \ No newline at end of file diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js index 0c98fb5d7..94ec9d511 100644 --- a/www/common/sframe-boot2.js +++ b/www/common/sframe-boot2.js @@ -1,8 +1,9 @@ // This is stage 1, it can be changed but you must bump the version of the project. // Note: This must only be loaded from inside of a sandbox-iframe. define([ - '/common/requireconfig.js' -], function (RequireConfig) { + '/common/requireconfig.js', + '/common/sframe-channel.js' +], function (RequireConfig, SFrameChannel) { require.config(RequireConfig); console.log('boot2'); // most of CryptPad breaks if you don't support isArray @@ -22,5 +23,7 @@ console.log('boot2'); window.__defineGetter__('localStorage', function () { return mkFakeStore(); }); window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); }); + SFrameChannel.init(window.top, function () { }); + require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]); }); diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index 746b1d733..32e1beb0f 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -15,39 +15,17 @@ * along with this program. If not, see . */ define([ - '/bower_components/netflux-websocket/netflux-client.js', + '/common/sframe-channel.js', '/bower_components/chainpad/chainpad.dist.js', -], function (Netflux) { +], function (SFrameChannel) { var ChainPad = window.ChainPad; - var USE_HISTORY = true; var module = { exports: {} }; var verbose = function (x) { console.log(x); }; verbose = function () {}; // comment out to enable verbose logging - var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; - - module.exports.start = function (config) { - console.log(config); - var websocketUrl = config.websocketURL; - var userName = config.userName; - var channel = config.channel; - var Crypto = config.crypto; - var validateKey = config.validateKey; - var readOnly = config.readOnly || false; - - // make sure configuration is defined - config = config || {}; - - var initializing = true; - var toReturn = {}; - var messagesHistory = []; - var chainpadAdapter = {}; - var realtime; - var network = config.network; - var lastKnownHash; - - var userList = { + var mkUserList = function () { + var userList = Object.freeze({ change : [], onChange : function(newData) { userList.change.forEach(function (el) { @@ -55,9 +33,9 @@ define([ }); }, users: [] - }; + }); - var onJoining = function(peer) { + var onJoining = function (peer) { if(peer.length !== 32) { return; } var list = userList.users; var index = list.indexOf(peer); @@ -67,100 +45,8 @@ define([ userList.onChange(); }; - var onReady = function(wc, network) { - // Trigger onReady only if not ready yet. This is important because the history keeper sends a direct - // message through "network" when it is synced, and it triggers onReady for each channel joined. - if (!initializing) { return; } - - realtime.start(); - - if(config.setMyID) { - config.setMyID({ - myID: wc.myID - }); - } - // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced - if (!readOnly) { - onJoining(wc.myID); - } - - // we're fully synced - initializing = false; - - if (config.onReady) { - config.onReady({ - realtime: realtime, - network: network, - userList: userList, - myId: wc.myID, - leave: wc.leave - }); - } - }; - - var onMessage = function(peer, msg, wc, network, direct) { - // unpack the history keeper from the webchannel - var hk = network.historyKeeper; - - // Old server - if(wc && (msg === 0 || msg === '0')) { - onReady(wc, network); - return; - } - if (direct && peer !== hk) { - return; - } - if (direct) { - var parsed = JSON.parse(msg); - if (parsed.validateKey && parsed.channel) { - if (parsed.channel === wc.id && !validateKey) { - validateKey = parsed.validateKey; - } - // We have to return even if it is not the current channel: - // we don't want to continue with other channels messages here - return; - } - if (parsed.state && parsed.state === 1 && parsed.channel) { - if (parsed.channel === wc.id) { - onReady(wc, network); - } - // We have to return even if it is not the current channel: - // we don't want to continue with other channels messages here - return; - } - } - // The history keeper is different for each channel : - // no need to check if the message is related to the current channel - if (peer === hk){ - // if the peer is the 'history keeper', extract their message - var parsed1 = JSON.parse(msg); - msg = parsed1[4]; - // Check that this is a message for us - if (parsed1[3] !== wc.id) { return; } - } - - lastKnownHash = msg.slice(0,64); - var message = chainpadAdapter.msgIn(peer, msg); - - verbose(message); - - if (!initializing) { - if (config.onLocal) { - config.onLocal(); - } - } - - // slice off the bencoded header - // Why are we getting bencoded stuff to begin with? - // FIXME this shouldn't be necessary - message = unBencode(message);//.slice(message.indexOf(':[') + 1); - - // pass the message into Chainpad - realtime.message(message); - }; - // update UI components to show that one of the other peers has left - var onLeaving = function(peer) { + var onLeaving = function (peer) { var list = userList.users; var index = list.indexOf(peer); if(index !== -1) { @@ -169,246 +55,93 @@ define([ userList.onChange(); }; - // shim between chainpad and netflux - chainpadAdapter = { - msgIn : function(peerId, msg) { - msg = msg.replace(/^cp\|/, ''); - try { - var decryptedMsg = Crypto.decrypt(msg, validateKey); - messagesHistory.push(decryptedMsg); - return decryptedMsg; - } catch (err) { - console.error(err); - return msg; - } - }, - msgOut : function(msg) { - if (readOnly) { return; } - try { - var cmsg = Crypto.encrypt(msg); - if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } - return cmsg; - } catch (err) { - console.log(msg); - throw err; - } - } + var onReset = function () { + userList.users.forEach(onLeaving); }; - var createRealtime = function() { - return ChainPad.create({ + return Object.freeze({ + list: userList, + onJoin: onJoining, + onLeave: onLeaving, + onReset: onReset + }); + }; + + module.exports.start = function (config) { + var onConnectionChange = config.onConnectionChange || function () { }; + var onRemote = config.onRemote || function () { }; + var onInit = config.onInit || function () { }; + var onLocal = config.onLocal || function () { }; + var setMyID = config.setMyID || function () { }; + var onReady = config.onReady || function () { }; + var userName = config.userName; + var initialState = config.initialState; + var transformFunction = config.transformFunction; + var validateContent = config.validateContent; + var avgSyncMilliseconds = config.avgSyncMilliseconds; + var logLevel = typeof(config.logLevel) !== 'undefined'? config.logLevel : 1; + var readOnly = config.readOnly || false; + config = undefined; + + var chainpad; + var userList = mkUserList(); + var myID; + var isReady = false; + + SFrameChannel.on('EV_RT_JOIN', userList.onJoin); + SFrameChannel.on('EV_RT_LEAVE', userList.onLeave); + SFrameChannel.on('EV_RT_DISCONNECT', function () { + isReady = false; + userList.onReset(); + onConnectionChange({ state: false }); + }); + SFrameChannel.on('EV_RT_CONNECT', function (content) { + content.members.forEach(userList.onJoin); + myID = content.myID; + isReady = false; + if (chainpad) { + // it's a reconnect + onConnectionChange({ state: true, myId: myID }); + return; + } + chainpad = ChainPad.create({ userName: userName, - initialState: config.initialState, - transformFunction: config.transformFunction, - validateContent: config.validateContent, - avgSyncMilliseconds: config.avgSyncMilliseconds, - logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1 + initialState: initialState, + transformFunction: transformFunction, + validateContent: validateContent, + avgSyncMilliseconds: avgSyncMilliseconds, + logLevel: logLevel }); - }; - - // We use an object to store the webchannel so that we don't have to push new handlers to chainpad - // and remove the old ones when reconnecting and keeping the same 'realtime' object - // See realtime.onMessage below: we call wc.bcast(...) but wc may change - var wcObject = {}; - var onOpen = function(wc, network, initialize) { - wcObject.wc = wc; - channel = wc.id; - - // Add the existing peers in the userList - wc.members.forEach(onJoining); - - // Add the handlers to the WebChannel - wc.on('message', function (msg, sender) { //Channel msg - onMessage(sender, msg, wc, network); + chainpad.onMessage(function(message, cb) { + SFrameChannel.query('Q_RT_MESSAGE', message, cb); }); - wc.on('join', onJoining); - wc.on('leave', onLeaving); - - if (initialize) { - toReturn.realtime = realtime = createRealtime(); - - realtime._patch = realtime.patch; - realtime.patch = function (patch, x, y) { - if (initializing) { - console.error("attempted to change the content before chainpad was synced"); - } - return realtime._patch(patch, x, y); - }; - realtime._change = realtime.change; - realtime.change = function (offset, count, chars) { - if (initializing) { - console.error("attempted to change the content before chainpad was synced"); - } - return realtime._change(offset, count, chars); - }; - - if (config.onInit) { - config.onInit({ - myID: wc.myID, - realtime: realtime, - getLag: network.getLag, - userList: userList, - network: network, - channel: channel - }); - } - - // Sending a message... - realtime.onMessage(function(message, cb) { - // Filter messages sent by Chainpad to make it compatible with Netflux - message = chainpadAdapter.msgOut(message); - if(message) { - // Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we - // want to keep the same chainpad (realtime) object - wcObject.wc.bcast(message).then(function() { - cb(); - }, function(err) { - // The message has not been sent, display the error. - console.error(err); - }); - } - }); - - realtime.onPatch(function () { - if (config.onRemote) { - config.onRemote({ - realtime: realtime - }); - } - }); - } - - // Get the channel history - if(USE_HISTORY) { - var hk; - - wc.members.forEach(function (p) { - if (p.length === 16) { hk = p; } - }); - network.historyKeeper = hk; - - var msg = ['GET_HISTORY', wc.id]; - // Add the validateKey if we are the channel creator and we have a validateKey - msg.push(validateKey); - msg.push(lastKnownHash); - if (hk) { network.sendto(hk, JSON.stringify(msg)); } - } - else { - onReady(wc, network); - } - }; - - // Set a flag to avoid calling onAbort or onConnectionChange when the user is leaving the page - var isIntentionallyLeaving = false; - window.addEventListener("beforeunload", function () { - isIntentionallyLeaving = true; - }); - - var findChannelById = function(webChannels, channelId) { - var webChannel; - - // Array.some terminates once a truthy value is returned - // best case is faster than forEach, though webchannel arrays seem - // to consistently have a length of 1 - webChannels.some(function(chan) { - if(chan.id === channelId) { webChannel = chan; return true;} + chainpad.onPatch(function () { + onRemote({ realtime: chainpad }); }); - return webChannel; - }; - - var onConnectError = function (err) { - if (config.onError) { - config.onError({ - error: err.type - }); - } - }; - - var joinSession = function (endPoint, cb) { - // a websocket URL has been provided - // connect to it with Netflux. - if (typeof(endPoint) === 'string') { - Netflux.connect(endPoint).then(cb, onConnectError); - } else if (typeof(endPoint.then) === 'function') { - // a netflux network promise was provided - // connect to it and use a channel - endPoint.then(cb, onConnectError); - } else { - // assume it's a network and try to connect. - cb(endPoint); - } - }; - - var firstConnection = true; - /* Connect to the Netflux network, or fall back to a WebSocket - in theory this lets us connect to more netflux channels using only - one network. */ - var connectTo = function (network) { - // join the netflux network, promise to handle opening of the channel - network.join(channel || null).then(function(wc) { - onOpen(wc, network, firstConnection); - firstConnection = false; - }, function(error) { - console.error(error); + onInit({ + myID: content.myID, + realtime: chainpad, + userList: userList, + readOnly: readOnly }); - }; - - joinSession(network || websocketUrl, function (network) { - // pass messages that come out of netflux into our local handler - if (firstConnection) { - toReturn.network = network; - - network.on('disconnect', function (reason) { - if (isIntentionallyLeaving) { return; } - if (reason === "network.disconnect() called") { return; } - if (config.onConnectionChange) { - config.onConnectionChange({ - state: false - }); - return; - } - if (config.onAbort) { - config.onAbort({ - reason: reason - }); - } - }); - - network.on('reconnect', function (uid) { - if (config.onConnectionChange) { - config.onConnectionChange({ - state: true, - myId: uid - }); - var afterReconnecting = function () { - initializing = true; - userList.users=[]; - joinSession(network, connectTo); - }; - if (config.beforeReconnecting) { - config.beforeReconnecting(function (newKey, newContent) { - channel = newKey; - config.initialState = newContent; - afterReconnecting(); - }); - return; - } - afterReconnecting(); - } - }); - - network.on('message', function (msg, sender) { // Direct message - var wchan = findChannelById(network.webChannels, channel); - if(wchan) { - onMessage(sender, msg, wchan, network, true); - } - }); + }); + SFrameChannel.on('Q_RT_MESSAGE', function (content, cb) { + if (isReady) { + onLocal(); // should be onBeforeMessage } - - connectTo(network); - }, onConnectError); - - return toReturn; + chainpad.message(content); + cb('OK'); + }); + SFrameChannel.on('EV_RT_READY', function () { + if (isReady) { return; } + isReady = true; + chainpad.start(); + setMyID({ myID: myID }); + // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced + if (!readOnly) { userList.onJoin(myID); } + onReady({ realtime: chainpad }); + }); + return; }; return module.exports; -}); +}); \ No newline at end of file diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index c27be9021..d28fbe631 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -15,10 +15,8 @@ * along with this program. If not, see . */ define([ - '/bower_components/netflux-websocket/netflux-client.js', - '/bower_components/chainpad/chainpad.dist.js', -], function (Netflux) { - var ChainPad = window.ChainPad; + '/common/sframe-channel.js', +], function (SFrameChannel) { var USE_HISTORY = true; var module = { exports: {} }; @@ -27,50 +25,53 @@ define([ var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; - module.exports.start = function (conf) { - var websocketUrl = conf.websocketURL; - var userName = conf.userName; + var start = function (conf) { var channel = conf.channel; var Crypto = conf.crypto; var validateKey = conf.validateKey; var readOnly = conf.readOnly || false; - var websocketURL = conf.websocketURL; var network = conf.network; conf = undefined; var initializing = true; - var toReturn = {}; - var messagesHistory = []; - var chainpadAdapter = {}; - var realtime; var lastKnownHash; - var onReady = function(wc, network) { + var queue = []; + var messageFromInner = function (m, cb) { queue.push([ m, cb ]); }; + SFrameChannel.on('Q_RT_MESSAGE', function (message, cb) { + messageFromInner(message, cb); + }); + + var onReady = function(wc) { // Trigger onReady only if not ready yet. This is important because the history keeper sends a direct // message through "network" when it is synced, and it triggers onReady for each channel joined. if (!initializing) { return; } - - realtime.start(); - - if(setMyID) { - setMyID({ myID: wc.myID }); - } - // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced - if (!readOnly) { - onJoining(wc.myID); - } - + SFrameChannel.event('EV_RT_READY', null); // we're fully synced initializing = false; + }; - if (config.onReady) { - config.onReady({ - realtime: realtime, - network: network, - userList: userList, - myId: wc.myID, - leave: wc.leave - }); + // shim between chainpad and netflux + var msgIn = function (peerId, msg) { + msg = msg.replace(/^cp\|/, ''); + try { + var decryptedMsg = Crypto.decrypt(msg, validateKey); + return decryptedMsg; + } catch (err) { + console.error(err); + return msg; + } + }; + + var msgOut = function (msg) { + if (readOnly) { return; } + try { + var cmsg = Crypto.encrypt(msg); + if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } + return cmsg; + } catch (err) { + console.log(msg); + throw err; } }; @@ -78,11 +79,6 @@ define([ // unpack the history keeper from the webchannel var hk = network.historyKeeper; - // Old server - if(wc && (msg === 0 || msg === '0')) { - onReady(wc, network); - return; - } if (direct && peer !== hk) { return; } @@ -98,7 +94,7 @@ define([ } if (parsed.state && parsed.state === 1 && parsed.channel) { if (parsed.channel === wc.id) { - onReady(wc, network); + onReady(wc); } // We have to return even if it is not the current channel: // we don't want to continue with other channels messages here @@ -107,7 +103,7 @@ define([ } // The history keeper is different for each channel : // no need to check if the message is related to the current channel - if (peer === hk){ + if (peer === hk) { // if the peer is the 'history keeper', extract their message var parsed1 = JSON.parse(msg); msg = parsed1[4]; @@ -116,146 +112,58 @@ define([ } lastKnownHash = msg.slice(0,64); - var message = chainpadAdapter.msgIn(peer, msg); + var message = msgIn(peer, msg); verbose(message); - if (!initializing) { - if (config.onLocal) { - config.onLocal(); - } - } - // slice off the bencoded header // Why are we getting bencoded stuff to begin with? // FIXME this shouldn't be necessary message = unBencode(message);//.slice(message.indexOf(':[') + 1); // pass the message into Chainpad - realtime.message(message); - }; - - // update UI components to show that one of the other peers has left - var onLeaving = function(peer) { - var list = userList.users; - var index = list.indexOf(peer); - if(index !== -1) { - userList.users.splice(index, 1); - } - userList.onChange(); - }; - - // shim between chainpad and netflux - chainpadAdapter = { - msgIn : function(peerId, msg) { - msg = msg.replace(/^cp\|/, ''); - try { - var decryptedMsg = Crypto.decrypt(msg, validateKey); - messagesHistory.push(decryptedMsg); - return decryptedMsg; - } catch (err) { - console.error(err); - return msg; - } - }, - msgOut : function(msg) { - if (readOnly) { return; } - try { - var cmsg = Crypto.encrypt(msg); - if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } - return cmsg; - } catch (err) { - console.log(msg); - throw err; - } - } - }; - - var createRealtime = function() { - return ChainPad.create({ - userName: userName, - initialState: config.initialState, - transformFunction: config.transformFunction, - validateContent: config.validateContent, - avgSyncMilliseconds: config.avgSyncMilliseconds, - logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1 - }); + SFrameChannel.query('Q_RT_MESSAGE', message, function () { }); }; // We use an object to store the webchannel so that we don't have to push new handlers to chainpad // and remove the old ones when reconnecting and keeping the same 'realtime' object // See realtime.onMessage below: we call wc.bcast(...) but wc may change var wcObject = {}; - var onOpen = function(wc, network, initialize) { + var onOpen = function(wc, network, firstConnection) { wcObject.wc = wc; channel = wc.id; // Add the existing peers in the userList - wc.members.forEach(onJoining); + SFrameChannel.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly }); // Add the handlers to the WebChannel wc.on('message', function (msg, sender) { //Channel msg onMessage(sender, msg, wc, network); }); - wc.on('join', onJoining); - wc.on('leave', onLeaving); - - if (initialize) { - toReturn.realtime = realtime = createRealtime(); - - realtime._patch = realtime.patch; - realtime.patch = function (patch, x, y) { - if (initializing) { - console.error("attempted to change the content before chainpad was synced"); - } - return realtime._patch(patch, x, y); - }; - realtime._change = realtime.change; - realtime.change = function (offset, count, chars) { - if (initializing) { - console.error("attempted to change the content before chainpad was synced"); - } - return realtime._change(offset, count, chars); - }; - - if (config.onInit) { - config.onInit({ - myID: wc.myID, - realtime: realtime, - getLag: network.getLag, - userList: userList, - network: network, - channel: channel - }); - } + wc.on('join', function (m) { SFrameChannel.event('EV_RT_JOIN', m); }); + wc.on('leave', function (m) { SFrameChannel.event('EV_RT_LEAVE', m); }); + if (firstConnection) { // Sending a message... - realtime.onMessage(function(message, cb) { + messageFromInner = function(message, cb) { // Filter messages sent by Chainpad to make it compatible with Netflux - message = chainpadAdapter.msgOut(message); - if(message) { + message = msgOut(message); + if (message) { // Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we // want to keep the same chainpad (realtime) object wcObject.wc.bcast(message).then(function() { - cb(); + cb('OK'); }, function(err) { // The message has not been sent, display the error. console.error(err); }); } - }); - - realtime.onPatch(function () { - if (config.onRemote) { - config.onRemote({ - realtime: realtime - }); - } - }); + }; + queue.forEach(function (arr) { messageFromInner(arr[0], arr[1]); }); } // Get the channel history - if(USE_HISTORY) { + if (USE_HISTORY) { var hk; wc.members.forEach(function (p) { @@ -268,19 +176,17 @@ define([ msg.push(validateKey); msg.push(lastKnownHash); if (hk) { network.sendto(hk, JSON.stringify(msg)); } - } - else { - onReady(wc, network); + } else { + onReady(wc); } }; - // Set a flag to avoid calling onAbort or onConnectionChange when the user is leaving the page var isIntentionallyLeaving = false; window.addEventListener("beforeunload", function () { isIntentionallyLeaving = true; }); - var findChannelById = function(webChannels, channelId) { + var findChannelById = function (webChannels, channelId) { var webChannel; // Array.some terminates once a truthy value is returned @@ -292,99 +198,39 @@ define([ return webChannel; }; - var onConnectError = function (err) { - if (config.onError) { - config.onError({ - error: err.type - }); - } - }; - - var joinSession = function (endPoint, cb) { - // a websocket URL has been provided - // connect to it with Netflux. - if (typeof(endPoint) === 'string') { - Netflux.connect(endPoint).then(cb, onConnectError); - } else if (typeof(endPoint.then) === 'function') { - // a netflux network promise was provided - // connect to it and use a channel - endPoint.then(cb, onConnectError); - } else { - // assume it's a network and try to connect. - cb(endPoint); - } - }; - - var firstConnection = true; - /* Connect to the Netflux network, or fall back to a WebSocket - in theory this lets us connect to more netflux channels using only - one network. */ - var connectTo = function (network) { + var connectTo = function (network, firstConnection) { // join the netflux network, promise to handle opening of the channel network.join(channel || null).then(function(wc) { onOpen(wc, network, firstConnection); - firstConnection = false; }, function(error) { console.error(error); }); }; - joinSession(network || websocketUrl, function (network) { - // pass messages that come out of netflux into our local handler - if (firstConnection) { - toReturn.network = network; - - network.on('disconnect', function (reason) { - if (isIntentionallyLeaving) { return; } - if (reason === "network.disconnect() called") { return; } - if (config.onConnectionChange) { - config.onConnectionChange({ - state: false - }); - return; - } - if (config.onAbort) { - config.onAbort({ - reason: reason - }); - } - }); + network.on('disconnect', function (reason) { + if (isIntentionallyLeaving) { return; } + if (reason === "network.disconnect() called") { return; } + SFrameChannel.event('EV_RT_DISCONNECT'); + }); - network.on('reconnect', function (uid) { - if (config.onConnectionChange) { - config.onConnectionChange({ - state: true, - myId: uid - }); - var afterReconnecting = function () { - initializing = true; - userList.users=[]; - joinSession(network, connectTo); - }; - if (config.beforeReconnecting) { - config.beforeReconnecting(function (newKey, newContent) { - channel = newKey; - config.initialState = newContent; - afterReconnecting(); - }); - return; - } - afterReconnecting(); - } - }); + network.on('reconnect', function (uid) { + initializing = true; + connectTo(network, false); + }); - network.on('message', function (msg, sender) { // Direct message - var wchan = findChannelById(network.webChannels, channel); - if(wchan) { - onMessage(sender, msg, wchan, network, true); - } - }); + network.on('message', function (msg, sender) { // Direct message + var wchan = findChannelById(network.webChannels, channel); + if (wchan) { + onMessage(sender, msg, wchan, network, true); } + }); - connectTo(network); - }, onConnectError); + connectTo(network, true); + }; - return toReturn; + return { + start: function (config) { + SFrameChannel.whenReg('EV_RT_READY', function () { start(config); }); + } }; - return module.exports; }); diff --git a/www/common/sframe-channel.js b/www/common/sframe-channel.js index 4291f5108..793906c5a 100644 --- a/www/common/sframe-channel.js +++ b/www/common/sframe-channel.js @@ -1,38 +1,108 @@ -// This file provides the internal API for talking from inside of the sandbox iframe -// The external API is in sframe-ctrl.js -define([], function () { - var iframe; +// This file provides the API for the channel for talking to and from the sandbox iframe. +define([ + '/common/sframe-protocol.js' +], function (SFrameProtocol) { + var otherWindow; var handlers = {}; var queries = {}; + + // list of handlers which are registered from the other side... + var insideHandlers = []; + var callWhenRegistered = {}; + var module = { exports: {} }; var mkTxid = function () { return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', ''); }; + module.exports.init = function (ow, cb) { + if (otherWindow) { throw new Error('already initialized'); } + var intr; + var txid; + window.addEventListener('message', function (msg) { + var data = JSON.parse(msg.data); + if (ow !== msg.source) { + console.log("DROP Message from unexpected source"); + console.log(msg); + } else if (!otherWindow) { + if (data.txid !== txid) { + console.log("DROP Message with weird txid"); + return; + } + clearInterval(intr); + otherWindow = ow; + cb(); + } else if (typeof(data.q) === 'string' && handlers[data.q]) { + handlers[data.q](data, msg); + } else if (typeof(data.q) === 'undefined' && queries[data.txid]) { + queries[data.txid](data, msg); + } else if (data.txid === txid) { + // stray message from init + return; + } else { + console.log("DROP Unhandled message"); + console.log(msg); + } + }); + if (window !== window.top) { + // we're in the sandbox + otherWindow = ow; + cb(); + } else { + require(['/common/requireconfig.js'], function (RequireConfig) { + txid = mkTxid(); + intr = setInterval(function () { + ow.postMessage(JSON.stringify({ + txid: txid, + content: { requireConf: RequireConfig }, + q: 'INIT' + }), '*'); + }); + }); + } + }; + module.exports.query = function (q, content, cb) { - if (!iframe) { throw new Error('not yet initialized'); } + if (!otherWindow) { throw new Error('not yet initialized'); } + if (!SFrameProtocol[q]) { + throw new Error('please only make queries are defined in sframe-protocol.js'); + } var txid = mkTxid(); var timeout = setTimeout(function () { delete queries[txid]; - cb("Timeout making query " + q); - }); + console.log("Timeout making query " + q); + }, 30000); queries[txid] = function (data, msg) { clearTimeout(timeout); delete queries[txid]; cb(undefined, data.content, msg); }; - iframe.contentWindow.postMessage(JSON.stringify({ + otherWindow.postMessage(JSON.stringify({ txid: txid, content: content, q: q }), '*'); }; - module.exports.registerHandler = function (queryType, handler) { + var event = module.exports.event = function (e, content) { + if (!otherWindow) { throw new Error('not yet initialized'); } + if (!SFrameProtocol[e]) { + throw new Error('please only fire events that are defined in sframe-protocol.js'); + } + if (e.indexOf('EV_') !== 0) { + throw new Error('please only use events (starting with EV_) for event messages'); + } + otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*'); + }; + + module.exports.on = function (queryType, handler) { + if (!otherWindow) { throw new Error('not yet initialized'); } if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } - handlers[queryType] = function (msg) { - var data = JSON.parse(msg.data); + if (!SFrameProtocol[queryType]) { + throw new Error('please only register handlers which are defined in sframe-protocol.js'); + } + handlers[queryType] = function (data, msg) { handler(data.content, function (replyContent) { msg.source.postMessage(JSON.stringify({ txid: data.txid, @@ -40,7 +110,28 @@ define([], function () { }), '*'); }, msg); }; + event('EV_REGISTER_HANDLER', queryType); + }; + + module.exports.whenReg = function (queryType, handler) { + if (!otherWindow) { throw new Error('not yet initialized'); } + if (!SFrameProtocol[queryType]) { + throw new Error('please only register handlers which are defined in sframe-protocol.js'); + } + if (insideHandlers.indexOf(queryType) > -1) { + handler(); + } else { + (callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(handler); + } + }; + + handlers['EV_REGISTER_HANDLER'] = function (data) { + if (callWhenRegistered[data.content]) { + callWhenRegistered[data.content].forEach(function (f) { f(); }); + delete callWhenRegistered[data.content]; + } + insideHandlers.push(data.content); }; return module.exports; -}); \ No newline at end of file +}); diff --git a/www/common/sframe-ctrl.js b/www/common/sframe-ctrl.js deleted file mode 100644 index ebae43985..000000000 --- a/www/common/sframe-ctrl.js +++ /dev/null @@ -1,76 +0,0 @@ -// This file provides the external API for launching and talking to the sandboxed iframe. -// The internal API is in sframe-channel.js -define([ - '/common/requireconfig.js' -], function (RequireConfig) { - var iframe; - var handlers = {}; - var queries = {}; - var module = { exports: {} }; - - var mkTxid = function () { - return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', ''); - }; - - module.exports.init = function (frame, cb) { - if (iframe) { throw new Error('already initialized'); } - var txid = mkTxid(); - var intr = setInterval(function () { - frame.contentWindow.postMessage(JSON.stringify({ - txid: txid, - content: { requireConf: RequireConfig }, - q: 'INIT' - }), '*'); - }); - window.addEventListener('message', function (msg) { - var data = JSON.parse(msg.data); - if (!iframe) { - if (data.txid !== txid) { return; } - clearInterval(intr); - iframe = frame; - cb(); - } else if (typeof(data.q) === 'string' && handlers[data.q]) { - handlers[data.q](data, msg); - } else if (typeof(data.q) === 'undefined' && queries[data.txid]) { - queries[data.txid](data, msg); - } else { - console.log("Unhandled message"); - console.log(msg); - } - }); - }; - - module.exports.query = function (q, content, cb) { - if (!iframe) { throw new Error('not yet initialized'); } - var txid = mkTxid(); - var timeout = setTimeout(function () { - delete queries[txid]; - cb("Timeout making query " + q); - }); - queries[txid] = function (data, msg) { - clearTimeout(timeout); - delete queries[txid]; - cb(undefined, data.content, msg); - }; - iframe.contentWindow.postMessage(JSON.stringify({ - txid: txid, - content: content, - q: q - }), '*'); - }; - - module.exports.registerHandler = function (queryType, handler) { - if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } - handlers[queryType] = function (msg) { - var data = JSON.parse(msg.data); - handler(data.content, function (replyContent) { - msg.source.postMessage(JSON.stringify({ - txid: data.txid, - content: replyContent - }), '*'); - }, msg); - }; - }; - - return module.exports; -}); diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index 56d32601a..503dc2983 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -1,5 +1,33 @@ -// This file defines all of the RPC calls -// The internal API is in sframe-channel.js +// This file defines all of the RPC calls which are used between the inner and outer iframe. +// Define *querys* (which expect a response) using Q_ +// Define *events* (which expect no response) using EV_ +// Please document the queries and events you create, and please please avoid making generic +// "do stuff" events/queries which are used for many different things because it makes the +// protocol unclear. define({ + // When the iframe first launches, this query is sent repeatedly by the controller + // to wait for it to awake and give it the requirejs config to use. + 'Q_INIT': true, + // When either the outside or inside registers a query handler, this is sent. + 'EV_REGISTER_HANDLER': true, + + // Realtime events called from the outside. + // When someone joins the pad, argument is a string with their netflux id. + 'EV_RT_JOIN': true, + // When someone leaves the pad, argument is a string with their netflux id. + 'EV_RT_LEAVE': true, + // When you have been disconnected, no arguments. + 'EV_RT_DISCONNECT': true, + // When you have connected, argument is an object with myID: string, members: list, readOnly: boolean. + 'EV_RT_CONNECT': true, + // Called after the history is finished synchronizing, no arguments. + 'EV_RT_READY': true, + // Called from both outside and inside, argument is a (string) chainpad message. + 'Q_RT_MESSAGE': true, + + // Called from the outside, this informs the inside whenever the user's data has been changed. + // The argument is the object representing the content of the user profile minus the netfluxID + // which changes per-reconnect. + 'EV_USERDATA_UPDATE': true }); \ No newline at end of file diff --git a/www/pad2/main.js b/www/pad2/main.js index ce8db2010..bf92010e8 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -13,6 +13,7 @@ define([ '/common/cryptpad-common.js', '/common/cryptget.js', '/pad/links.js', + '/bower_components/nthen/index.js', '/bower_components/file-saver/FileSaver.min.js', '/bower_components/diff-dom/diffDOM.js', @@ -21,18 +22,12 @@ define([ 'less!/customize/src/less/cryptpad.less', 'less!/customize/src/less/toolbar.less' ], function ($, Crypto, realtimeInput, Hyperjson, - Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links) { + Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links, nThen) { var saveAs = window.saveAs; var Messages = Cryptpad.Messages; - - console.log('two'); - - var Ckeditor; // to be initialized later... var DiffDom = window.diffDOM; - var stringify = function (obj) { - return JSONSortify(obj); - }; + var stringify = function (obj) { return JSONSortify(obj); }; window.Toolbar = Toolbar; window.Hyperjson = Hyperjson; @@ -89,7 +84,7 @@ define([ Cryptpad.errorLoadingScreen(Messages.websocketError); }; - var andThen = function (Ckeditor) { + var andThen = function (editor) { //var $iframe = $('#pad-iframe').contents(); //var secret = Cryptpad.getSecrets(); //var readOnly = secret.keys && !secret.keys.editKeyStr; @@ -98,645 +93,665 @@ define([ //} var readOnly = false; // TODO - var editor = window.editor = Ckeditor.replace('editor1', { - customConfig: '/customize/ckeditor-config.js', - }); - - editor.on('instanceReady', Links.addSupportForOpeningLinksInNewTab(Ckeditor)); - editor.on('instanceReady', function () { - var $bar = $('#cke_1_toolbox'); - - var $html = $bar.closest('html'); - var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); - if ($faLink.length) { - $html.find('iframe').contents().find('head').append($faLink.clone()); - } - var isHistoryMode = false; - - if (readOnly) { - $('#cke_1_toolbox > .cke_toolbox_main').hide(); - } - - /* add a class to the magicline plugin so we can pick it out more easily */ - var ml = window.CKEDITOR.instances.editor1.plugins.magicline.backdoor.that.line.$; + var $bar = $('#cke_1_toolbox'); - [ml, ml.parentElement].forEach(function (el) { - el.setAttribute('class', 'non-realtime'); - }); + var $html = $bar.closest('html'); + var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); + if ($faLink.length) { + $html.find('iframe').contents().find('head').append($faLink.clone()); + } + var isHistoryMode = false; - var documentBody = document.body; + if (readOnly) { + $('#cke_1_toolbox > .cke_toolbox_main').hide(); + } - var inner = window.inner = documentBody; + /* add a class to the magicline plugin so we can pick it out more easily */ - // hide all content until the realtime doc is ready - $(inner).css({ - color: '#fff', - }); + var ml = window.CKEDITOR.instances.editor1.plugins.magicline.backdoor.that.line.$; - var cursor = module.cursor = Cursor(inner); + [ml, ml.parentElement].forEach(function (el) { + el.setAttribute('class', 'non-realtime'); + }); - var setEditable = module.setEditable = function (bool) { - if (bool) { - $(inner).css({ - color: '#333', - }); - } - if (!readOnly || !bool) { - inner.setAttribute('contenteditable', bool); - } - }; + var documentBody = $html.find('iframe')[0].contentWindow.document.body; - // don't let the user edit until the pad is ready - setEditable(false); + var inner = window.inner = documentBody; - var forbiddenTags = [ - 'SCRIPT', - 'IFRAME', - 'OBJECT', - 'APPLET', - 'VIDEO', - 'AUDIO' - ]; - - var diffOptions = { - preDiffApply: function (info) { - /* - Don't accept attributes that begin with 'on' - these are probably listeners, and we don't want to - send scripts over the wire. - */ - if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) { - if (info.diff.name === 'href') { - // console.log(info.diff); - //var href = info.diff.newValue; - - // TODO normalize HTML entities - if (/javascript *: */.test(info.diff.newValue)) { - // TODO remove javascript: links - } - } + var cursor = module.cursor = Cursor(inner); - if (/^on/.test(info.diff.name)) { - console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name); - return true; - } - } - /* - Also reject any elements which would insert any one of - our forbidden tag types: script, iframe, object, - applet, video, or audio - */ - if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) { - if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) { - console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName); - return true; - } else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) { - console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName); - return true; + var setEditable = module.setEditable = function (bool) { + if (bool) { + $(inner).css({ + color: '#333', + }); + } + if (!readOnly || !bool) { + inner.setAttribute('contenteditable', bool); + } + }; + + // don't let the user edit until the pad is ready + setEditable(false); + + var forbiddenTags = [ + 'SCRIPT', + 'IFRAME', + 'OBJECT', + 'APPLET', + 'VIDEO', + 'AUDIO' + ]; + + var diffOptions = { + preDiffApply: function (info) { + /* + Don't accept attributes that begin with 'on' + these are probably listeners, and we don't want to + send scripts over the wire. + */ + if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) { + if (info.diff.name === 'href') { + // console.log(info.diff); + //var href = info.diff.newValue; + + // TODO normalize HTML entities + if (/javascript *: */.test(info.diff.newValue)) { + // TODO remove javascript: links } } - if (info.node && info.node.tagName === 'BODY') { - if (info.diff.action === 'removeAttribute' && - ['class', 'spellcheck'].indexOf(info.diff.name) !== -1) { - return true; - } + if (/^on/.test(info.diff.name)) { + console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name); + return true; + } + } + /* + Also reject any elements which would insert any one of + our forbidden tag types: script, iframe, object, + applet, video, or audio + */ + if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) { + if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) { + console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName); + return true; + } else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) { + console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName); + return true; } + } - /* DiffDOM will filter out magicline plugin elements - in practice this will make it impossible to use it - while someone else is typing, which could be annoying. - - we should check when such an element is going to be - removed, and prevent that from happening. */ - if (info.node && info.node.tagName === 'SPAN' && - info.node.getAttribute('contentEditable') === "false") { - // it seems to be a magicline plugin element... - if (info.diff.action === 'removeElement') { - // and you're about to remove it... - // this probably isn't what you want - - /* - I have never seen this in the console, but the - magic line is still getting removed on remote - edits. This suggests that it's getting removed - by something other than diffDom. - */ - console.log("preventing removal of the magic line!"); - - // return true to prevent diff application - return true; - } + if (info.node && info.node.tagName === 'BODY') { + if (info.diff.action === 'removeAttribute' && + ['class', 'spellcheck'].indexOf(info.diff.name) !== -1) { + return true; } + } - // Do not change the contenteditable value in view mode - if (readOnly && info.node && info.node.tagName === 'BODY' && - info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') { + /* DiffDOM will filter out magicline plugin elements + in practice this will make it impossible to use it + while someone else is typing, which could be annoying. + + we should check when such an element is going to be + removed, and prevent that from happening. */ + if (info.node && info.node.tagName === 'SPAN' && + info.node.getAttribute('contentEditable') === "false") { + // it seems to be a magicline plugin element... + if (info.diff.action === 'removeElement') { + // and you're about to remove it... + // this probably isn't what you want + + /* + I have never seen this in the console, but the + magic line is still getting removed on remote + edits. This suggests that it's getting removed + by something other than diffDom. + */ + console.log("preventing removal of the magic line!"); + + // return true to prevent diff application return true; } + } - // no use trying to recover the cursor if it doesn't exist - if (!cursor.exists()) { return; } + // Do not change the contenteditable value in view mode + if (readOnly && info.node && info.node.tagName === 'BODY' && + info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') { + return true; + } - /* frame is either 0, 1, 2, or 3, depending on which - cursor frames were affected: none, first, last, or both - */ - var frame = info.frame = cursor.inNode(info.node); + // no use trying to recover the cursor if it doesn't exist + if (!cursor.exists()) { return; } - if (!frame) { return; } + /* frame is either 0, 1, 2, or 3, depending on which + cursor frames were affected: none, first, last, or both + */ + var frame = info.frame = cursor.inNode(info.node); - if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') { - var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue); + if (!frame) { return; } - if (frame & 1) { - // push cursor start if necessary - if (pushes.commonStart < cursor.Range.start.offset) { - cursor.Range.start.offset += pushes.delta; - } - } - if (frame & 2) { - // push cursor end if necessary - if (pushes.commonStart < cursor.Range.end.offset) { - cursor.Range.end.offset += pushes.delta; - } + if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') { + var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue); + + if (frame & 1) { + // push cursor start if necessary + if (pushes.commonStart < cursor.Range.start.offset) { + cursor.Range.start.offset += pushes.delta; } } - }, - postDiffApply: function (info) { - if (info.frame) { - if (info.node) { - if (info.frame & 1) { cursor.fixStart(info.node); } - if (info.frame & 2) { cursor.fixEnd(info.node); } - } else { console.error("info.node did not exist"); } - - var sel = cursor.makeSelection(); - var range = cursor.makeRange(); - - cursor.fixSelection(sel, range); + if (frame & 2) { + // push cursor end if necessary + if (pushes.commonStart < cursor.Range.end.offset) { + cursor.Range.end.offset += pushes.delta; + } } } - }; + }, + postDiffApply: function (info) { + if (info.frame) { + if (info.node) { + if (info.frame & 1) { cursor.fixStart(info.node); } + if (info.frame & 2) { cursor.fixEnd(info.node); } + } else { console.error("info.node did not exist"); } + + var sel = cursor.makeSelection(); + var range = cursor.makeRange(); + + cursor.fixSelection(sel, range); + } + } + }; - var initializing = true; + var initializing = true; - var Title; - var UserList; - var Metadata; + var Title; + var UserList; + var Metadata; - var getHeadingText = function () { - var text; - if (['h1', 'h2', 'h3'].some(function (t) { - var $header = $(inner).find(t + ':first-of-type'); - if ($header.length && $header.text()) { - text = $header.text(); - return true; - } - })) { return text; } - }; + var getHeadingText = function () { + var text; + if (['h1', 'h2', 'h3'].some(function (t) { + var $header = $(inner).find(t + ':first-of-type'); + if ($header.length && $header.text()) { + text = $header.text(); + return true; + } + })) { return text; } + }; - var DD = new DiffDom(diffOptions); + var DD = new DiffDom(diffOptions); - var openLink = function (e) { - var el = e.currentTarget; - if (!el || el.nodeName !== 'A') { return; } - var href = el.getAttribute('href'); - if (href) { window.open(href, '_blank'); } - }; + var openLink = function (e) { + var el = e.currentTarget; + if (!el || el.nodeName !== 'A') { return; } + var href = el.getAttribute('href'); + if (href) { window.open(href, '_blank'); } + }; - // apply patches, and try not to lose the cursor in the process! - var applyHjson = function (shjson) { - var userDocStateDom = hjsonToDom(JSON.parse(shjson)); + // apply patches, and try not to lose the cursor in the process! + var applyHjson = function (shjson) { + var userDocStateDom = hjsonToDom(JSON.parse(shjson)); - if (!readOnly && !initializing) { - userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf - } - var patch = (DD).diff(inner, userDocStateDom); - (DD).apply(inner, patch); - if (readOnly) { - var $links = $(inner).find('a'); - // off so that we don't end up with multiple identical handlers - $links.off('click', openLink).on('click', openLink); - } - }; + if (!readOnly && !initializing) { + userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf + } + var patch = (DD).diff(inner, userDocStateDom); + (DD).apply(inner, patch); + if (readOnly) { + var $links = $(inner).find('a'); + // off so that we don't end up with multiple identical handlers + $links.off('click', openLink).on('click', openLink); + } + }; - var stringifyDOM = module.stringifyDOM = function (dom) { - var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); - hjson[3] = { - metadata: { - users: UserList.userData, - defaultTitle: Title.defaultTitle, - type: 'pad' - } - }; - if (!initializing) { - hjson[3].metadata.title = Title.title; - } else if (Cryptpad.initialName && !hjson[3].metadata.title) { - hjson[3].metadata.title = Cryptpad.initialName; + var stringifyDOM = module.stringifyDOM = function (dom) { + var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); + + /*hjson[3] = { TODO + users: UserList.userData, + defaultTitle: Title.defaultTitle, + type: 'pad' } - return stringify(hjson); - }; + };*/ + if (!initializing) { + //TODO hjson[3].metadata.title = Title.title; + } else if (Cryptpad.initialName && !hjson[3].metadata.title) { + hjson[3].metadata.title = Cryptpad.initialName; + } + return stringify(hjson); + }; - var realtimeOptions = { - // the websocket URL - websocketURL: Cryptpad.getWebsocketURL(), + var realtimeOptions = { + // the websocket URL + websocketURL: Cryptpad.getWebsocketURL(), - // the channel we will communicate over - channel: 'x',//secret.channel, + // the channel we will communicate over + channel: 'x',//secret.channel, - // the nework used for the file store if it exists - network: Cryptpad.getNetwork(), + // the nework used for the file store if it exists + network: Cryptpad.getNetwork(), - // our public key - validateKey: undefined,//secret.keys.validateKey || undefined, - readOnly: readOnly, + // our public key + validateKey: undefined,//secret.keys.validateKey || undefined, + readOnly: readOnly, - // Pass in encrypt and decrypt methods - crypto: undefined,//Crypto.createEncryptor(secret.keys), + // Pass in encrypt and decrypt methods + crypto: undefined,//Crypto.createEncryptor(secret.keys), - // really basic operational transform - transformFunction : JsonOT.validate, + // really basic operational transform + transformFunction : JsonOT.validate, - // cryptpad debug logging (default is 1) - // logLevel: 0, + // cryptpad debug logging (default is 1) + // logLevel: 0, - validateContent: function (content) { - try { - JSON.parse(content); - return true; - } catch (e) { - console.log("Failed to parse, rejecting patch"); - return false; - } + validateContent: function (content) { + try { + JSON.parse(content); + return true; + } catch (e) { + console.log("Failed to parse, rejecting patch"); + return false; } - }; + } + }; - var setHistory = function (bool, update) { - isHistoryMode = bool; - setEditable(!bool); - if (!bool && update) { - realtimeOptions.onRemote(); - } - }; + var setHistory = function (bool, update) { + isHistoryMode = bool; + setEditable(!bool); + if (!bool && update) { + realtimeOptions.onRemote(); + } + }; - realtimeOptions.onRemote = function () { - if (initializing) { return; } - if (isHistoryMode) { return; } + var meta; + var metaStr; - var oldShjson = stringifyDOM(inner); + realtimeOptions.onRemote = function () { + if (initializing) { return; } + if (isHistoryMode) { return; } - var shjson = module.realtime.getUserDoc(); + var oldShjson = stringifyDOM(inner); - // remember where the cursor is - cursor.update(); + var shjson = module.realtime.getUserDoc(); - // Update the user list (metadata) from the hyperjson - Metadata.update(shjson); + // remember where the cursor is + cursor.update(); - var newInner = JSON.parse(shjson); - var newSInner; - if (newInner.length > 2) { - newSInner = stringify(newInner[2]); - } + // Update the user list (metadata) from the hyperjson + // TODO Metadata.update(shjson); - // build a dom from HJSON, diff, and patch the editor - applyHjson(shjson); + var newInner = JSON.parse(shjson); + var newSInner; + if (newInner.length > 2) { + newSInner = stringify(newInner[2]); + } - if (!readOnly) { - var shjson2 = stringifyDOM(inner); - if (shjson2 !== shjson) { - console.error("shjson2 !== shjson"); - module.patchText(shjson2); - - /* pushing back over the wire is necessary, but it can - result in a feedback loop, which we call a browser - fight */ - if (module.logFights) { - // what changed? - var op = TextPatcher.diff(shjson, shjson2); - // log the changes - TextPatcher.log(shjson, op); - var sop = JSON.stringify(TextPatcher.format(shjson, op)); - - var index = module.fights.indexOf(sop); - if (index === -1) { - module.fights.push(sop); - console.log("Found a new type of browser disagreement"); - console.log("You can inspect the list in your " + - "console at `REALTIME_MODULE.fights`"); - console.log(module.fights); - } else { - console.log("Encountered a known browser disagreement: " + - "available at `REALTIME_MODULE.fights[%s]`", index); - } + // build a dom from HJSON, diff, and patch the editor + applyHjson(shjson); + + if (!readOnly) { + var shjson2 = stringifyDOM(inner); + + // TODO + //shjson = JSON.stringify(JSON.parse(shjson).slice(0,3)); + + if (shjson2 !== shjson) { + console.error("shjson2 !== shjson"); + module.patchText(shjson2); + + /* pushing back over the wire is necessary, but it can + result in a feedback loop, which we call a browser + fight */ + if (module.logFights) { + // what changed? + var op = TextPatcher.diff(shjson, shjson2); + // log the changes + TextPatcher.log(shjson, op); + var sop = JSON.stringify(TextPatcher.format(shjson, op)); + + var index = module.fights.indexOf(sop); + if (index === -1) { + module.fights.push(sop); + console.log("Found a new type of browser disagreement"); + console.log("You can inspect the list in your " + + "console at `REALTIME_MODULE.fights`"); + console.log(module.fights); + } else { + console.log("Encountered a known browser disagreement: " + + "available at `REALTIME_MODULE.fights[%s]`", index); } } } + } - // Notify only when the content has changed, not when someone has joined/left - var oldSInner = stringify(JSON.parse(oldShjson)[2]); - if (newSInner && newSInner !== oldSInner) { - Cryptpad.notify(); - } - }; - - var getHTML = function () { - return ('\n' + '\n' + inner.innerHTML); - }; + // Notify only when the content has changed, not when someone has joined/left + var oldSInner = stringify(JSON.parse(oldShjson)[2]); + if (newSInner && newSInner !== oldSInner) { + Cryptpad.notify(); + } - var domFromHTML = function (html) { - return new DOMParser().parseFromString(html, 'text/html'); - }; + var newMeta = newInner[3]; + var newMetaStr = JSON.stringify(newMeta); + if (newMetaStr !== metaStr) { + metaStr = newMetaStr; + meta = newMeta; + //meta[] HERE + } + }; + + var getHTML = function () { + return ('\n' + '\n' + inner.innerHTML); + }; + + var domFromHTML = function (html) { + return new DOMParser().parseFromString(html, 'text/html'); + }; + + var exportFile = function () { + var html = getHTML(); + var suggestion = Title.suggestTitle('cryptpad-document'); + Cryptpad.prompt(Messages.exportPrompt, + Cryptpad.fixFileName(suggestion) + '.html', function (filename) { + if (!(typeof(filename) === 'string' && filename)) { return; } + var blob = new Blob([html], {type: "text/html;charset=utf-8"}); + saveAs(blob, filename); + }); + }; + var importFile = function (content) { + var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body)); + applyHjson(shjson); + realtimeOptions.onLocal(); + }; - var exportFile = function () { - var html = getHTML(); - var suggestion = Title.suggestTitle('cryptpad-document'); - Cryptpad.prompt(Messages.exportPrompt, - Cryptpad.fixFileName(suggestion) + '.html', function (filename) { - if (!(typeof(filename) === 'string' && filename)) { return; } - var blob = new Blob([html], {type: "text/html;charset=utf-8"}); - saveAs(blob, filename); - }); - }; - var importFile = function (content) { - var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body)); - applyHjson(shjson); - realtimeOptions.onLocal(); - }; + realtimeOptions.onInit = function (info) { - realtimeOptions.onInit = function (info) { - UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad); - - var titleCfg = { getHeadingText: getHeadingText }; - Title = Cryptpad.createTitle(titleCfg, realtimeOptions.onLocal, Cryptpad); - - Metadata = Cryptpad.createMetadata(UserList, Title, null, Cryptpad); - - var configTb = { - displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'], - userList: UserList.getToolbarConfig(), - share: { - secret: secret, - channel: info.channel - }, - title: Title.getTitleConfig(), - common: Cryptpad, - readOnly: readOnly, - ifrw: window, - realtime: info.realtime, - network: info.network, - $container: $bar, - $contentContainer: $('#cke_1_contents'), - }; - toolbar = info.realtime.toolbar = Toolbar.create(configTb); - - var src = 'less!/customize/src/less/toolbar.less'; - require([ - src - ], function () { - var $html = $bar.closest('html'); - $html - .find('head style[data-original-src="' + src.replace(/less!/, '') + '"]') - .appendTo($html.find('head')); - }); + // TODO + return; - Title.setToolbar(toolbar); + UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad); - var $rightside = toolbar.$rightside; - var $drawer = toolbar.$drawer; + var titleCfg = { getHeadingText: getHeadingText }; + Title = Cryptpad.createTitle(titleCfg, realtimeOptions.onLocal, Cryptpad); - var editHash; + Metadata = Cryptpad.createMetadata(UserList, Title, null, Cryptpad); - if (!readOnly) { - editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); - } + var configTb = { + displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'], + userList: UserList.getToolbarConfig(), + share: { + secret: secret, + channel: info.channel + }, + title: Title.getTitleConfig(), + common: Cryptpad, + readOnly: readOnly, + ifrw: window, + realtime: info.realtime, + network: info.network, + $container: $bar, + $contentContainer: $('#cke_1_contents'), + }; + toolbar = info.realtime.toolbar = Toolbar.create(configTb); + + var src = 'less!/customize/src/less/toolbar.less'; + require([ + src + ], function () { + var $html = $bar.closest('html'); + $html + .find('head style[data-original-src="' + src.replace(/less!/, '') + '"]') + .appendTo($html.find('head')); + }); - $bar.find('#cke_1_toolbar_collapser').hide(); - if (!readOnly) { - // Expand / collapse the toolbar - var $collapse = Cryptpad.createButton(null, true); - $collapse.removeClass('fa-question'); - var updateIcon = function () { - $collapse.removeClass('fa-caret-down').removeClass('fa-caret-up'); - var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible'); - if (isCollapsed) { - if (!initializing) { Cryptpad.feedback('HIDETOOLBAR_PAD'); } - $collapse.addClass('fa-caret-down'); - } - else { - if (!initializing) { Cryptpad.feedback('SHOWTOOLBAR_PAD'); } - $collapse.addClass('fa-caret-up'); - } - }; - updateIcon(); - $collapse.click(function () { - $(window).trigger('resize'); - $iframe.find('.cke_toolbox_main').toggle(); - $(window).trigger('cryptpad-ck-toolbar'); - updateIcon(); - }); - $rightside.append($collapse); - } + Title.setToolbar(toolbar); - /* add a history button */ - var histConfig = { - onLocal: realtimeOptions.onLocal, - onRemote: realtimeOptions.onRemote, - setHistory: setHistory, - applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); }, - $toolbar: $bar - }; - var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig}); - $drawer.append($hist); - - /* save as template */ - if (!Cryptpad.isTemplate(window.location.href)) { - var templateObj = { - rt: info.realtime, - Crypt: Cryptget, - getTitle: function () { return document.title; } - }; - var $templateButton = Cryptpad.createButton('template', true, templateObj); - $rightside.append($templateButton); - } + var $rightside = toolbar.$rightside; + var $drawer = toolbar.$drawer; - /* add an export button */ - var $export = Cryptpad.createButton('export', true, {}, exportFile); - $drawer.append($export); + var editHash; - if (!readOnly) { - /* add an import button */ - var $import = Cryptpad.createButton('import', true, { - accept: 'text/html' - }, importFile); - $drawer.append($import); - } + if (!readOnly) { + editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); + } - /* add a forget button */ - var forgetCb = function (err) { - if (err) { return; } - setEditable(false); + $bar.find('#cke_1_toolbar_collapser').hide(); + if (!readOnly) { + // Expand / collapse the toolbar + var $collapse = Cryptpad.createButton(null, true); + $collapse.removeClass('fa-question'); + var updateIcon = function () { + $collapse.removeClass('fa-caret-down').removeClass('fa-caret-up'); + var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible'); + if (isCollapsed) { + if (!initializing) { Cryptpad.feedback('HIDETOOLBAR_PAD'); } + $collapse.addClass('fa-caret-down'); + } + else { + if (!initializing) { Cryptpad.feedback('SHOWTOOLBAR_PAD'); } + $collapse.addClass('fa-caret-up'); + } }; - var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb); - $rightside.append($forgetPad); + updateIcon(); + $collapse.click(function () { + $(window).trigger('resize'); + $('.cke_toolbox_main').toggle(); + $(window).trigger('cryptpad-ck-toolbar'); + updateIcon(); + }); + $rightside.append($collapse); + } - // set the hash - if (!readOnly) { Cryptpad.replaceHash(editHash); } + /* add a history button */ + var histConfig = { + onLocal: realtimeOptions.onLocal, + onRemote: realtimeOptions.onRemote, + setHistory: setHistory, + applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); }, + $toolbar: $bar }; + var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig}); + $drawer.append($hist); + + /* save as template */ + if (!Cryptpad.isTemplate(window.location.href)) { + var templateObj = { + rt: info.realtime, + Crypt: Cryptget, + getTitle: function () { return document.title; } + }; + var $templateButton = Cryptpad.createButton('template', true, templateObj); + $rightside.append($templateButton); + } - // this should only ever get called once, when the chain syncs - realtimeOptions.onReady = function (info) { - if (!module.isMaximized) { - module.isMaximized = true; - $iframe.find('iframe.cke_wysiwyg_frame').css('width', ''); - $iframe.find('iframe.cke_wysiwyg_frame').css('height', ''); - } - $iframe.find('body').addClass('app-pad'); - - if (module.realtime !== info.realtime) { - module.patchText = TextPatcher.create({ - realtime: info.realtime, - //logging: true, - }); - } + /* add an export button */ + var $export = Cryptpad.createButton('export', true, {}, exportFile); + $drawer.append($export); - module.realtime = info.realtime; + if (!readOnly) { + /* add an import button */ + var $import = Cryptpad.createButton('import', true, { + accept: 'text/html' + }, importFile); + $drawer.append($import); + } - var shjson = module.realtime.getUserDoc(); + /* add a forget button */ + var forgetCb = function (err) { + if (err) { return; } + setEditable(false); + }; + var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb); + $rightside.append($forgetPad); + + // set the hash + if (!readOnly) { Cryptpad.replaceHash(editHash); } + }; + + // this should only ever get called once, when the chain syncs + realtimeOptions.onReady = function (info) { + if (!module.isMaximized) { + module.isMaximized = true; + $('iframe.cke_wysiwyg_frame').css('width', ''); + $('iframe.cke_wysiwyg_frame').css('height', ''); + } + $('body').addClass('app-pad'); - var newPad = false; - if (shjson === '') { newPad = true; } + if (module.realtime !== info.realtime) { + module.patchText = TextPatcher.create({ + realtime: info.realtime, + //logging: true, + }); + } - if (!newPad) { - applyHjson(shjson); + module.realtime = info.realtime; - // Update the user list (metadata) from the hyperjson - Metadata.update(shjson); + var shjson = module.realtime.getUserDoc(); - if (!readOnly) { - var shjson2 = stringifyDOM(inner); - var hjson2 = JSON.parse(shjson2).slice(0,-1); - var hjson = JSON.parse(shjson).slice(0,-1); - if (stringify(hjson2) !== stringify(hjson)) { - console.log('err'); - console.error("shjson2 !== shjson"); - Cryptpad.errorLoadingScreen(Messages.wrongApp); - throw new Error(); - } - } - } else { - Title.updateTitle(Cryptpad.initialName || Title.defaultTitle); - documentBody.innerHTML = Messages.initialState; - } + var newPad = false; + if (shjson === '') { newPad = true; } - Cryptpad.removeLoadingScreen(emitResize); - setEditable(!readOnly); - initializing = false; - - if (readOnly) { return; } - UserList.getLastName(toolbar.$userNameButton, newPad); - editor.focus(); - if (newPad) { - cursor.setToEnd(); - } else { - cursor.setToStart(); - } - }; + if (!newPad) { + applyHjson(shjson); - realtimeOptions.onAbort = function () { - console.log("Aborting the session!"); - // stop the user from continuing to edit - setEditable(false); - toolbar.failed(); - Cryptpad.alert(Messages.common_connectionLost, undefined, true); - }; + // Update the user list (metadata) from the hyperjson + // XXX Metadata.update(shjson); - realtimeOptions.onConnectionChange = function (info) { - setEditable(info.state); - toolbar.failed(); - if (info.state) { - initializing = true; - toolbar.reconnecting(info.myId); - Cryptpad.findOKButton().click(); - } else { - Cryptpad.alert(Messages.common_connectionLost, undefined, true); + if (!readOnly) { + var shjson2 = stringifyDOM(inner); + var hjson2 = JSON.parse(shjson2).slice(0,3); + var hjson = JSON.parse(shjson).slice(0,3); + if (stringify(hjson2) !== stringify(hjson)) { + console.log('err'); + console.error("shjson2 !== shjson"); + console.log(stringify(hjson2)); + console.log(stringify(hjson)); + Cryptpad.errorLoadingScreen(Messages.wrongApp); + throw new Error(); + } } - }; - - realtimeOptions.onError = onConnectError; + } else { + Title.updateTitle(Cryptpad.initialName || Title.defaultTitle); + documentBody.innerHTML = Messages.initialState; + } - var onLocal = realtimeOptions.onLocal = function () { - if (initializing) { return; } - if (isHistoryMode) { return; } - if (readOnly) { return; } + Cryptpad.removeLoadingScreen(emitResize); + setEditable(!readOnly); + initializing = false; - // stringify the json and send it into chainpad - var shjson = stringifyDOM(inner); + if (readOnly) { return; } + //TODO UserList.getLastName(toolbar.$userNameButton, newPad); + editor.focus(); + if (newPad) { + cursor.setToEnd(); + } else { + cursor.setToStart(); + } + }; +/* unreachable + realtimeOptions.onAbort = function () { + console.log("Aborting the session!"); + // stop the user from continuing to edit + setEditable(false); + toolbar.failed(); + Cryptpad.alert(Messages.common_connectionLost, undefined, true); + }; */ + + realtimeOptions.onConnectionChange = function (info) { + setEditable(info.state); + toolbar.failed(); + if (info.state) { + initializing = true; + toolbar.reconnecting(info.myId); + Cryptpad.findOKButton().click(); + } else { + Cryptpad.alert(Messages.common_connectionLost, undefined, true); + } + }; - module.patchText(shjson); - if (module.realtime.getUserDoc() !== shjson) { - console.error("realtime.getUserDoc() !== shjson"); - } - }; + realtimeOptions.onError = onConnectError; - module.realtimeInput = realtimeInput.start(realtimeOptions); - - Cryptpad.onLogout(function () { setEditable(false); }); - - /* hitting enter makes a new line, but places the cursor inside - of the
instead of the

. This makes it such that you - cannot type until you click, which is rather unnacceptable. - If the cursor is ever inside such a
, you probably want - to push it out to the parent element, which ought to be a - paragraph tag. This needs to be done on keydown, otherwise - the first such keypress will not be inserted into the P. */ - inner.addEventListener('keydown', cursor.brFix); - - editor.on('change', onLocal); - - // export the typing tests to the window. - // call like `test = easyTest()` - // terminate the test like `test.cancel()` - window.easyTest = function () { - cursor.update(); - var start = cursor.Range.start; - var test = TypingTest.testInput(inner, start.el, start.offset, onLocal); - onLocal(); - return test; - }; + var onLocal = realtimeOptions.onLocal = function () { + if (initializing) { return; } + if (isHistoryMode) { return; } + if (readOnly) { return; } - $bar.find('.cke_button').click(function () { - var e = this; - var classString = e.getAttribute('class'); - var classes = classString.split(' ').filter(function (c) { - return /cke_button__/.test(c); - }); + // stringify the json and send it into chainpad + var shjson = stringifyDOM(inner); - var id = classes[0]; - if (typeof(id) === 'string') { - Cryptpad.feedback(id.toUpperCase()); - } + module.patchText(shjson); + if (module.realtime.getUserDoc() !== shjson) { + console.error("realtime.getUserDoc() !== shjson"); + } + }; + + module.realtimeInput = realtimeInput.start(realtimeOptions); + + Cryptpad.onLogout(function () { setEditable(false); }); + + /* hitting enter makes a new line, but places the cursor inside + of the
instead of the

. This makes it such that you + cannot type until you click, which is rather unnacceptable. + If the cursor is ever inside such a
, you probably want + to push it out to the parent element, which ought to be a + paragraph tag. This needs to be done on keydown, otherwise + the first such keypress will not be inserted into the P. */ + inner.addEventListener('keydown', cursor.brFix); + + editor.on('change', onLocal); + + // export the typing tests to the window. + // call like `test = easyTest()` + // terminate the test like `test.cancel()` + window.easyTest = function () { + cursor.update(); + var start = cursor.Range.start; + var test = TypingTest.testInput(inner, start.el, start.offset, onLocal); + onLocal(); + return test; + }; + + $bar.find('.cke_button').click(function () { + var e = this; + var classString = e.getAttribute('class'); + var classes = classString.split(' ').filter(function (c) { + return /cke_button__/.test(c); }); + + var id = classes[0]; + if (typeof(id) === 'string') { + Cryptpad.feedback(id.toUpperCase()); + } }); }; - var interval = 100; - var second = function (Ckeditor) { - //Cryptpad.ready(function () { - andThen(Ckeditor); - //Cryptpad.reportAppUsage(); - //}); - Cryptpad.onError(function (info) { - if (info && info.type === "store") { - onConnectError(); + + var CKEDITOR_CHECK_INTERVAL = 100; + var ckEditorAvailable = function (cb) { + var intr; + var check = function () { + if (window.CKEDITOR) { + clearTimeout(intr); + cb(window.CKEDITOR); } - }); + }; + intr = setInterval(function () { + console.log("Ckeditor was not defined. Trying again in %sms", CKEDITOR_CHECK_INTERVAL); + check(); + }, CKEDITOR_CHECK_INTERVAL); + check(); }; - var first = function () { - Ckeditor = window.CKEDITOR; - if (Ckeditor) { - // mobile configuration + var main = function () { + var Ckeditor; + var editor; + + nThen(function (waitFor) { + ckEditorAvailable(waitFor(function (ck) { Ckeditor = ck; })); + $(waitFor(function () { + Cryptpad.addLoadingScreen(); + })); + }).nThen(function (waitFor) { Ckeditor.config.toolbarCanCollapse = true; if (screen.height < 800) { Ckeditor.config.toolbarStartupExpanded = false; @@ -744,15 +759,19 @@ define([ } else { $('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes'); } - second(Ckeditor); - } else { - //console.log("Ckeditor was not defined. Trying again in %sms",interval); - setTimeout(first, interval); - } + editor = Ckeditor.replace('editor1', { + customConfig: '/customize/ckeditor-config.js', + }); + editor.on('instanceReady', waitFor()); + }).nThen(function (waitFor) { + Links.addSupportForOpeningLinksInNewTab(Ckeditor); + Cryptpad.onError(function (info) { + if (info && info.type === "store") { + onConnectError(); + } + }); + andThen(editor); + }); }; - - $(function () { - Cryptpad.addLoadingScreen(); - first(); - }); + main(); }); diff --git a/www/pad2/outer.js b/www/pad2/outer.js index 5c7c11364..8e09a6ea0 100644 --- a/www/pad2/outer.js +++ b/www/pad2/outer.js @@ -1,13 +1,39 @@ define([ - '/common/sframe-ctrl.js', - 'jquery' -], function (SFrameCtrl, $) { + '/common/sframe-channel.js', + 'jquery', + '/common/sframe-chainpad-netflux-outer.js', + '/bower_components/nthen/index.js', + '/common/cryptpad-common.js', + '/bower_components/chainpad-crypto/crypto.js' +], function (SFrameChannel, $, CpNfOuter, nThen, Cryptpad, Crypto) { console.log('xxx'); - $(function () { - console.log('go'); - SFrameCtrl.init($('#sbox-iframe')[0], function () { - console.log('\n\ndone\n\n'); + nThen(function (waitFor) { + $(waitFor()); + }).nThen(function (waitFor) { + SFrameChannel.init($('#sbox-iframe')[0].contentWindow, waitFor(function () { + console.log('sframe initialized'); + })); + Cryptpad.ready(waitFor()); + }).nThen(function (waitFor) { + Cryptpad.onError(function (info) { + console.log('error'); + console.log(info); + if (info && info.type === "store") { + //onConnectError(); + } + }); + }).nThen(function (waitFor) { + var secret = Cryptpad.getSecrets(); + var readOnly = secret.keys && !secret.keys.editKeyStr; + if (!secret.keys) { secret.keys = secret.key; } + + var outer = CpNfOuter.start({ + channel: secret.channel, + network: Cryptpad.getNetwork(), + validateKey: secret.keys.validateKey || undefined, + readOnly: readOnly, + crypto: Crypto.createEncryptor(secret.keys), }); }); }); \ No newline at end of file From c304071492377a0bd87727796bd0b834cf9c7ddd Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Wed, 9 Aug 2017 17:37:55 +0200 Subject: [PATCH 04/63] wip --- www/common/common-userlist2.js | 112 ++++++++++ www/common/metadata-manager.js | 17 ++ www/common/sframe-boot2.js | 9 +- www/common/sframe-chainpad-netflux-inner.js | 24 +- www/common/sframe-chainpad-netflux-outer.js | 24 +- www/common/sframe-channel.js | 168 +++++++------- www/common/sframe-protocol.js | 5 + www/pad2/main.js | 232 ++++++++++---------- www/pad2/outer.js | 9 +- 9 files changed, 360 insertions(+), 240 deletions(-) create mode 100644 www/common/common-userlist2.js diff --git a/www/common/common-userlist2.js b/www/common/common-userlist2.js new file mode 100644 index 000000000..e7f2449fa --- /dev/null +++ b/www/common/common-userlist2.js @@ -0,0 +1,112 @@ +define(function () { + var module = {}; + + module.create = function (info, onLocal, Cryptget, Cryptpad) { + var exp = {}; + + var userData = exp.userData = {}; + var userList = exp.userList = info.userList; + var myData = exp.myData = {}; + exp.myUserName = info.myID; + exp.myNetfluxId = info.myID; + + var network = Cryptpad.getNetwork(); + + var parsed = Cryptpad.parsePadUrl(window.location.href); + var appType = parsed ? parsed.type : undefined; + + var addToUserData = exp.addToUserData = function(data) { + var users = userList.users; + for (var attrname in data) { userData[attrname] = data[attrname]; } + + if (users && users.length) { + for (var userKey in userData) { + if (users.indexOf(userKey) === -1) { + delete userData[userKey]; + } + } + } + + if(userList && typeof userList.onChange === "function") { + userList.onChange(userData); + } + }; + + exp.getToolbarConfig = function () { + return { + data: userData, + list: userList, + userNetfluxId: exp.myNetfluxId + }; + }; + + var setName = exp.setName = function (newName, cb) { + if (typeof(newName) !== 'string') { return; } + var myUserNameTemp = newName.trim(); + if(myUserNameTemp.length > 32) { + myUserNameTemp = myUserNameTemp.substr(0, 32); + } + exp.myUserName = myUserNameTemp; + myData = {}; + myData[exp.myNetfluxId] = { + name: exp.myUserName, + uid: Cryptpad.getUid(), + avatar: Cryptpad.getAvatarUrl(), + profile: Cryptpad.getProfileUrl(), + curvePublic: Cryptpad.getProxy().curvePublic + }; + addToUserData(myData); + /*Cryptpad.setAttribute('username', exp.myUserName, function (err) { + if (err) { + console.log("Couldn't set username"); + console.error(err); + return; + } + if (typeof cb === "function") { cb(); } + });*/ + if (typeof cb === "function") { cb(); } + }; + + exp.getLastName = function ($changeNameButton, isNew) { + Cryptpad.getLastName(function (err, lastName) { + if (err) { + console.log("Could not get previous name"); + console.error(err); + return; + } + // Update the toolbar list: + // Add the current user in the metadata + if (typeof(lastName) === 'string') { + setName(lastName, onLocal); + } else { + myData[exp.myNetfluxId] = { + name: "", + uid: Cryptpad.getUid(), + avatar: Cryptpad.getAvatarUrl(), + profile: Cryptpad.getProfileUrl(), + curvePublic: Cryptpad.getProxy().curvePublic + }; + addToUserData(myData); + onLocal(); + $changeNameButton.click(); + } + if (isNew && appType) { + Cryptpad.selectTemplate(appType, info.realtime, Cryptget); + } + }); + }; + + Cryptpad.onDisplayNameChanged(function (newName) { + setName(newName, onLocal); + }); + + network.on('reconnect', function (uid) { + exp.myNetfluxId = uid; + exp.setName(exp.myUserName); + }); + + return exp; + }; + + return module; +}); diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index e69de29bb..9d26d5588 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -0,0 +1,17 @@ +define([], function () { + var metadataChange = function (ctx, newMeta) { + + }; + var getMetadata = function (ctx) { + + }; + var create = function (sframeChan, cpNfInner) { + var ctx = { + sframeChan: sframeChan, + personalMetadata: {} + }; + + }; + return { create: create }; + +}); \ No newline at end of file diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js index 94ec9d511..8285032d2 100644 --- a/www/common/sframe-boot2.js +++ b/www/common/sframe-boot2.js @@ -1,11 +1,8 @@ // This is stage 1, it can be changed but you must bump the version of the project. // Note: This must only be loaded from inside of a sandbox-iframe. -define([ - '/common/requireconfig.js', - '/common/sframe-channel.js' -], function (RequireConfig, SFrameChannel) { +define(['/common/requireconfig.js'], function (RequireConfig) { require.config(RequireConfig); -console.log('boot2'); + // most of CryptPad breaks if you don't support isArray if (!Array.isArray) { Array.isArray = function(arg) { // CRYPTPAD_SHIM @@ -23,7 +20,5 @@ console.log('boot2'); window.__defineGetter__('localStorage', function () { return mkFakeStore(); }); window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); }); - SFrameChannel.init(window.top, function () { }); - require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]); }); diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index 32e1beb0f..efce47a91 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -15,9 +15,8 @@ * along with this program. If not, see . */ define([ - '/common/sframe-channel.js', '/bower_components/chainpad/chainpad.dist.js', -], function (SFrameChannel) { +], function () { var ChainPad = window.ChainPad; var module = { exports: {} }; @@ -81,6 +80,7 @@ define([ var avgSyncMilliseconds = config.avgSyncMilliseconds; var logLevel = typeof(config.logLevel) !== 'undefined'? config.logLevel : 1; var readOnly = config.readOnly || false; + var sframeChan = config.sframeChan; config = undefined; var chainpad; @@ -88,14 +88,14 @@ define([ var myID; var isReady = false; - SFrameChannel.on('EV_RT_JOIN', userList.onJoin); - SFrameChannel.on('EV_RT_LEAVE', userList.onLeave); - SFrameChannel.on('EV_RT_DISCONNECT', function () { + sframeChan.on('EV_RT_JOIN', userList.onJoin); + sframeChan.on('EV_RT_LEAVE', userList.onLeave); + sframeChan.on('EV_RT_DISCONNECT', function () { isReady = false; userList.onReset(); onConnectionChange({ state: false }); }); - SFrameChannel.on('EV_RT_CONNECT', function (content) { + sframeChan.on('EV_RT_CONNECT', function (content) { content.members.forEach(userList.onJoin); myID = content.myID; isReady = false; @@ -113,26 +113,26 @@ define([ logLevel: logLevel }); chainpad.onMessage(function(message, cb) { - SFrameChannel.query('Q_RT_MESSAGE', message, cb); + sframeChan.query('Q_RT_MESSAGE', message, cb); }); chainpad.onPatch(function () { onRemote({ realtime: chainpad }); }); onInit({ - myID: content.myID, + myID: myID, realtime: chainpad, userList: userList, readOnly: readOnly }); }); - SFrameChannel.on('Q_RT_MESSAGE', function (content, cb) { + sframeChan.on('Q_RT_MESSAGE', function (content, cb) { if (isReady) { onLocal(); // should be onBeforeMessage } chainpad.message(content); cb('OK'); }); - SFrameChannel.on('EV_RT_READY', function () { + sframeChan.on('EV_RT_READY', function () { if (isReady) { return; } isReady = true; chainpad.start(); @@ -141,7 +141,9 @@ define([ if (!readOnly) { userList.onJoin(myID); } onReady({ realtime: chainpad }); }); - return; + return { + getMyID: function () { return myID; } + }; }; return module.exports; }); \ No newline at end of file diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index d28fbe631..078a920e7 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -14,11 +14,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -define([ - '/common/sframe-channel.js', -], function (SFrameChannel) { +define([], function () { var USE_HISTORY = true; - var module = { exports: {} }; var verbose = function (x) { console.log(x); }; verbose = function () {}; // comment out to enable verbose logging @@ -31,6 +28,7 @@ define([ var validateKey = conf.validateKey; var readOnly = conf.readOnly || false; var network = conf.network; + var sframeChan = conf.sframeChan; conf = undefined; var initializing = true; @@ -38,15 +36,15 @@ define([ var queue = []; var messageFromInner = function (m, cb) { queue.push([ m, cb ]); }; - SFrameChannel.on('Q_RT_MESSAGE', function (message, cb) { + sframeChan.on('Q_RT_MESSAGE', function (message, cb) { messageFromInner(message, cb); }); - var onReady = function(wc) { + var onReady = function () { // Trigger onReady only if not ready yet. This is important because the history keeper sends a direct // message through "network" when it is synced, and it triggers onReady for each channel joined. if (!initializing) { return; } - SFrameChannel.event('EV_RT_READY', null); + sframeChan.event('EV_RT_READY', null); // we're fully synced initializing = false; }; @@ -122,7 +120,7 @@ define([ message = unBencode(message);//.slice(message.indexOf(':[') + 1); // pass the message into Chainpad - SFrameChannel.query('Q_RT_MESSAGE', message, function () { }); + sframeChan.query('Q_RT_MESSAGE', message, function () { }); }; // We use an object to store the webchannel so that we don't have to push new handlers to chainpad @@ -134,14 +132,14 @@ define([ channel = wc.id; // Add the existing peers in the userList - SFrameChannel.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly }); + sframeChan.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly }); // Add the handlers to the WebChannel wc.on('message', function (msg, sender) { //Channel msg onMessage(sender, msg, wc, network); }); - wc.on('join', function (m) { SFrameChannel.event('EV_RT_JOIN', m); }); - wc.on('leave', function (m) { SFrameChannel.event('EV_RT_LEAVE', m); }); + wc.on('join', function (m) { sframeChan.event('EV_RT_JOIN', m); }); + wc.on('leave', function (m) { sframeChan.event('EV_RT_LEAVE', m); }); if (firstConnection) { // Sending a message... @@ -210,7 +208,7 @@ define([ network.on('disconnect', function (reason) { if (isIntentionallyLeaving) { return; } if (reason === "network.disconnect() called") { return; } - SFrameChannel.event('EV_RT_DISCONNECT'); + sframeChan.event('EV_RT_DISCONNECT'); }); network.on('reconnect', function (uid) { @@ -230,7 +228,7 @@ define([ return { start: function (config) { - SFrameChannel.whenReg('EV_RT_READY', function () { start(config); }); + config.sframeChan.whenReg('EV_RT_READY', function () { start(config); }); } }; }); diff --git a/www/common/sframe-channel.js b/www/common/sframe-channel.js index 793906c5a..12b390fa7 100644 --- a/www/common/sframe-channel.js +++ b/www/common/sframe-channel.js @@ -2,22 +2,92 @@ define([ '/common/sframe-protocol.js' ], function (SFrameProtocol) { - var otherWindow; - var handlers = {}; - var queries = {}; - - // list of handlers which are registered from the other side... - var insideHandlers = []; - var callWhenRegistered = {}; - - var module = { exports: {} }; var mkTxid = function () { return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', ''); }; - module.exports.init = function (ow, cb) { - if (otherWindow) { throw new Error('already initialized'); } + var create = function (ow, cb) { + var otherWindow; + var handlers = {}; + var queries = {}; + + // list of handlers which are registered from the other side... + var insideHandlers = []; + var callWhenRegistered = {}; + + var chan = {}; + + chan.query = function (q, content, cb) { + if (!otherWindow) { throw new Error('not yet initialized'); } + if (!SFrameProtocol[q]) { + throw new Error('please only make queries are defined in sframe-protocol.js'); + } + var txid = mkTxid(); + var timeout = setTimeout(function () { + delete queries[txid]; + console.log("Timeout making query " + q); + }, 30000); + queries[txid] = function (data, msg) { + clearTimeout(timeout); + delete queries[txid]; + cb(undefined, data.content, msg); + }; + otherWindow.postMessage(JSON.stringify({ + txid: txid, + content: content, + q: q + }), '*'); + }; + + var event = chan.event = function (e, content) { + if (!otherWindow) { throw new Error('not yet initialized'); } + if (!SFrameProtocol[e]) { + throw new Error('please only fire events that are defined in sframe-protocol.js'); + } + if (e.indexOf('EV_') !== 0) { + throw new Error('please only use events (starting with EV_) for event messages'); + } + otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*'); + }; + + chan.on = function (queryType, handler) { + if (!otherWindow) { throw new Error('not yet initialized'); } + if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } + if (!SFrameProtocol[queryType]) { + throw new Error('please only register handlers which are defined in sframe-protocol.js'); + } + handlers[queryType] = function (data, msg) { + handler(data.content, function (replyContent) { + msg.source.postMessage(JSON.stringify({ + txid: data.txid, + content: replyContent + }), '*'); + }, msg); + }; + event('EV_REGISTER_HANDLER', queryType); + }; + + chan.whenReg = function (queryType, handler) { + if (!otherWindow) { throw new Error('not yet initialized'); } + if (!SFrameProtocol[queryType]) { + throw new Error('please only register handlers which are defined in sframe-protocol.js'); + } + if (insideHandlers.indexOf(queryType) > -1) { + handler(); + } else { + (callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(handler); + } + }; + + handlers['EV_REGISTER_HANDLER'] = function (data) { + if (callWhenRegistered[data.content]) { + callWhenRegistered[data.content].forEach(function (f) { f(); }); + delete callWhenRegistered[data.content]; + } + insideHandlers.push(data.content); + }; + var intr; var txid; window.addEventListener('message', function (msg) { @@ -32,7 +102,7 @@ define([ } clearInterval(intr); otherWindow = ow; - cb(); + cb(chan); } else if (typeof(data.q) === 'string' && handlers[data.q]) { handlers[data.q](data, msg); } else if (typeof(data.q) === 'undefined' && queries[data.txid]) { @@ -48,7 +118,7 @@ define([ if (window !== window.top) { // we're in the sandbox otherWindow = ow; - cb(); + cb(chan); } else { require(['/common/requireconfig.js'], function (RequireConfig) { txid = mkTxid(); @@ -63,75 +133,5 @@ define([ } }; - module.exports.query = function (q, content, cb) { - if (!otherWindow) { throw new Error('not yet initialized'); } - if (!SFrameProtocol[q]) { - throw new Error('please only make queries are defined in sframe-protocol.js'); - } - var txid = mkTxid(); - var timeout = setTimeout(function () { - delete queries[txid]; - console.log("Timeout making query " + q); - }, 30000); - queries[txid] = function (data, msg) { - clearTimeout(timeout); - delete queries[txid]; - cb(undefined, data.content, msg); - }; - otherWindow.postMessage(JSON.stringify({ - txid: txid, - content: content, - q: q - }), '*'); - }; - - var event = module.exports.event = function (e, content) { - if (!otherWindow) { throw new Error('not yet initialized'); } - if (!SFrameProtocol[e]) { - throw new Error('please only fire events that are defined in sframe-protocol.js'); - } - if (e.indexOf('EV_') !== 0) { - throw new Error('please only use events (starting with EV_) for event messages'); - } - otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*'); - }; - - module.exports.on = function (queryType, handler) { - if (!otherWindow) { throw new Error('not yet initialized'); } - if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } - if (!SFrameProtocol[queryType]) { - throw new Error('please only register handlers which are defined in sframe-protocol.js'); - } - handlers[queryType] = function (data, msg) { - handler(data.content, function (replyContent) { - msg.source.postMessage(JSON.stringify({ - txid: data.txid, - content: replyContent - }), '*'); - }, msg); - }; - event('EV_REGISTER_HANDLER', queryType); - }; - - module.exports.whenReg = function (queryType, handler) { - if (!otherWindow) { throw new Error('not yet initialized'); } - if (!SFrameProtocol[queryType]) { - throw new Error('please only register handlers which are defined in sframe-protocol.js'); - } - if (insideHandlers.indexOf(queryType) > -1) { - handler(); - } else { - (callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(handler); - } - }; - - handlers['EV_REGISTER_HANDLER'] = function (data) { - if (callWhenRegistered[data.content]) { - callWhenRegistered[data.content].forEach(function (f) { f(); }); - delete callWhenRegistered[data.content]; - } - insideHandlers.push(data.content); - }; - - return module.exports; + return { create: create }; }); diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index 503dc2983..b6ab3f7b8 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -4,6 +4,11 @@ // Please document the queries and events you create, and please please avoid making generic // "do stuff" events/queries which are used for many different things because it makes the // protocol unclear. +// +// WARNING: At this point, this protocol is still EXPERIMENTAL. This is not it's final form. +// We need to define protocol one piece at a time and then when we are satisfied that we +// fully understand the problem, we will define the *right* protocol and this file will be dynomited. +// define({ // When the iframe first launches, this query is sent repeatedly by the controller // to wait for it to awake and give it the requirejs config to use. diff --git a/www/pad2/main.js b/www/pad2/main.js index bf92010e8..064153914 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -14,6 +14,7 @@ define([ '/common/cryptget.js', '/pad/links.js', '/bower_components/nthen/index.js', + '/common/sframe-channel.js', '/bower_components/file-saver/FileSaver.min.js', '/bower_components/diff-dom/diffDOM.js', @@ -21,8 +22,8 @@ define([ 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/customize/src/less/cryptpad.less', 'less!/customize/src/less/toolbar.less' -], function ($, Crypto, realtimeInput, Hyperjson, - Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links, nThen) { +], function ($, Crypto, CpNfInner, Hyperjson, + Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links, nThen, SFrameChannel) { var saveAs = window.saveAs; var Messages = Cryptpad.Messages; var DiffDom = window.diffDOM; @@ -84,67 +85,48 @@ define([ Cryptpad.errorLoadingScreen(Messages.websocketError); }; - var andThen = function (editor) { - //var $iframe = $('#pad-iframe').contents(); - //var secret = Cryptpad.getSecrets(); - //var readOnly = secret.keys && !secret.keys.editKeyStr; - //if (!secret.keys) { - // secret.keys = secret.key; - //} - var readOnly = false; // TODO - - - var $bar = $('#cke_1_toolbox'); - - var $html = $bar.closest('html'); - var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); - if ($faLink.length) { - $html.find('iframe').contents().find('head').append($faLink.clone()); - } - var isHistoryMode = false; - - if (readOnly) { - $('#cke_1_toolbox > .cke_toolbox_main').hide(); - } - - /* add a class to the magicline plugin so we can pick it out more easily */ - - var ml = window.CKEDITOR.instances.editor1.plugins.magicline.backdoor.that.line.$; - - [ml, ml.parentElement].forEach(function (el) { - el.setAttribute('class', 'non-realtime'); - }); - - var documentBody = $html.find('iframe')[0].contentWindow.document.body; + var domFromHTML = function (html) { + return new DOMParser().parseFromString(html, 'text/html'); + }; - var inner = window.inner = documentBody; + var forbiddenTags = [ + 'SCRIPT', + 'IFRAME', + 'OBJECT', + 'APPLET', + 'VIDEO', + 'AUDIO' + ]; + + var openLink = function (e) { + var el = e.currentTarget; + if (!el || el.nodeName !== 'A') { return; } + var href = el.getAttribute('href'); + if (href) { window.open(href, '_blank'); } + }; - var cursor = module.cursor = Cursor(inner); + var getHTML = function (inner) { + return ('\n' + '\n' + inner.innerHTML); + }; - var setEditable = module.setEditable = function (bool) { - if (bool) { - $(inner).css({ - color: '#333', - }); - } - if (!readOnly || !bool) { - inner.setAttribute('contenteditable', bool); + var CKEDITOR_CHECK_INTERVAL = 100; + var ckEditorAvailable = function (cb) { + var intr; + var check = function () { + if (window.CKEDITOR) { + clearTimeout(intr); + cb(window.CKEDITOR); } }; + intr = setInterval(function () { + console.log("Ckeditor was not defined. Trying again in %sms", CKEDITOR_CHECK_INTERVAL); + check(); + }, CKEDITOR_CHECK_INTERVAL); + check(); + }; - // don't let the user edit until the pad is ready - setEditable(false); - - var forbiddenTags = [ - 'SCRIPT', - 'IFRAME', - 'OBJECT', - 'APPLET', - 'VIDEO', - 'AUDIO' - ]; - - var diffOptions = { + var mkDiffOptions = function (cursor, readOnly) { + return { preDiffApply: function (info) { /* Don't accept attributes that begin with 'on' @@ -262,12 +244,69 @@ define([ } } }; + }; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + + var andThen = function (editor, Ckeditor, sframeChan) { + //var $iframe = $('#pad-iframe').contents(); + //var secret = Cryptpad.getSecrets(); + //var readOnly = secret.keys && !secret.keys.editKeyStr; + //if (!secret.keys) { + // secret.keys = secret.key; + //} + var readOnly = false; // TODO + + + var $bar = $('#cke_1_toolbox'); + + var $html = $bar.closest('html'); + var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); + if ($faLink.length) { + $html.find('iframe').contents().find('head').append($faLink.clone()); + } + var isHistoryMode = false; + + if (readOnly) { + $('#cke_1_toolbox > .cke_toolbox_main').hide(); + } + + /* add a class to the magicline plugin so we can pick it out more easily */ + + var ml = Ckeditor.instances.editor1.plugins.magicline.backdoor.that.line.$; + [ml, ml.parentElement].forEach(function (el) { + el.setAttribute('class', 'non-realtime'); + }); + + var documentBody = $html.find('iframe')[0].contentWindow.document.body; + + var inner = window.inner = documentBody; + + var cursor = module.cursor = Cursor(inner); + + var setEditable = module.setEditable = function (bool) { + if (bool) { + $(inner).css({ + color: '#333', + }); + } + if (!readOnly || !bool) { + inner.setAttribute('contenteditable', bool); + } + }; + + // don't let the user edit until the pad is ready + setEditable(false); var initializing = true; - var Title; - var UserList; - var Metadata; + //var Title; + //var UserList; + //var Metadata; var getHeadingText = function () { var text; @@ -280,14 +319,7 @@ define([ })) { return text; } }; - var DD = new DiffDom(diffOptions); - - var openLink = function (e) { - var el = e.currentTarget; - if (!el || el.nodeName !== 'A') { return; } - var href = el.getAttribute('href'); - if (href) { window.open(href, '_blank'); } - }; + var DD = new DiffDom(mkDiffOptions(cursor, readOnly)); // apply patches, and try not to lose the cursor in the process! var applyHjson = function (shjson) { @@ -323,28 +355,12 @@ define([ }; var realtimeOptions = { - // the websocket URL - websocketURL: Cryptpad.getWebsocketURL(), - - // the channel we will communicate over - channel: 'x',//secret.channel, - - // the nework used for the file store if it exists - network: Cryptpad.getNetwork(), - - // our public key - validateKey: undefined,//secret.keys.validateKey || undefined, + sframeChan: sframeChan, readOnly: readOnly, - - // Pass in encrypt and decrypt methods - crypto: undefined,//Crypto.createEncryptor(secret.keys), - // really basic operational transform transformFunction : JsonOT.validate, - // cryptpad debug logging (default is 1) // logLevel: 0, - validateContent: function (content) { try { JSON.parse(content); @@ -440,16 +456,8 @@ define([ } }; - var getHTML = function () { - return ('\n' + '\n' + inner.innerHTML); - }; - - var domFromHTML = function (html) { - return new DOMParser().parseFromString(html, 'text/html'); - }; - var exportFile = function () { - var html = getHTML(); + var html = getHTML(inner); var suggestion = Title.suggestTitle('cryptpad-document'); Cryptpad.prompt(Messages.exportPrompt, Cryptpad.fixFileName(suggestion) + '.html', function (filename) { @@ -477,7 +485,8 @@ define([ Metadata = Cryptpad.createMetadata(UserList, Title, null, Cryptpad); var configTb = { - displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'], + displayed: [ + 'title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'], userList: UserList.getToolbarConfig(), share: { secret: secret, @@ -648,14 +657,6 @@ define([ cursor.setToStart(); } }; -/* unreachable - realtimeOptions.onAbort = function () { - console.log("Aborting the session!"); - // stop the user from continuing to edit - setEditable(false); - toolbar.failed(); - Cryptpad.alert(Messages.common_connectionLost, undefined, true); - }; */ realtimeOptions.onConnectionChange = function (info) { setEditable(info.state); @@ -685,7 +686,9 @@ define([ } }; - module.realtimeInput = realtimeInput.start(realtimeOptions); + var cpNfInner = CpNfInner.start(realtimeOptions); + + Cryptpad.onLogout(function () { setEditable(false); }); @@ -725,32 +728,17 @@ define([ }); }; - - var CKEDITOR_CHECK_INTERVAL = 100; - var ckEditorAvailable = function (cb) { - var intr; - var check = function () { - if (window.CKEDITOR) { - clearTimeout(intr); - cb(window.CKEDITOR); - } - }; - intr = setInterval(function () { - console.log("Ckeditor was not defined. Trying again in %sms", CKEDITOR_CHECK_INTERVAL); - check(); - }, CKEDITOR_CHECK_INTERVAL); - check(); - }; - var main = function () { var Ckeditor; var editor; + var sframeChan; nThen(function (waitFor) { ckEditorAvailable(waitFor(function (ck) { Ckeditor = ck; })); $(waitFor(function () { Cryptpad.addLoadingScreen(); })); + SFrameChannel.create(window.top, waitFor(function (sfc) { sframeChan = sfc; })); }).nThen(function (waitFor) { Ckeditor.config.toolbarCanCollapse = true; if (screen.height < 800) { @@ -770,7 +758,7 @@ define([ onConnectError(); } }); - andThen(editor); + andThen(editor, Ckeditor, sframeChan); }); }; main(); diff --git a/www/pad2/outer.js b/www/pad2/outer.js index 8e09a6ea0..e24a9a4b8 100644 --- a/www/pad2/outer.js +++ b/www/pad2/outer.js @@ -8,10 +8,12 @@ define([ '/bower_components/chainpad-crypto/crypto.js' ], function (SFrameChannel, $, CpNfOuter, nThen, Cryptpad, Crypto) { console.log('xxx'); + var sframeChan; nThen(function (waitFor) { $(waitFor()); }).nThen(function (waitFor) { - SFrameChannel.init($('#sbox-iframe')[0].contentWindow, waitFor(function () { + SFrameChannel.create($('#sbox-iframe')[0].contentWindow, waitFor(function (sfc) { + sframeChan = sfc; console.log('sframe initialized'); })); Cryptpad.ready(waitFor()); @@ -23,12 +25,13 @@ define([ //onConnectError(); } }); - }).nThen(function (waitFor) { + var secret = Cryptpad.getSecrets(); var readOnly = secret.keys && !secret.keys.editKeyStr; if (!secret.keys) { secret.keys = secret.key; } - var outer = CpNfOuter.start({ + CpNfOuter.start({ + sframeChan: sframeChan, channel: secret.channel, network: Cryptpad.getNetwork(), validateKey: secret.keys.validateKey || undefined, From 0dde1d750740ddbf62d40a0922de3210e5d9bd72 Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Thu, 10 Aug 2017 14:49:21 +0200 Subject: [PATCH 05/63] wip --- www/common/metadata-manager.js | 72 +++++++++++++++++---- www/common/sframe-chainpad-netflux-inner.js | 66 +++---------------- www/common/sframe-channel.js | 22 ++++--- www/pad2/main.js | 4 +- 4 files changed, 86 insertions(+), 78 deletions(-) diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 9d26d5588..822880a02 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -1,17 +1,67 @@ define([], function () { - var metadataChange = function (ctx, newMeta) { + var create = function (sframeChan) { + var personalMetadata = 'uninitialized'; + var myID = 'uninitialized'; + var members = []; + var metadataObj = 'unintialized'; + var dirty = true; + var changeHandlers = []; - }; - var getMetadata = function (ctx) { - - }; - var create = function (sframeChan, cpNfInner) { - var ctx = { - sframeChan: sframeChan, - personalMetadata: {} + var checkUpdate = function () { + if (!dirty) { return; } + if (metadataObj === 'uninitialized') { throw new Error(); } + if (myID === 'uninitialized') { throw new Error(); } + if (personalMetadata === 'uninitialized') { throw new Error(); } + var mdo = {}; + Object.keys(metadataObj).forEach(function (x) { + if (members.indexOf(x) === -1) { return; } + mdo[x] = metadataObj[x]; + }); + mdo[myID] = personalMetadata; + metadataObj = mdo; + dirty = false; + changeHandlers.forEach(function (f) { f(); }); + }; + var change = function () { + dirty = true; + setTimeout(checkUpdate); }; - }; - return { create: create }; + sframeChan.on('EV_USERDATA_UPDATE', function (ev) { + personalMetadata = ev; + change(); + }); + sframeChan.on('EV_RT_CONNECT', function (ev) { + myID = ev.myID; + members = ev.members; + change(); + }); + sframeChan.on('EV_RT_JOIN', function (ev) { + members.push(ev); + change(); + }); + sframeChan.on('EV_RT_LEAVE', function (ev) { + var idx = members.indexOf(ev); + if (idx === -1) { console.log('Error: ' + ev + ' not in members'); return; } + members.splice(idx, 1); + change(); + }); + sframeChan.on('EV_RT_DISCONNECT', function () { + members = []; + change(); + }); + return Object.freeze({ + metadataChange: function (meta) { + metadataObj = meta; + change(); + }, + getMetadata: function () { + checkUpdate(); + return metadataObj; + }, + onChange: function (f) { changeHandlers.push(f); } + }); + }; + return Object.freeze({ create: create }); }); \ No newline at end of file diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index efce47a91..d4bd31f0e 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -15,57 +15,15 @@ * along with this program. If not, see . */ define([ - '/bower_components/chainpad/chainpad.dist.js', -], function () { + '/common/metadata-manager.js', + '/bower_components/chainpad/chainpad.dist.js' +], function (MetadataMgr) { var ChainPad = window.ChainPad; var module = { exports: {} }; var verbose = function (x) { console.log(x); }; verbose = function () {}; // comment out to enable verbose logging - var mkUserList = function () { - var userList = Object.freeze({ - change : [], - onChange : function(newData) { - userList.change.forEach(function (el) { - el(newData); - }); - }, - users: [] - }); - - var onJoining = function (peer) { - if(peer.length !== 32) { return; } - var list = userList.users; - var index = list.indexOf(peer); - if(index === -1) { - userList.users.push(peer); - } - userList.onChange(); - }; - - // update UI components to show that one of the other peers has left - var onLeaving = function (peer) { - var list = userList.users; - var index = list.indexOf(peer); - if(index !== -1) { - userList.users.splice(index, 1); - } - userList.onChange(); - }; - - var onReset = function () { - userList.users.forEach(onLeaving); - }; - - return Object.freeze({ - list: userList, - onJoin: onJoining, - onLeave: onLeaving, - onReset: onReset - }); - }; - module.exports.start = function (config) { var onConnectionChange = config.onConnectionChange || function () { }; var onRemote = config.onRemote || function () { }; @@ -84,15 +42,13 @@ define([ config = undefined; var chainpad; - var userList = mkUserList(); var myID; var isReady = false; - sframeChan.on('EV_RT_JOIN', userList.onJoin); - sframeChan.on('EV_RT_LEAVE', userList.onLeave); + var metadataMgr = MetadataMgr.create(sframeChan); + sframeChan.on('EV_RT_DISCONNECT', function () { isReady = false; - userList.onReset(); onConnectionChange({ state: false }); }); sframeChan.on('EV_RT_CONNECT', function (content) { @@ -121,7 +77,6 @@ define([ onInit({ myID: myID, realtime: chainpad, - userList: userList, readOnly: readOnly }); }); @@ -137,13 +92,12 @@ define([ isReady = true; chainpad.start(); setMyID({ myID: myID }); - // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced - if (!readOnly) { userList.onJoin(myID); } onReady({ realtime: chainpad }); }); - return { - getMyID: function () { return myID; } - }; + return Object.freeze({ + getMyID: function () { return myID; }, + metadataMgr: metadataMgr + }); }; - return module.exports; + return Object.freeze(module.exports); }); \ No newline at end of file diff --git a/www/common/sframe-channel.js b/www/common/sframe-channel.js index 12b390fa7..3dd0a3d8d 100644 --- a/www/common/sframe-channel.js +++ b/www/common/sframe-channel.js @@ -51,21 +51,22 @@ define([ otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*'); }; - chan.on = function (queryType, handler) { + chan.on = function (queryType, handler, quiet) { if (!otherWindow) { throw new Error('not yet initialized'); } - if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } if (!SFrameProtocol[queryType]) { throw new Error('please only register handlers which are defined in sframe-protocol.js'); } - handlers[queryType] = function (data, msg) { + (handlers[queryType] = handlers[queryType] || []).push(function (data, msg) { handler(data.content, function (replyContent) { msg.source.postMessage(JSON.stringify({ txid: data.txid, content: replyContent }), '*'); }, msg); - }; - event('EV_REGISTER_HANDLER', queryType); + }); + if (!quiet) { + event('EV_REGISTER_HANDLER', queryType); + } }; chan.whenReg = function (queryType, handler) { @@ -80,13 +81,13 @@ define([ } }; - handlers['EV_REGISTER_HANDLER'] = function (data) { + (handlers['EV_REGISTER_HANDLER'] = handlers['EV_REGISTER_HANDLER'] || []).push(function (data) { if (callWhenRegistered[data.content]) { callWhenRegistered[data.content].forEach(function (f) { f(); }); delete callWhenRegistered[data.content]; } insideHandlers.push(data.content); - }; + }); var intr; var txid; @@ -104,9 +105,12 @@ define([ otherWindow = ow; cb(chan); } else if (typeof(data.q) === 'string' && handlers[data.q]) { - handlers[data.q](data, msg); + handlers[data.q].forEach(function (f) { + f(data || JSON.parse(msg.data), msg); + data = undefined; + }); } else if (typeof(data.q) === 'undefined' && queries[data.txid]) { - queries[data.txid](data, msg); + queries[data.txid](msg, msg); } else if (data.txid === txid) { // stray message from init return; diff --git a/www/pad2/main.js b/www/pad2/main.js index 064153914..675b05a25 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -660,10 +660,10 @@ define([ realtimeOptions.onConnectionChange = function (info) { setEditable(info.state); - toolbar.failed(); + //toolbar.failed(); TODO if (info.state) { initializing = true; - toolbar.reconnecting(info.myId); + //toolbar.reconnecting(info.myId); // TODO Cryptpad.findOKButton().click(); } else { Cryptpad.alert(Messages.common_connectionLost, undefined, true); From 1e56fa31c0173593f30c760bac32dd870b64620a Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Thu, 10 Aug 2017 18:31:44 +0200 Subject: [PATCH 06/63] yay, they talk and they don't fight --- www/common/metadata-manager.js | 43 ++++++++++++++------- www/common/sframe-chainpad-netflux-inner.js | 2 +- www/common/sframe-chainpad-netflux-outer.js | 4 +- www/common/sframe-channel.js | 36 ++++++++++++----- www/common/sframe-protocol.js | 2 +- www/pad2/main.js | 27 ++++++------- www/pad2/outer.js | 28 ++++++++++++++ 7 files changed, 99 insertions(+), 43 deletions(-) diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 822880a02..24d03df2e 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -1,24 +1,38 @@ define([], function () { + var UNINIT = 'uninitialized'; var create = function (sframeChan) { - var personalMetadata = 'uninitialized'; - var myID = 'uninitialized'; + var meta = UNINIT; + var myID = UNINIT; var members = []; - var metadataObj = 'unintialized'; + var metadataObj = UNINIT; var dirty = true; var changeHandlers = []; var checkUpdate = function () { if (!dirty) { return; } - if (metadataObj === 'uninitialized') { throw new Error(); } - if (myID === 'uninitialized') { throw new Error(); } - if (personalMetadata === 'uninitialized') { throw new Error(); } + if (meta === UNINIT) { throw new Error(); } + if (myID === UNINIT) { myID = meta.myID; } + if (metadataObj === UNINIT) { + metadataObj = { + defaultTitle: meta.doc.defaultTitle, + title: meta.doc.defaultTitle, + users: {} + }; + } var mdo = {}; - Object.keys(metadataObj).forEach(function (x) { + // We don't want to add our user data to the object multiple times. + var containsYou = false; + console.log(metadataObj); + Object.keys(metadataObj.users).forEach(function (x) { if (members.indexOf(x) === -1) { return; } - mdo[x] = metadataObj[x]; + mdo[x] = metadataObj.users[x]; + if (metadataObj.users[x].uid === meta.user.uid) { + console.log('document already contains you'); + containsYou = true; + } }); - mdo[myID] = personalMetadata; - metadataObj = mdo; + if (!containsYou) { mdo[myID] = meta.user; } + metadataObj.users = mdo; dirty = false; changeHandlers.forEach(function (f) { f(); }); }; @@ -27,8 +41,8 @@ define([], function () { setTimeout(checkUpdate); }; - sframeChan.on('EV_USERDATA_UPDATE', function (ev) { - personalMetadata = ev; + sframeChan.on('EV_METADATA_UPDATE', function (ev) { + meta = ev; change(); }); sframeChan.on('EV_RT_CONNECT', function (ev) { @@ -52,8 +66,9 @@ define([], function () { }); return Object.freeze({ - metadataChange: function (meta) { - metadataObj = meta; + updateMetadata: function (m) { + if (JSON.stringify(metadataObj) === JSON.stringify(m)) { return; } + metadataObj = m; change(); }, getMetadata: function () { diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index d4bd31f0e..e5b921859 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -52,7 +52,7 @@ define([ onConnectionChange({ state: false }); }); sframeChan.on('EV_RT_CONNECT', function (content) { - content.members.forEach(userList.onJoin); + //content.members.forEach(userList.onJoin); myID = content.myID; isReady = false; if (chainpad) { diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index 078a920e7..01b26d494 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -228,7 +228,9 @@ define([], function () { return { start: function (config) { - config.sframeChan.whenReg('EV_RT_READY', function () { start(config); }); + config.sframeChan.whenReg('EV_RT_READY', function () { + start(config); + }); } }; }); diff --git a/www/common/sframe-channel.js b/www/common/sframe-channel.js index 3dd0a3d8d..62250704a 100644 --- a/www/common/sframe-channel.js +++ b/www/common/sframe-channel.js @@ -18,6 +18,7 @@ define([ var chan = {}; + // Send a query. channel.query('Q_SOMETHING', { args: "whatever" }, function (reply) { ... }); chan.query = function (q, content, cb) { if (!otherWindow) { throw new Error('not yet initialized'); } if (!SFrameProtocol[q]) { @@ -40,6 +41,7 @@ define([ }), '*'); }; + // Fire an event. channel.event('EV_SOMETHING', { args: "whatever" }); var event = chan.event = function (e, content) { if (!otherWindow) { throw new Error('not yet initialized'); } if (!SFrameProtocol[e]) { @@ -51,13 +53,17 @@ define([ otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*'); }; + // Be notified on query or event. channel.on('EV_SOMETHING', function (args, reply) { ... }); + // If the type is a query, your handler will be invoked with a reply function that takes + // one argument (the content to reply with). chan.on = function (queryType, handler, quiet) { - if (!otherWindow) { throw new Error('not yet initialized'); } + if (!otherWindow && !quiet) { throw new Error('not yet initialized'); } if (!SFrameProtocol[queryType]) { throw new Error('please only register handlers which are defined in sframe-protocol.js'); } (handlers[queryType] = handlers[queryType] || []).push(function (data, msg) { handler(data.content, function (replyContent) { + if (queryType.indexOf('Q_') !== 0) { throw new Error("replies to events are invalid"); } msg.source.postMessage(JSON.stringify({ txid: data.txid, content: replyContent @@ -69,25 +75,35 @@ define([ } }; - chan.whenReg = function (queryType, handler) { + // If a particular handler is registered, call the callback immediately, otherwise it will be called + // when that handler is first registered. + // channel.whenReg('Q_SOMETHING', function () { ...query Q_SOMETHING?... }); + chan.whenReg = function (queryType, cb, always) { if (!otherWindow) { throw new Error('not yet initialized'); } if (!SFrameProtocol[queryType]) { throw new Error('please only register handlers which are defined in sframe-protocol.js'); } + var reg = always; if (insideHandlers.indexOf(queryType) > -1) { - handler(); + cb(); } else { - (callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(handler); + reg = true; + } + if (reg) { + (callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(cb); } }; - (handlers['EV_REGISTER_HANDLER'] = handlers['EV_REGISTER_HANDLER'] || []).push(function (data) { - if (callWhenRegistered[data.content]) { - callWhenRegistered[data.content].forEach(function (f) { f(); }); - delete callWhenRegistered[data.content]; + // Same as whenReg except it will invoke every time there is another registration, not just once. + chan.onReg = function (queryType, cb) { chan.whenReg(queryType, cb, true); }; + + chan.on('EV_REGISTER_HANDLER', function (content) { + if (callWhenRegistered[content]) { + callWhenRegistered[content].forEach(function (f) { f(); }); + delete callWhenRegistered[content]; } - insideHandlers.push(data.content); - }); + insideHandlers.push(content); + }, true); var intr; var txid; diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index b6ab3f7b8..ea7557fc7 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -34,5 +34,5 @@ define({ // Called from the outside, this informs the inside whenever the user's data has been changed. // The argument is the object representing the content of the user profile minus the netfluxID // which changes per-reconnect. - 'EV_USERDATA_UPDATE': true + 'EV_METADATA_UPDATE': true }); \ No newline at end of file diff --git a/www/pad2/main.js b/www/pad2/main.js index 675b05a25..3f527e72c 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -260,6 +260,7 @@ define([ // secret.keys = secret.key; //} var readOnly = false; // TODO + var cpNfInner; var $bar = $('#cke_1_toolbox'); @@ -339,7 +340,9 @@ define([ var stringifyDOM = module.stringifyDOM = function (dom) { var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); - + hjson[3] = { + metadata: cpNfInner.metadataMgr.getMetadata() + }; /*hjson[3] = { TODO users: UserList.userData, defaultTitle: Title.defaultTitle, @@ -380,9 +383,6 @@ define([ } }; - var meta; - var metaStr; - realtimeOptions.onRemote = function () { if (initializing) { return; } if (isHistoryMode) { return; } @@ -403,6 +403,10 @@ define([ newSInner = stringify(newInner[2]); } + if (newInner[3]) { + cpNfInner.metadataMgr.updateMetadata(newInner[3].metadata); + } + // build a dom from HJSON, diff, and patch the editor applyHjson(shjson); @@ -446,14 +450,6 @@ define([ if (newSInner && newSInner !== oldSInner) { Cryptpad.notify(); } - - var newMeta = newInner[3]; - var newMetaStr = JSON.stringify(newMeta); - if (newMetaStr !== metaStr) { - metaStr = newMetaStr; - meta = newMeta; - //meta[] HERE - } }; var exportFile = function () { @@ -473,7 +469,7 @@ define([ }; realtimeOptions.onInit = function (info) { - + console.log('onInit'); // TODO return; @@ -599,6 +595,7 @@ define([ // this should only ever get called once, when the chain syncs realtimeOptions.onReady = function (info) { + console.log('onReady'); if (!module.isMaximized) { module.isMaximized = true; $('iframe.cke_wysiwyg_frame').css('width', ''); @@ -686,9 +683,7 @@ define([ } }; - var cpNfInner = CpNfInner.start(realtimeOptions); - - + cpNfInner = CpNfInner.start(realtimeOptions); Cryptpad.onLogout(function () { setEditable(false); }); diff --git a/www/pad2/outer.js b/www/pad2/outer.js index e24a9a4b8..4d835bf03 100644 --- a/www/pad2/outer.js +++ b/www/pad2/outer.js @@ -18,6 +18,34 @@ define([ })); Cryptpad.ready(waitFor()); }).nThen(function (waitFor) { + var parsed = Cryptpad.parsePadUrl(window.location.href); + if (!parsed.type) { throw new Error(); } + var defaultTitle = Cryptpad.getDefaultName(parsed); + var updateMeta = function () { + console.log('EV_METADATA_UPDATE'); + var name; + nThen(function (waitFor) { + Cryptpad.getLastName(waitFor(function (n) { name = n })); + }).nThen(function (waitFor) { + sframeChan.event('EV_METADATA_UPDATE', { + doc: { + defaultTitle: defaultTitle, + type: parsed.type + }, + myID: Cryptpad.getNetwork().webChannels[0].myID, + user: { + name: name, + uid: Cryptpad.getUid(), + avatar: Cryptpad.getAvatarUrl(), + profile: Cryptpad.getProfileUrl(), + curvePublic: Cryptpad.getProxy().curvePublic + } + }); + }); + }; + Cryptpad.onDisplayNameChanged(updateMeta); + sframeChan.onReg('EV_METADATA_UPDATE', updateMeta); + Cryptpad.onError(function (info) { console.log('error'); console.log(info); From 33e73dd5e529dfea1fb9de7b91a873d95dd150ea Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Thu, 10 Aug 2017 21:40:34 +0200 Subject: [PATCH 07/63] fixed one last fight --- www/common/metadata-manager.js | 11 +++++------ www/pad2/main.js | 4 ++++ www/pad2/outer.js | 7 ++++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 24d03df2e..930a7c138 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -2,7 +2,6 @@ define([], function () { var UNINIT = 'uninitialized'; var create = function (sframeChan) { var meta = UNINIT; - var myID = UNINIT; var members = []; var metadataObj = UNINIT; var dirty = true; @@ -11,27 +10,27 @@ define([], function () { var checkUpdate = function () { if (!dirty) { return; } if (meta === UNINIT) { throw new Error(); } - if (myID === UNINIT) { myID = meta.myID; } if (metadataObj === UNINIT) { metadataObj = { defaultTitle: meta.doc.defaultTitle, title: meta.doc.defaultTitle, + type: meta.doc.type, users: {} }; } var mdo = {}; // We don't want to add our user data to the object multiple times. var containsYou = false; - console.log(metadataObj); + //console.log(metadataObj); Object.keys(metadataObj.users).forEach(function (x) { if (members.indexOf(x) === -1) { return; } mdo[x] = metadataObj.users[x]; if (metadataObj.users[x].uid === meta.user.uid) { - console.log('document already contains you'); + //console.log('document already contains you'); containsYou = true; } }); - if (!containsYou) { mdo[myID] = meta.user; } + if (!containsYou) { mdo[meta.user.netfluxId] = meta.user; } metadataObj.users = mdo; dirty = false; changeHandlers.forEach(function (f) { f(); }); @@ -46,7 +45,7 @@ define([], function () { change(); }); sframeChan.on('EV_RT_CONNECT', function (ev) { - myID = ev.myID; + meta.user.netfluxId = ev.myID; members = ev.members; change(); }); diff --git a/www/pad2/main.js b/www/pad2/main.js index 3f527e72c..62cc64922 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -622,6 +622,10 @@ define([ // Update the user list (metadata) from the hyperjson // XXX Metadata.update(shjson); + var parsed = JSON.parse(shjson); + if (parsed[3] && parsed[3].metadata) { + cpNfInner.metadataMgr.updateMetadata(parsed[3].metadata); + } if (!readOnly) { var shjson2 = stringifyDOM(inner); diff --git a/www/pad2/outer.js b/www/pad2/outer.js index 4d835bf03..2b88bb420 100644 --- a/www/pad2/outer.js +++ b/www/pad2/outer.js @@ -19,10 +19,11 @@ define([ Cryptpad.ready(waitFor()); }).nThen(function (waitFor) { var parsed = Cryptpad.parsePadUrl(window.location.href); + parsed.type = parsed.type.replace('pad2', 'pad'); if (!parsed.type) { throw new Error(); } var defaultTitle = Cryptpad.getDefaultName(parsed); var updateMeta = function () { - console.log('EV_METADATA_UPDATE'); + //console.log('EV_METADATA_UPDATE'); var name; nThen(function (waitFor) { Cryptpad.getLastName(waitFor(function (n) { name = n })); @@ -32,13 +33,13 @@ define([ defaultTitle: defaultTitle, type: parsed.type }, - myID: Cryptpad.getNetwork().webChannels[0].myID, user: { name: name, uid: Cryptpad.getUid(), avatar: Cryptpad.getAvatarUrl(), profile: Cryptpad.getProfileUrl(), - curvePublic: Cryptpad.getProxy().curvePublic + curvePublic: Cryptpad.getProxy().curvePublic, + netfluxId: Cryptpad.getNetwork().webChannels[0].myID, } }); }); From 4acd9957a928bfa8f3793d3a3a35a4001718298d Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Fri, 11 Aug 2017 10:40:57 +0200 Subject: [PATCH 08/63] Set the url if there is none --- bower.json | 2 +- www/common/sframe-chainpad-netflux-outer.js | 6 +++++- www/pad2/main.js | 2 +- www/pad2/outer.js | 12 ++++++++---- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/bower.json b/bower.json index bfc74af20..c680abdd6 100644 --- a/bower.json +++ b/bower.json @@ -40,7 +40,7 @@ "less": "^2.7.2", "bootstrap": "#v4.0.0-alpha.6", "diff-dom": "2.1.1", - "nthen": "^0.1.5" + "nthen": "^0.1.5", "open-sans-fontface": "^1.4.2" } } diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index 01b26d494..904cb9521 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -29,6 +29,7 @@ define([], function () { var readOnly = conf.readOnly || false; var network = conf.network; var sframeChan = conf.sframeChan; + var onConnect = conf.onConnect || function () { }; conf = undefined; var initializing = true; @@ -40,7 +41,7 @@ define([], function () { messageFromInner(message, cb); }); - var onReady = function () { + var onReady = function (wc) { // Trigger onReady only if not ready yet. This is important because the history keeper sends a direct // message through "network" when it is synced, and it triggers onReady for each channel joined. if (!initializing) { return; } @@ -131,6 +132,9 @@ define([], function () { wcObject.wc = wc; channel = wc.id; + onConnect(wc); + onConnect = function () { }; + // Add the existing peers in the userList sframeChan.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly }); diff --git a/www/pad2/main.js b/www/pad2/main.js index 62cc64922..e4874a532 100644 --- a/www/pad2/main.js +++ b/www/pad2/main.js @@ -641,7 +641,7 @@ define([ } } } else { - Title.updateTitle(Cryptpad.initialName || Title.defaultTitle); + //Title.updateTitle(Cryptpad.initialName || Title.defaultTitle); documentBody.innerHTML = Messages.initialState; } diff --git a/www/pad2/outer.js b/www/pad2/outer.js index 2b88bb420..526035d3d 100644 --- a/www/pad2/outer.js +++ b/www/pad2/outer.js @@ -18,6 +18,10 @@ define([ })); Cryptpad.ready(waitFor()); }).nThen(function (waitFor) { + var secret = Cryptpad.getSecrets(); + var readOnly = secret.keys && !secret.keys.editKeyStr; + if (!secret.keys) { secret.keys = secret.key; } + var parsed = Cryptpad.parsePadUrl(window.location.href); parsed.type = parsed.type.replace('pad2', 'pad'); if (!parsed.type) { throw new Error(); } @@ -55,10 +59,6 @@ define([ } }); - var secret = Cryptpad.getSecrets(); - var readOnly = secret.keys && !secret.keys.editKeyStr; - if (!secret.keys) { secret.keys = secret.key; } - CpNfOuter.start({ sframeChan: sframeChan, channel: secret.channel, @@ -66,6 +66,10 @@ define([ validateKey: secret.keys.validateKey || undefined, readOnly: readOnly, crypto: Crypto.createEncryptor(secret.keys), + onConnect: function (wc) { + if (readOnly) { return; } + Cryptpad.replaceHash(Cryptpad.getEditHashFromKeys(wc.id, secret.keys)); + } }); }); }); \ No newline at end of file From 744fb407ae87631760578fe1f20b6b582a27c270 Mon Sep 17 00:00:00 2001 From: CatalinScr Date: Wed, 16 Aug 2017 15:53:10 +0300 Subject: [PATCH 09/63] Updated the what is page --- customize.dist/bkabout.jpg | Bin 17129 -> 67668 bytes customize.dist/bkwhat.jpg | Bin 0 -> 103964 bytes customize.dist/pages.js | 60 ++++++++++++------ .../src/less2/pages/page-login.less | 2 +- .../less2/pages/page-what-is-cryptpad.less | 38 ++++++++++- 5 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 customize.dist/bkwhat.jpg diff --git a/customize.dist/bkabout.jpg b/customize.dist/bkabout.jpg index 43de082685b0dd5ad25189e81a4585715d741b02..01a51c1769011b6a578e50af4f4a052a0c3538d3 100644 GIT binary patch literal 67668 zcmbrm2|!KT-#@(eaOQ^7q*1nt(k!J?v9GZ~bySj49aI`rM`@B_AJ-5r&NWm*&UFzI z(X3+MOc7T~gGx?avnHXC;aw-(`}}^-@Be?E_k9=VoW0jxYwfkapYOcZ?rZDUE}%6h zH7WuCH#bwj2LF_=JwRo4((;%zfB+m;I|J~w3)>L6dbMbpl~r7#<p=(8Pf^p)M=@)qS)}&ONrFjm2rvFQe6y22cHIy<<(XO#AuV%F)ju_$?r-8 zcnXMF@d@FCo#j-E<&&p45cZCilkFTGY;4U58|%qat*jlbCfizAPo8G&IL&%8@x2(p zyCsB0O!JvN_xruznTx^qhe}CFu}qm_8K1DiYOZNhZ!++d63f}vB-I^>_wu(+K~M~5Fj?~Izw1U15GEg>@G1j71kS*46vY6dG9I1$6SiQfD$ZCX{#A|n z+xdG2>H~jt*=US;J=4c=Ie4E(fJ!Qt=Cv0BbpQ;hi86zeV}LCRq`+-v^iQO<4P8MkMvgH$0GEUDpgdgAnE{^U4d~hTJ zM=3x9upp6Gm#tlaTz|{T9dPJix*K9Ac1<5}`RMc0Lu<3um?AY_(O8C@!t4!prL=Ej z<&LZcPZ9<&KpLWMv(f4GF3x8iQY7gTy?!z+iRuPN?Pszd%wA1UaKccp>tvDg|}IaEOe={3Jpcr>aIX7GeUX zM4c%V5RhZQ^Z^_AJ(dD~Tn0d7EZutj*f|PH_UVA;E9F>9(~AqSi|9r1oo)shg-8{V zKOeJnd$G+!eb_;AsaQ)@8ZOqwnr__z!{Rz$Yv-KvHUpSMi8frRP$(Z+UT}mE03hHZ z)&TOQ)Lsk#2`gOHS&;x}kW$w^q~{Qj8j!m?FaVZNyP!6(Lm(YT=u={{p7|PN>v)1r zv>fbz4|2pSB5?+Cm<&UY@KrIs_DswwQDWqPXmnr%HyI!jl~R;E0~9EVv?>SccriXq zNh~4}!iZ!e{g|s_T&<#2q{3$z03)t|*UA8Nt_&<>a{**kK93{TL4CW?85x=#%uE7` z$w?k*h$)Ly^=Ne-F$Jwvl(`}x;G`mLgCf2UDRo{yDvn0{@cV#s8sM3@L`$3+z=~Aj z^KpvJ0H?%ZxHy+k6R4HZJP9Wtj1eFvOOR|XMVdkp;qZ#t_mOPULBK|8u+7${u3U+Z zRD&>4YXd-&FyWv8{tH7%0H~8(z$6o^3FnVH_|A~rA|~_gdF;_~N`J>fP=Nd?v&q&i z5f{_TaACrwrfj|VQt7ITO+@0wdYfjUkd0$4w=Bw$zB-X!iM7Nzr&V0b{)@>B;Ijc> zBP0Pv3nYU&DumAs?<+A;prjxSQ>Gw%z(s02002`6(&@`lsH?)T*p$(OE+aTL$wB~bqliFTtOZQQ*uzm#v7Zm9md#}bVK$6T#t_(f2?(qr;Z=Ybc4OA+wTg%k ziXkinP?rL8Dk}r1;u1pjT%jUiCNT^F5ukw#KmdV@1stfo09S$+A;2(FMQVgVgKRiM z2vjj^TPcc>Aellb%7l;Cv*}-$!ud2L9%OIj&hFXKkC&xrWH3QIEU>LVvrb;((8-~RGcIc+is!4&H z=5M4x5eY?|G#wIy>GhP)8H2~_xdVKuf$hc&MM7hpm}PD{w|+YVv@)4XQ#L!0lwi9$ z`c#E9Vjw@Gj;QTppb^15#v^9aUPMSe&{CZ#RwXHiD&VSNtwyN^;LjNX#u@4lF|6E+fVZ)h~IdqLnu)5I~u{~B3d(134=13!(Df5JPCsTAkD=t)ZCgW4a7f3=5 z00x_<*RrbE3@PXJZJZhg9nc%N#inL#Mz{GXx~eM@swjDZaiD`Q$)7>vH=xxEGuZ51 zEDrwyj0ccM<2B=g(BB`*@I&Mt{`^U{?BCpw@!HY_S4ZeQfKZp{qUtAoDB!sYQAb;1 z!8+QA&=@y>di1c{3`!`CX^n&}Rzw(`m8pWGM_H+yGf%XrVR0ftOaLp{j7~HG73&YE zWpG;2wLmIRV594(&@~9*CWVrwDdd9n^}0}@uegdqx8hMZo=SyAqUR!Y=0nbfssu6X zLlLkay}AsKZ-e}m?o3|5r3y4*IxLQPC{dMy3k)b#CRi^3)sztc7O(=~D;Z!PN@{jM zD?=J=FB|3q0dGhM;?Q-p+X)$zX*N!c&=VOFHGwKrZGc7TCGquR>h&xiO0-bq#glU< z$IWwpiL@%M*?uHd-_wt!If*L$xQrDPj~%_7s`D2zrH#1+*>b9tWhT*z{M}+L+q(BG z?b314y?$C1=sHOchGuA`F2hriOr-ZV)TQ4f6OlkSa61VNumFHdH1HMzH5t}RfO1gF z3>g4MOcGdiGY~@8KgQBu>T>s`^?$sseHhAb}+@ zkuu_FNSV~mGli(VWUQgJIj!MHa}P=ssRP46s*Vkd*AEyRH{!W8~|B>bM-Rh6I^00KK2PZ0ysF)SKV+5p&PpU2w0dpuGV5-za?;LjLBrEqV%b7;1 z&2q|>fB$WrX0fu5&|{y}Av%yA?4+L3r)BnWc`|0RRA-U3o)2G!DnotaWQajg)9m0V0MI3NC90tX~9@2r~uccZbD;gP8~)H z41>rz((|>1`-KREk2nFu3snV7=tFV7fY1w=E6)jHBjq5Znhan=&=(K6O-qeu07Id5 z4}>3uk0^yMnRE>43e06F6%jLxWSMY6X`tZ>OgICG9qQeUMl){n`jHACG2K8bgs_e* zK%U*t>tyNCdl=a#^)H3`DX%}=*v6XU$8zd9fZaJ2CvzUQpqG{-SDE|is3n!4t+I!z z3IaPD4$GjwP6pe(5x;8m!|bcbH_;be_0!K|F0{MYh>c5uu?Xr`HX{?7P-wZQAW%{8 zvN`BEAd1Ur3LBzga84a?+R${${Sb+e-W0Gs3>`kQ z$Xy&o7+%Gxy!KAUv6d=cSqI}$FLarN__+)YWT>M(=qk?u1=@=?FW85AXQ*S*VCQ{> z%Y0>;k$~0#)K9>%Lp}N-6xqW7v~q;TLQWE5jS&ippQSv)2^0;HMu1y8hIGG*3Kx@p zBXlyNrKU%gNR-H8)H{`f=6QZVCS0mlrB6XeV2Kk*Mu61Gl(D%DE64k!9P}=w)`tWP zgWJ4Ll=KC_2;C6I*k6o4E5yadSD;$v3?oMP2$|nQ6~9L!sKuBKE>T6($qT3e`T!G! zq$(nWL4k3GCQeyUfnK-iox|V|<*GBEN9dB%n_v`XbcQYx7`>+?YQZwdRC3~}34okC z9I2I@8I*P`q{si^uX^(LCs)8d?j0m-6k z^g6|{ZaXW)uS#yUGNQ?@Q(OM`So!3!_^StdqjHwN`aeHzOOF zaTQ83cp%ZRRswra(p?C^XoM?O>;;TN_P~ppD4qBVerkmF0>-c9(vSnPI#3w4LD0~< zBD`K_AH(~KP-8|n!}}^OPQ9F=1PRSoWu*A>^%f>z+t$%u6G%0}q3~`Ph$U393>IH$ zBDKmYL&<6!(DM^|St(bsG)0_U1IBzME1Jb0vz~Ap5(j-D<|QCzk(_o)$vgp>k)cFD zxt6LGIS*4u_;-z_BO>lFbruE>h_8qtF{h4PRczX_nMc?F*;b_+;&c=X9Bf`61YvRk}-qY&;__@t}LT$vynk92cRU> zSwgRR^L}(0W5ik&Gr+}RzY#`k#t}YDjNoD$Xs0Er2x7#ugTy7hfLtn6(L^W`FrbBq z!4n}5dafrvk*ko1%2p*4^fh73*9gwm+$L3$8EZm(z^ZuUDz>8)DYHYaWh}Ie(gY##)7;@c9^*W(xyS!4_;2KI3PkW`vw|qZQwRj!R{4;9K#s z0)OG^eo9LV(bMZ@bwVr zT1{E8)x_f|T%5 z{RONt{{qJQUxT;wZcnj;mP#v~7EkOib0t;eUrD)aCHnw6 zH@pMI7?P|s2-@}~HS{U$QTp!>LP_i7vN6tP} zv!{|S>dNLIPBYaua_zHK1>-z!u9h->C$08;BGYsQYB$p-BabiByvNm)a~9#8H6t3) z-w{kNJVxhxEJF)4JL$9;SGmOjnzifbdA||!e!ECHXJWOKKB+U9vYb*TSdxKS*4}d7 zoFd>oi37zTjR-YD?{#1|(GSW(Ou4hT8{FyeG#~fts@f_rdHE*uec@ra$C)9{MI{;bY zXek&JC>yc%lp@ai3t4+X=p-$P>b9 zAC=}rr4io7B4r~C*2E!C-cy5@Gn6N-M4lkdIn>~3Mxed3y7+iq^o%E%$KLyC$oj1d zo-#i2hA8+EACX>;R2iIhklelZK4!K5e&{V`$mTj-aj$;UC(?(e z1yQ@_v)^XYHgD-G4cOj$>EPL8Bs6i?P|Ny4S|oaBpTxE@$C=k_ zt?>-QW$=D_zya*`M6yocYtoRd$EdmnOy9kg8L|g;F%`m}KFn%fr1Hc{s-aAzQXoyd z*z#Un^J-j%)|wFDZC1s3B6cvv*TjdDhZL*8AuU%WR)(6GlATm(8v#45VPYhvoTEHW zsFK%#oc=6xMdt#70|Jo6Bh-q16i|G0DnL2`9*3ST)TQA{1_Tynt56t%0-F&UoF*p` zT1xadk)jODL@t39<+vXk@Srz7FGC&XG1&m8@~_IHaGoCqISB+%h&};45CSF-m6K_0 zsusH<9Fr^#Uqc5#lo?_}A>R?JhZqdfmwccs zk>2JkD7p|j>Qdk+U?M^v9!T{A(SL=Gg5;Ecq*g#v3}mPXNd<>M2(SW5d~UrJTXPeO zt;9EmA3Y*u^kY0)_pM%!a3DqEh0^h06E@m#w1Vn_R`>6IR%|u`U^<$><%9zg6f`te zJZ5I@xUJZJRH7RgsVI_~%Zc#4uptxJ^pMMXtsNg8W!ayoSA#>TwBgC(vC{B1W-ulH zI)FGpYh?@_93`kL8Zpf%tE&nwM%0xCa>Y&3+e)PJg0Oj;b883uzSN57=ROZ2bAv%{ z;1xpIn^vf{h6?6x)*(LyClfpw$y(kk)IXFAlC7uEG&F)5s7S!68KGx~m$MYs$qdRD zAmgd+hz@lBO!@(pP8B}J5HDK!J(UiuC%WQ&)VK(wOUoL#w3EvN5MpW%GA;vXQvpC| z{X6og8{<*{TS=X*2h(fuUK;-b%uL|ZI+4}VVA@kQU1kcZ1keHk#kwjK3?Mydp+Rpi zLg}kX2?6AhN{X5wKO|TssW8aDg*?6eqbOc zo*X+lPO0P}`LFyE*<~VI&IC znALoR${Bp)Oxz!Fn9fkE3&nrZ&)Ifu-_qyL1U7G9!4F;6&@RK^3Rqp;RnC(qxsXR* z%m=%@G|Q>C!|L89?6=j&R9-aXPEdKkE9B77b62Z7{a_e@6tR>#=-(1Mgo>2LNXFPB z^DLa_rT%*%`NvDe1lLL<}~%yYdCY5~s&7muM>34$=eI$dZBEC}GY8 z22#URmYrOtcSVrtG@(L{TMz6J(vPa*T6O~^8AI`b#6>F5h9Mnbl`}t4`>!7D1MpeL zGfaNLf$4Fi)sGSo3?K zp)<7IUf^U8>NJI2*LZCC`OQlo9K!W;9tiXUxw$?<{rxM1bJrdp=tW)BHS~9Hp|?GL zjNJ?ZbqyWNwK=^|8$j=8C>6;%enIMdW@Fx-rnNJT_m`ezzb$3VJE5Y#FFqYT+L+DG z75IwhOh%rZ%A$UH@uVYLdyp1c>)D}B8~Db`8SDpwqh(bF165$>dIIV=jS3nsM1W2~ zdd<=K_%ePZM(Phh3thC-pFjJ~GYLC`n?4h9UkeIHT4!G7TTU60f{8K}fmtUWM4KYy zB4>e$>liQ-1-B=4tE_LpbNbs5xIlH42_{||IPrcHAKdrjIL$n8AKw&O&KW;vYdJCW zo;niAC2{o*3P+EL6OdN^88P_sGXm!$7f}e(DEYt1GMj{o2Cf;&^)Sh1lRp0q7~YRw zn*enT_T`U$L&=BG|6l_-n4=3%-^WI3=qj-A#NW98tEhiJgSV3aQ$zIxr=aB~((DVU)rLp@TK)ZGr5z(n0j0pW9N- zg3d)_s!A)h>o#?Ww+s~-d&%^4$-0I*!mz<(48A5^go-AiX))!jtjc1}lXKnp;J(|i zGn(eUw~ZNcII0i4J(MiY$}2}36I<~?I%_d(^5Q{;+Qx%aT?y1X_bCxbP7OmAZ=v;w z@b=sRsGT;8Q&$a(9YlGN`0tj@=u~8EB`K=7QpHh6uRI`)!0rezUTko1Du{8R95!Sy z;*;x@9(AK&-fQF#!~-#`!eOd~GF;$E8CD9}Qtk%+xcF{l8CGVk7=fCV<6szMKv#iz zA-5h(p~gibLlXS~ec41&y{29jx!swx9w$;RA>}L3L&CZBV+{LX3+SP=Dv)2Gc828S z06(e_K=enUhXAO8LI^m_wL>=r`n#jm-}pm*2vx^uy%t8J~(4m;)@Q!fRB^Uml zh<;FDN$`&%!zc9aEX-=A%Q%pZ{*h5BPW-30I2*9jb=@#L05^!$o8x&Uzq z!x4iLM6s6r1sJ}kT+tB1&_Zf(khG(?ODQHs=-2a)R0CJsT%xL_VJaR^b)~~3MQ{bR zyOdV+;j4}%XYQoO3>bR$fyTaCX1s_!fRuB}St*g+@NNtToGLDTB$`TUFNWI!`jJpt z`FTBrN$#=9MHC%H76H%(z}j!q} z9}?sa7$G`xO9c5wN9fv$Z*@Q%LI~G~lmaP1+6gZ{$F~@B*HYWEp70t=&BeF(-y~Fa zHRI96uTjw#;H8~^n{%Zs4!E|rq7!pvHobY}3Y7wOE55r34UVqk>siRC?KjSg*H#Xe zT?2#XzGTVpL8~uW=fR*|L}}ra$DOBY>a}I5cDhAqzHv7a)!+|clEvGir;ld$#1s*R zMU^5Y0Cfn(E!a_JPM>|a8%*H|)wVjIPODl`UF1q32aK&8p(pIAWjB6;LOYpsU$KQ$ zeb7NWeuD8N<%dpb&rs44PipT&+^G}F(~xC{rG`};o-;)%gwiY;pFcu7r%ypA3?Q6B zm98b=*CJ~pupLBo$tbW@qB3)YHV2DnrlS}ve~oxOWxdTHMnMPvt`)w6X!JwL%emlh zbAl-Fz<8(^AjfC~GWVb40jO5~Ul%^UaTA{eu5d4qtHic|4ETEl9Bjh7$a%|97h4$6 zwKDZL!Vu816xn)9IyuFJInGR^wYEeyudWjxw3~|PZ#h95*58LJ+-@pZqqCP%c^+Lw z-EDl?gS`D%2`+o)7qUMDduEHL?+$v6dF*LI-ol*3<)2QzK|ZbzIj<8{k0V`Q_o4Gr*{?y9y1D^uR814&ngxdC_^fs#);_WR?F%HqQI1bl$2??$ zv_BngqAPkcR7gd7{s4NP=Ovrs@T7{H)85LsDYiMw=9F{#Kv@K44W>xh`voe+oPLCx zKG|g7xwzSEk0NdC<9zvc6zK4EaYxNR8!GWah`uU%MsCPw^07@E#FaghI}<^moq=A= zH+66fvJd}nyJA{wCg4+i4dHm;3di|R4hX!-|50y@GU~rKK>6y_Ca6yYa7>he8lao~ zHrgTns1ZZXu%lzKzjp*01dfkqc&X#EpI9fTTptt$$@09|KI|qbPmW2bG@ej-ygXqx z0Sx-nFs;Cl=gLN-YUh$d7*VH1vi8A@S~IO5*oxH!fwUN*vIR_s6;PwsOOds(8H&J+ z29P22_qf;uI>d56oQv5Om*lp{vhIaD*=g1K$`*#QAEfA zS}Jfm4-{XyF9DJN=0@@v{^FCP#2*!G_TS?D-w()9R{_?`djok(HiAI+6d1|<2a?Y| z4>tww!36T~5;mQLu(^_dCITn-jUE57JpbkDcYXrYXkYmk`aC{z3Um6w(Wv)GNtp|^_`!Z~t%*!rYc;M2fx-c@+6mM1DKG%B zCTAZ$sc0YbOFI2{=nTj_!SaaBrEQzQT}nysX$A7Hp5|^6nCLCs$~v-GHgqVRL^1( zYKvH9uy1tW>+8*9R~7Sg$=O|`;>7Ohx3PepHhQNE<^E)gF!0HH@n_3_@$w*ekcu$J z4hS~AUkK5GN>?tQqcUwGrwA+>lQ7Y@qyO>y8$#b9!{OFrNO!ncPjKMCb8+nWNk#Je z7~~Hbl-~}DBlJ7un>y!EMx*zJ&`8uNN&sL8`44vA4xwK87hNFFv+b&PUi--WTuBY~ zW(icJZ--C$P;CbDL2LY#wH-ZM{6T6!Gy58JIBS=?_*VUpe6*owj&OKYv>O zlJG(nW2_TjdrkVHNR{@K_DJonZO5dBB{KTgPvE>4ZSzdV46aP9vof z44}S6scIdcr8>t~jZj|>$nz|tILYINAJk9wx#NTRVIyjR#zbKXELnm2r zUt}|m{{bI+RFdQ{K^7+!6kZc{CJKdYxX6#VdF71Q2+6| z5gf}EBj!S?KcrUvTN8PV_+3=L!|@HA|5__o^iV^8AN|{`iEqtzB6oeRC2V0m``F@j zFu3{jY4L}5NzD(hBy6D?$`#G>R#fq<%)#lGh~as{)bHkXHoDO7gn;o&J!8cHdc4RS zEZ<31uCHYe_;%xdtN3L|enHN;IGIh$4jMa@+|$Mk+{aaLbVK?0^>r?m7xx1aFLb+4Er2>q;r-Xt|Ro+DT9(lE_762PMo2L6}#%_ zO&L??&ebY<*ZuQk_@u*Bh$dV(9(esIn-!KUqYv5UJwS4`k_OO9A72M?c7Zi=MVG1? zM=MnN`A`}aPphff`SU}_MbQHiJyWLyRMZ92#tr+RpERnoU{YbmLRqRi0TS1QYTU0_srP?|4ITgoVw3%=W#EHi5=~{HKDDTj1{n$|uA1 zhYJOT8`=vJs7M`NJr7z}V9R>^@$URe_u>3y>CN@i7ZyBa&DnK_UhUh4OiJtmQIp!M zcr^_pgXjQqvWDEbpz%DC8Uq)etKV&iLR}pC(R_6iH!Ar?S0QIp^Iio`Ql%5U{pJ~3 ziKTJF+Vs5LWueB46FGN9dU1_i6+OYSk33va%0Hu`bWq@M`UGur|HSLX zl>eLzpR(pC(~$|Iqsf zrT@>r&yp|gJd!iAC{-{1<&uKSxBK85CRVS;Sknsj5qdu@{zL3*^mDwGZBqyl zl{nRY@a3dlc3boPFF~KBah)*-0z!r-VaE@994`BCu=nJmm^F{AFWa3u^H_QAk)V4U za$7sI^{XvYGMhe?9UGd4CtrvAsL3;hzijqXshnBL808>n3ZQ!~HyrY3`-kQJ=5_pE zo*ACW8%stTPLeOX5z6*2%KoHN@qbqT|x3(qBDM@zh9 z`QvPjccqgpyxiL}xi@>byIS0Rt8hHlw;yveI_1V~C<)tW;J$}Z=@q?4;^;KKkK1^D zS!FgO+7G;KT$df8VsZYjtWIW5^i}u=UrB-M<*OjHn z*G@jw)-bfmVN$uN(~@HYpeL>A>3Q~-EgIx^wF}>RYUT5QDu6k%^S})-q@&qA$aYdl zQ8wI-iUwuN!vuTysuE^auu!2PD5^2Bq2_k90|xj03bW_Pi<%6-R}DWAv> zA3w+4T))CP_eFdMYuA^`-{YTjUk)dfT^=SbzE$LX?Y@$I()2Z7!Bda($kuazhP1u0 z{3UAun;ugA{$bKXSyVz`)`Y<_BB>}Zq~*!Z9ip+ZFBVPBRO@6dYIu7lw4);E(o?wJ zTXbTvN6DFW`-hRO`3+fb${xfB!{3ax+;F(R>l0}iljrHsaBFurv*%ew_=lwa63+?N zPA}duCdXB0oqKJ9HLbAj_i0(PqW~*7=k*ml>5YclTxLwDZ+1jlJdQkbOxcm#gS{B6 zUNOsmOkQ1b(*V|axNK6%tu(3OY;BWMNHMcs?xD&HT&ex$fzE`gnk)BI*nrTGdo%<* zf=n&1mkT;Wp3hPTeD2@u{PzLC$TY`GUUy=~7AzEhm>z#3`gx+#{Ug{P;&$)66UW6? zkDJP^9fR{~KJQ1OhX~vK{z1UWxBtC3SIhS=5EWGVtH3sEwRq`-Vuj)|kW!^@c2!|U z5_$i9ESB7Mn5@#a)|<2_qK7IA+~e2Z$+B1O7baf3mU#5=ui1J{RaOtd8jA$FBX&VZ z2iZ0P-)-CZc6*_bmmpg^DQKN6BG_1A7&$$!M11R2zE{0rMVxT#D^tR>diKTR8Qief z;INYotx#ME&Ra3tM&SOflZcmNM5|WnpB&2HG!`vxStfMN+#h-28j_jo*pEKQOKPz_ z5+n5V7+Y|H>3e7SFqLA|_{&6aCGmk>UmNSKD^VyQpamhI&7lnC2}&t}1I&_~1<80+ z0*w%tPeL0rN_Xhp%Bia3`aPP21RtgLC?-%2V9!yX>Dv)81`Y}J&9_uG9FEDzq*2gY%(g-btJiX*z*Xc0|#Iz^5vIZ4$syx+vM4ILNo4!tOy?{qf3Ag`GHvCJ&ygYDS%z9=NCIcY}L-1Zfm zmF$m~onZ`fp|fs1vFiRym$@V5`Xx7Km&Ll!t1jLNI^9_Pw0)&wdtK#G4Sf|#r)cl} z?Z+!lVIIHUKd`Tv)=an=ov-sDCs$_uAwK7ssl~OGy_oWx%G5pGotI>jm$j7K+4*9~ zLAb{DVwU6bt!wUkn6-4CW?i0lCMw!@p_9%Z-Ix2`i~Ad!8ZR|nNr?Je>T|A za{*g>+4pIhNB7GIWs&33_dQ#kb}-_`n-%f2QZFE*JHYe5*A}QzDf#w=kcb>Lz=f^_ ztbss`7CD?SE`eb@+*AA$L{MFdZ-f!!D0rhS*!B9;Q}&hlKiDpv;qqtFxD%B(yFL^` zU2`|*Y0}x_cdCR6Z|_}NLNUn=@QFJeFCumkFnI@xk6oujduF9#SeV z7hmC=rJX-!J-Ghn5Y=|!>E3pCBzQ+yAGy>nei>Gfb>e&<=2^8Wjq3IbVJnoLZ(~NR zH&JlPsR5U#T)RGC{kC27C{iWL`A{7C!EJV`IMAbPC35iaa*GU(Z7*`Q>0Y7I9w~co z-5+4Vg#%%Td5BQQZF#6rJE|rxVML;`a4t_9d<9qL+am?MrnABJh~2rsUHJ;ek$UfE zjCq4uw)un$izGbHrMa%k&xNBD<6sVA0h5Ng9r$V)3FPZNP!u4MzZV8dgVEH>H!91m z?>}<$KOQC*I=p;y=xo-rCXZxcYRQ^@tCb&;E6Xkt&eJ}pXD8P5HkK@F3N2R7y8W(F z??cX?y$z4fBeMIiP6sw#NVcEN+Vkc3pN}{lXTAc<>E=qBX&3ijdbYaDl59Fuw90)Y zcJKJj;AG#jLS_G~`S(*#lB+Ad^AcV^eHD~+XMNE06+PI_&&x9}_;0oB*kIS^^qiFy zbUI0K4)-gt*xs(wlMjL79bRQbX%B`Pv_6Xg0v8^{PxmM%( zV6s(TbbD;w+L2@IcNf>~T-(=om9Bf1W(0>BcHXnY2eW_kagkA$<--GciRUQob@LWp z-s@H8&zV)L@a4BZ?dHW+b_toGE_WTj{MGuLS{LtRHgYzdI2nQXn}6op~kqti}zpuA)U5rFe_+kNmApS4=~Mq?p{=7YC(tw60x&+x`o+a(E%w^ zmmIy%Lw0!u@pWYzI#?ACA`14d42{We2Wk7;KAk<_J;d^5EV3%nTR@?|;*4`3DTYbx_< zU44Q`OB)F(cx2JkgFSn6^JUn}YHC^M!uqOoAqWt6J%hDA;gKigDVshF1M{UwM(FaDVrGXbg?Bl%k!sXkBW62fU;4|S7+JmC|#dr*1(fYrg$-B&r!+;>)(1B-<&y&$FSp*>RR z6B8e)9UDYxEovOYJbSUVE)nB?E)Szmm1f0_r8dkOrDDW~TGS7vRWC50*?o` zHZ+#LdbAO{cJ8HO$_I^$>svZmcb;DCIPos2{o#xR>}mA7x*2UHWrt*}EZ@_KYj*v* ztS6I|`>Ld5+}*^=OO(aELsg(R@7ALvjkBR`#8C34#6#sJ1#dg6nU?BlTLT#-%jkP^H}8bv{YD4dFn5&EVn4Q zb-rAA(Tg%O-=>PiaqT(e+>7gvJZ)YStQBxfDRhzaRK($yGsg!;sLaHVL-xs;hfBRy zUHvTe_|UoL$(P?toC`KpQwbLz20h#LY;|lF$;&$24)U`K7NzPesqTxsFJrb$I>n`; z5*8&RP;umJEmT~`8;L*$SN%e2ug!n(0E7>wEEo_UIktM+*+}jOvkSfhpu#iAEGBtq zs#K{t`pw0Zg6DECQ4as8cL=kb0LI;)|C#q=dgRfQJo;H=09$z72`M=C>quWv( z(`mDljXjueFXrZanvwD$uDFwF=XfwAk?=lM)+ZVB#zb-5!jx3;v*u^xZRu+{7kAn| z$1{%yPm$=BbUa5Y{bLfayXRDMhN#9C|C&3 zF0-HPIR82sKJ8Dq+cmoQI<@STPLxHqcxza9L(uuf$d06H8}4OO$4sTs@cr;6u+4gs{9QZ<2N>0E zC^-|hf&L&|eS&LESQH5qk1_R2c4&f#z7=ye*symTdUE-(TR-M;;7O-W=6i9;`hzu3 zy5So}CMrTx`5%P%*HXxgyHvNq?j|&l(MkFq(9H{S(jW(4CiS^Ls=Kyf8Fn-O<-Exe zIYI45MEVvR7Q|dR9J4ZLKm^Lgzo%e>6LsAAZV8q`hRT z-P&96rDqf^51cC+D~($Hs=M&$ei6&Vk?V2kp)9E^??ugr!^sm_L5^b?N+BP}q$QWF z!pB$NBMpi?Ck4j#$4&^A#?(DHkzaDT9FLw9-L4tEQsJCS!|LYN(seypS%~VJF*p4Q zV`lz=OV7rA1%YUA+_7sd51mp$zFX-L-Cs^zKaE$qZK%!rbOvqkxs-0~yr{~|y>9YB z-GJ!?w{F$8&K$Ox?d#~r`4XM7c{6*OCIupm-kDK(C%X4<-5sgj^u%hHg;j4%(1*ZJ z1^w7Lvpb(>t-U-sKI_kbqMCQ*3NIdQ@Os(;|M<>951;xOi+{_HID7bFreo@D#<>T@jpZB6o5j2ad)u--RCs z8!34Q!IY%!j~h=5*RMFcdaPYyb3@saqza4Q#vy}KSJ^fk=<7$%&g}RNIhPl;z-MOV z2g|EPskOTPjyv}vnw(-@?CBn|r?_#`i}&LE;=c3lr?EQ)iI=ihN*o^ko_M#M6Zdob z!G83^k{MGuVKt{KJ$9a!yO#cyfW{rFKZY6ObX;g1zhqQK;P8xZ*W}!Mu1`l z8q!qSBz5l3{>74Ve0b+k{K$5$^T&toyOJ!t&-2&zu0MJI`q}p;122=dU3r>TK{*6B zW%_Qhl+J0+x#4+1vAwx@O^VT*jk`+U9n?AAV$pDqd3i}`TeU}T>&pd|Yd)qX+D|OK zpw#@X{VT}I>B%;EwIcZGiYHGkDru#mq(9?k*kwW2wEEoP%<1p3qp$9MD6W>S4o>m> z>)0oOZg70&pY0Oe;N^SU6Z$ZZ`~>e0k0{M|Uh}?qFGs)R)qVxHs--i0A8&XPQg8h7 z(C$z9e~8EVKVCGFd!0Eh@bRKI!7`@htE!womW)u>I;HgiQ4c#Cu$`U1FBp7rc8JCM z?2*6cLQ6O6;G+9gH$I(8CY;w)r-$?p$5*yegD>9&&)Yxa>a%`q^TT;GW16@1rC3p_oiqQOZdM+2 zb5%`9&CavF?(OAAvdoms4q_fz4V%i}{vH;bvW+z-@5m9AuR#CM%-(Q~!8Yqp4`V%s zCjOPGxBswZo44*Srl0B6=M(D7ZC8Zr4qs3zeQna!(6#tl>ACc9-R?@G=B7!hGl{g! zb9Z8*R#@C9SBOrWdg6UrO7h5&?giJs#2ga!-1CTQy}_UM{Op>K#rrFqd{_Ql`Q$_1 zhyDu7tJP1m8lO0<+~u_Ax|%1_-09!^*Y;-@$(I!#tJeAz1gQNyxBYm#)B6n0;82Oj zlBJffvX!SkxbmfaDsr&oo$-?ky(Mlx&Ep-hUw3bg(`7!-!+&}l^V9SJr=W1Nq~e}| ztK8ho!@)0CRfK&$rjzpGx_8>#xXQUbD?QCkqzX%lBm8p)Zf~^kbV_a;_L`&fSIM8c z>c>zE&&oJidh9;d!GkZWS|-Hq-hLAm-GuMJQ@T9B@ELg+~#v?w4*hlGHH(4++jJs|xZpWk}dde=LDBs0m( zWY%QP+57DM-JjDxHO!n%M|TrTwCdPE=StDgVg$npOj4TyuEfNUOs zL)`Q@n2~t$yLJ8U?Lb%w_|383oBzMXWyZk^>uZ2f7R1+ehM&;3yuHLq;U$o{T&H35=0sUq1`{4ut)G z#&9Qi!3{9^%L9sACJJmsWkMZN^y^c^?xH?Y-zepca`(`N8kvVb-@LTs?j0N$4D1Yc z;Fj!A+9`XL1nXq(1}@&*fTm6R7#{%^KdbrNKM)*onSDHQc-wCIByStdR66Rgbh~QC zcMa-T=g3yFsx+4XT$Q}j$|wy)3&U+llQcZcS-wCKvc7Am{W<@S4yfFHqcEhtgfN-# zqq_@%E6|wmG25t@*&(^JMxaKA-a7QQ&?2qQ7~yOlca?6NNmep>WL5=pv23?d+<8Hz zR@ch^77r?;tas@#$I$H0gGUIkv@3NsSv8}=wuM*I`(x@t;}{p0m|B;TsJBIU{ssiB zi!MtE-E{*9jV~4*yD0X@1(=K4foVi#nB7Pft#NJ`jc~)T8RH=11I|PDxac>}fJ>kM zsT5p%Y(3Bko|h0^DsKAeyNZ<`7-6l2>(GM3x=#J*SdSd|v5N8#F^h@UmB$%cxY|IJ zZrc?rscWFk-5Mk`FE&R3bDJaYi*)_gZpjTS+%QQO1yK2epOg5HqvZyW3XLD${D?qF zxEj0mv|y*8^Q=fBe35Wy`b28FtQRi1uO~Ypzk@82+Z`X}61(v!6~Bb*1&zEp27d$; zyzq%^<9#aPLg^n)^T1e1F0g8K-}-4%Y&LH zm)XWtr$v0JVKUD!*WmsQ#2m6Y@i}Cq^xNXt?%uHA>rQ7MgmU{Cr{O$Fg>-VPg-17H z=~qNjXpfeHqPRS+5!ul>a|9@=OmopP>%QaSxnyU2gYEIv5HFn3jMExNsnNH>l6CZ&v{uHiZ7diAU_F3I#P7mDN1S)xZK zpG)kNc?WH=m`jAHUP@HGr734NI5lgQb)KG|(-83!ZUUdsU?`={F3|`KN#D^sTXklk z-9cNt+G9Kx2lA zqhu{hk99h6#~%TP4o3hnf0>ni*^SM83k*BO6K!31HHp3I%H|%ILhUokdXnd%{3RRC zBBo2gZtvaL%p5!#ggsc9i=lEO0+-<{ zomdAES1P4$eKadT=zrmX|Gj{;xtG8V*#C8#$Hf0{hzR`OP!ZhWcVrwKG5+5Ik*lP$ z{6_PKqM#cYwz)C_ez;;E`YLpy7QedGfktWMnU8Yq9}I_ z^4#BNYcWo4TO$^aA?vm8!*ut4x#734==BCsqtd}3Rt=~`uCGc@HYM}oG?7w&ZOD?0`i3KcF3#h+&CtV9S0C}YwJh(Gik+R3 z#LaW^3@~OXrZCPfJ*t1{VVa`1ST%F*7 zPW$EY+qp%4`KiGv=elP`yJ_-U`4D;s-kN)qT;$cSuK4#-9uWTXv3Ao|M4nshFYA^# zVY459C%j3`17a8wQEqo!xZ4xpn>j(yw5CMkR;)nrlnb9h3bX36IZF9zDVW@|%H1ak zj%NLWh3bACGdC8xt^>ik4+ePoFYhD+zQhHw!+~G)Z|M1&fil?y(;fWjOB-AC{CIj1 zv7Gln*G)7(H3OprpXS0f!I$LR6$fg!z|zvR08eu#M{&9McnfVw1r`U^hbk-m$BXeP zu6Vk2i?c zXDn@_tNx|d_~6jeRL`^VPsx`Vsjh@cM$8FPL_uhz(T(_g<%o_wR#x&LvIr7gBi-uq zKnEbPc;uHGW${llhK)VMFw{ZPfgWBKH>5c3a4EG6Oyp3~@%5@B%jar$f{foy4F!k= z|2x#F=K6J*G}aci23=+@s@1wmo6q`XYpCkvqknd>WArddzCA31Uo2DTjJNgYS@)zo zr-3SG961E;j-`8N==&G>JKF@5Evy|Ty|0(vj_78?0}l~K->*XjH|h^q;hx!d)en}5 zV~f??0~;|oMq{=?_N=9e>sCY=#dnTd&Hl6$GiVPOU!DB(|)!he3tV- z*!1S@mgBpcrDo-l%#gtv={OV_ zGwwd-OZL6iR6GsjmBSLSvU z4i`8y^S;Y%p9UR&(Bvfy9}7jq9~Mj;J3u9j>;qZY^{lB?s8-2(oh-0BBy*qy(Xs^3 z`FTQe(Ne76;|#k&?AP!acDL5AthZ;__~a}wCrL-K%1+xvF8M93|Y zI(}ICMa(u)L&n#CI4$X;iNzZvOkPnw+r_62{)hY;v~`g5BKg(Ug2QNOKNUwX=py8E z_geB_GU+;nlXi`)m3qSWA@6ysKLl%?M0HbyAW1T2GoxH)$qC^fA4j}LiimA3ABsP7 zRDQkD9B{_M|NKn?P%AY}mT=a2BwY0-Qail4hR5$chOy50$}PN!mw`Xan>c3DQwI9HNtYi_+UlQ-hh|n$%{4EMp=ZAMe(xNm^~q)spE9fY?T9p_)eEj6 z;Ww4p+NCW02O29aCtO*R<_W*hmZZoQJ)bxpqs>%ESblyFJMQu?y#4y1aS*^(U|tyd z2e3>XZhIFIqw(UiP`aQEHx~O}_Y+>vg@&=!J7ZO?kI{Ojc3Jf&SExzy2K9VaK~*Yt z<7hq&bGP%>;0`&S(OYQJ;%cD)G8kWWoZ~^x$KoDzINX2Xv9x3LsT&+ti6)#yZ z3)<=TYJ4Dba{@F)M0NSaiqWBf%SQm5p8u&|zjx}`fhIN4!lg$BBp7_gD_xM%4g+Bl zxmg`KAnIu+L~fmQGl4_nkY`!5+jyAT|+8lx}9*Vnan8(V6AAFyL zDY>PR?6pf4zml2LmIc##`{Vy{G)N@i)&KMYtFIqlTXNDWiU`R&;eqgScZQlVO|?JB z`}#cMEB;pL2o}lB6`d4&pYTKlDf{P~>A?-i^ZC@8QFPYL!v|}_kOx6>q2}aMmJHi_ zmfxDtLQdiI$?$;7V^QI$-$$ttqOVguSfx+m_(V9(dApGRI-{HvtV+-)FRwXSO%>aZf@Mmi1qRkf`4ynuY4N_B z5mo#P5|-M!<3pYP1+dHgJrdPkeYx(iX z%tX25y#f!x$g4TRqKnoer!-*gfw~Nfw!uh7EJ<_g<*m*ox3JwU#zQ0I_Br+J`6F zFE7(5@7z5PPhXi8_``HkCSZzNZP{RFK+l%L(;`;Tw(ay?70YptGsu$yRmyA1Xf=yz zHRUoA-CfTmVsasfi0laTQ$wXJl4n+nsr(-{Yg}v|c2XkEQXmbYMP2LP9p|fYL_dYh z-3WG&mbHPQ!{SKNNQ_L=v6=2*@`3vwz4P>%r$p!M@@)gfZK)nqU*yAx(Ph+rAC9ABO>JwHkmzr)^4c$Gia0$p$| z*upjf*t9`|nSn_#U9xib2G(pskraDZ03QruCp1cCqmrDmeTgZ9wKv+h0iP;_X-gLi zz$*x~PQIMrj4s&u^z+H-*;3{g1#hTdvC(3l?Zy)xnX+!bnbtKRt(D=m0<%`j+R*&f za+2;T8yv?ioKt?{Yo=L*dYb4BKW#u0p-mdeJ+Ya3F;AER)cTaWYW9ihckV2QW?S>D zhogne#?v3bP#Nap@?}(urql9xU;7QKI`Y7*??B$RMAB0*ojjWu?zM7So)oLt#2|&h znu<1}4?59J#opoTj0q?k?tN$mVas|SBC??i>Z9gn6QF@8Tdds%J?F7!#ZJg6t}rZW zw{#7{Rs}-s-;v?YWa^bn2!GO{Ir{{W_oY!pB=`{Bi4r*iN-ZYe2Yny3wEC|Cw zpaY4(hUoPZvkboC+u8hz%wcF(-AOkNoZhy~gz`wvTaF!!{do=zJwg$H7pp!oi@dFY z7R{r33`49L0rf!4uS1*5er2;98qOvm!xP6gd~!DO)SjN%5ISLQcoW?^x-H?d=6#`b zNMyp64Nq?xH>fDTvrE)Bmzaa=UOgpS&|a`Jv-D;OAjL}lSNqM!#&M&*@$CLZ0dK21 zw~DkjKxJXW(p8_RAbhQ=hW}c@UH|=<(`Uzg5#IQ5*`-X73hj+4H2awb<*}SyTe9)R zJ0rbxdRFTBFcv|?Syx-_G^ipy*h$)|96FSfpK-b43#6a!cEj}fAz-&);r_yb7u9W< z?%{V!8CmArVTyjiLae^CJw?_oDpK&Q;c4^U-`4);l;_pYFD?L1EQC;q^qg+#hwI1j zw@gz_2MSMN$&+2)=7ogTG3bK>AvKokESVKb@>F#ef+UW>Z-4cJyeSHejcmejrHDrR zo_{=WgWrMsvPrGELV}G`Tk|=c_T!B&h$eiQmT$a$o?hb$I9KceS-alr^W=D0txBs6 zQ(T!I*zuWT4S$+YG{_j?USq57IIWZkCDc17QMt}Yv(77RlqEw5guD%K=^~g-&_wcz|JRF z{W}D83=KT_o$~dsolvy4ta)O1By<_I8-K+Ikd4cZGU9Zr=Zul}@?Xct(|8NyD`2)Q}KN+j^!@p{^1_(zbOwmzt5s-%34T*^8gBjq0vLO=U* zyc#-4x<2B569~k#Nc!#P^0Z)hliQa&!ex_Z8J)NIdOX|O zld!UK|8mfCdeoflJ_xjy%h!hb731C&k{jX0dXe-I&h=8FFwWpoBJ`DgadJTR?Ss5S zc9d5lH2SX_A>GjpkSwkE<^`6%Q9#_84Wy!`+l!$i;K`-k!z5?pF2ocavP;yhI@ocm zjfkZMX`SDx8l}7ND`^bvsL50=8_zacHYmi2`Yx#ddgh`he(T_`mc`qmeYbZ<-cil` zAZa9efaP|l&GSQi5&w+I(^{w?+cjvvNZA;6Yqvl#sULpXO5*LMP`z|3l>CDm?x31y zlwogJ^Xl!zkfB=%hd7apKPWi%x7I-V|LLXUvE1{d3T zSMkZB;`%RGeT|2Uj%m;tNl?mCOROll%1_yBEGp=7KopdPI9WUx6NK!EzAq~B7pFHx z^~j2-Pu_(69Fi}9cf)0con3#Ji))IEHeCb95bIlOk1o5<)Jl7$M8-~WOP9>x^xoWC zB(&VzE>}k4+}GD}d1<6^hs(>e3OO?~0@6Ei7em^V&bUjzXhXH|h8k%7+h2KW&xBXg zxzOk)-dzWxi7hOfsqm9fLl4$(8yW1F+Vw-qsH!=y6mw~<)zCRo)nUMo6DHDfDGS?L z?91O;A;SICxnJ=#{|ACd@t{=N)c*EX2bnL~BDhTSGslj9)3)r^BfFS8B`N@)tMYP5D)gtbpt#}h@qf0m@A@m~I$5Ytp|knsX)hK)dsk$`X8m~gh`*R(8q!+v z3JA4M3||m@4+0<#;EC6EqWx^eANF|t3S7A33zbXn2csW;IJ9QfL%}kA^urzp&v4x9 z+rRhNK-#f@ogQm!kH;P?6eWjR9vbGtD0SW=E!$uIvs?5H2PUOJAVR2deB@4AtKvK-5|I#_`uxgujzera9%0o*1tp?a)AIVNvM+@q z61;D8e5Q|b|5xZOq&j2~7Vncthc>re{Ac=P*(BBY?@aMvpPg<#iEkrIIuT2h1@-vY zkM*liT!Vew7wOFHIH3dkI77Wu1gw?eo>az_)EjK$L0wGsNAPG%JX!|D3)GuFRq#Lh zmpInz2O5`!nZ7+Bh?J!!OroDn9?=Vl=wCoNJN-GmKwCmHFm+kocUB8UqlFyrS$&O2)(oV(7fm#pH$r$F`fbvGNnWUnLOW3m7)YKds)6m#yzwSdD zR`5i(E+BGrHop9IE-xYBt4wP9nPGKOw*gVZExn z#*{*Y;68}^&MXbi$~xwt97aZl97O6p~27DP3xYO2@z1DQUjSX_cQ&+k0{A30m`^_7PB%vupedAQr4M z?+>~+O>i}0?tLg83J2+zfzHo%a0>XjDb9($Uh6tLNaBuTR-xxhPw|`Ujw(aQZE+ss z4`8c=pYXiFp#j(9*SO#Nj(4a2efYY}(o_ogk#_jBIArl%_?e-R14W(yifP9wb3Mg?%Ij`Mk9xP>Nhz8ycosr_sE?K$Uh_xT6sA6Z{67~dx#J7 zSHsz8SwXFX5fzJ1v@@0|lV3?q+ zZ4{I*)$;I{#IeJjN#rVOfu&)mk?D*3BD7vi~ z|M2+-r4`yagq^;p4UU-+`^&Aj|s*_z)u5yaPH&+sgCovi-gUke}%9 zt-5WC^)?p!7@5+&%{}?hy=#g%8QigU8x?3v#$T|E;i~?1730iM23<*l2MKjI1^-9g?H`L-+ zpu0r98A0iGul+_pgwhd^x==1>HX}0|(l-THHIFc)QB&z*sLL=6Bod_8}5 zjvU)ixU%sNWMKh}Jh(e>7lnj5e#w^mSae|E($0y)CUx=5Esp$r|FxZmt;*Kj8gdZY zZG7G|>Nqi317U|WXJyQ2d2n}c2w2;;9Iq{X*3>fv3xzkaYY^ln(^BeAjOPfa)mYuj z73XUN&<8*kkJm<+D`1a+D+sczv!!6q!mJY7idGF8|C`5aagI3l9=XZ5nE7o+2G0@t zprkF$3>^xuekdvxOa{k)bSt-V&X6APDSo9Gp_yYQ*84gbtr2Ru&%y)dU5l!o#FzCG z$9LZAkmLd=(aquf04jId-jWqy|b;e}99jV#{(O|KOOjQ-@E|AS`?&5 zAt)y&la>hUyy|xs+;8!i4V$Fs2Sg~(*IkZKZ+&>_J_k-awd2YPhuWOL`9i_&B-4>& z{)DkMv@mIFNhfbZ$?I3dM~|%tVepuEU9@QsE4H3nl*b36oXiY{c^Z3v0r|w^Ya`HT ze+LmCxe2OyeI=CTH9{C~_67Bo6&ADigBL*UTX2BALUh|OcTNZK!W646PQ+)ae<#h& z`Vp@pjg1F=c#{Itq&Ba{K zSRd)ro04yPOHP24*yo1(8-He)3%Of7qiB8&I!)3vA0b3ENe!7RmVSpOZPC4U+|6<+ zPR_#UIk)6IOkK&)$E1qpc1}`=bJP)#S&DcXP6ZoyR!5EV`N-^xK|xz1pWD43xzo#e zt0|MDkJuOgfD$p3YU1_;rxmR2k~W~AO!+WM#MqB zNHy0`8?3H-wYt>`?3>?Scps{zlss4KEZa!K4OY5QYDdd;XR7%>_Oin=x*h3x;RcnR zG9=kLOar1V_^X?9loB|0eIJHo;xtjomabN;4dn77s3L%VZw)E1>k*E&TY~DP41G3N zP}_oF1IO$|l}wdM5^*AB3ygbgSE5r5VZ`;-zb<_KzI>j8iz+cULqT_PLq?BAGOa9L z6xXXQbt+n5{GBx$7?Fh?iq1;&)sjF3@a(*QDA!f{?JPOk?C`>Vn_}BZoE>_DE#oTl zVDg(Bs7!a#SGY__mIFlXz(MIy_r-ce^iNmX$}Ff_|3=4+cbmqYu};%xfSHLiRz-43 zYG}e(k(^8Rg|Wgkx<)p_jc_*Q@Uf{{n>EAHV^fVu9YE>qY%T08B6kXnBV^c%UrAzvfz=!Cz3^ zp5k()>^(Wb^G^*V@u9=H3Lw%LFm-M<(m-q5f&^z9^M8;hjKw9P>B10SqOOgy6?~K& z=>IGYx)Ox=U|B!2J7mTXc34js&w_vlw z>%>T`?eovjo>Gb)7c*v6hLq@+8}Dge!{5JqH^0%pIB=wLGthk*rL0&E3;!E)XU)b( zN5z2a({{u@mKst0A>keX_IJQ%zmOTlQYfnlvqL-vK3T$_O<8};`nrj2%9(SaO@zFugX2?J1)=DSbN zwNq0^wzB0Yi}!Xe^5@gCI`)B3#j3-HDuo;eicOoU%RsI5v;L=Z;$k{ok3EnRoV%LO zBoFG)vIeMK(Is@#{jMY6ew@^_%DW{;JKYG)|5EkVZ}G&oA6-M#4dNC+usX)*wtC%q^(&G$?Z z#qk*%>pAt@0p<94Ke z5D!5KGUOZ%N zWt0C2LD{`>x(co_5$GFwj=JO(@B4ikESVX|fVc8}H+}@|I?$rZxJT>fmOJnw`*#05 z0$jJ@fLBwVcXC-8d;!wEu0G)8vkYci?|13$$gToAGCn_FZOLC#ab5pG2_zRlE z#SHx4o@;MXm8<8bFko^N)Y)TJ4J?ScOyR12xzvpB6)uPu>3x{F#)Rqj1;s=yK(2qG z+tq^{n@Am=t3O#QWB;m*%(JOFPVu6{c;aYI0kWZF&HpU!`&kuu13Rbe_K9|Jg7dyu zMIy^?0_Z zbVASz7cLz*!5M;mciC2XD?&`pN7ulEvy)WlRSubU^BLwCv##-X<7nWP&nHVJq-)Zk z9a~e-?*u7mhf$7$U`M(G$jW!}89rudn*X~Qv}vTlusU;RKmhM6Ga?YA(_%UW?F=0c z;Fp_CNwI*05XF)kY9JRfdOVFA$U>IJDg{sY4HP*oJ$Jmx+!$ZGzas){rNDmCP8^yT z?-VBf@b(RGf}mn&+idPF<2bHQx~5`ta2v7VJh~G6 z%n-6!*1y#c*--6XOrFO3#_tT4(k;hfTE~|3iP2yVsWh)AS&_T!Vcvq1!ikd4SNZ2P z)0uvoV;}9_hx;S;Qe%&RDfDhKl`lDBCk)T*Et|uu#9TL51t+}N82?=2N*FB{3{veN zEPw8_ceoT5J1_~SXYu$3mNAGTY?%4gy8?k=d@1fi>d}+XNoiy9YoCYYAq5IWy$glavcw65am$kf&I+OQh?lJ zFWq$cQ7-UNUza{0MPNg<+NY{lS2+&Hr#&roJu?Y-|oUi;bhSrSixUWkO0ea`_@nHy|U|3eJ*A}I- zqF|Ua^h#2%K0@J($*Iz$j_OH71OAC5Q=M?z8M13GS2;Fu2YgEcgDt#=m>jYAp(YiM zu#Y+C$VC;8E``KISxM_FmM3zduH?wg>=e#_dL(4^il~KwD^{a>2j+|G!W_~ZL3NIF zr;*FsFZW;w9~Iwyd^gA(kCoF(GWN64%)+=8XP^x}&y0k%zdW|9iM;obP{J@+omTMb zn#l93yp1TG?$W9UKTpe1n62mShPgD8X`duWXuUvFGukB?^uta^;;lNx704#Zs64Qp zKd)b>@S8d;6P`#L6?hJyI$QS3r>sTEcc)8m`(PzcTD6P*y;TmJz*(FQ@x(r$A$&GO z-_sVbWmYen?tj-j&$)|0Dqpy$h|bctd(mo@R$z0+;d+XRXX_o#b(}w3`qZM0;CJGy zdpkM2zyR@;w%RqcSjRfzS%fLG$X^m~I|hx0o2Uf#L8*kn=qckN&5@QQlg zI@arq??ugZ4rdpu(=a|76nNjATnK5R9QrkUf%x_QT zhnF_K^q4Q|o!fZ<+Ze+ee?h!xp>LJ{Ig|9g?yTFj$ZK`5?vX7=3Af@NC9-J4)v=S- zU0Q&KrKNwZtAvWwEO)RE%hXThsS3n5vKWa>gkI(w?&;HhZ{Fc-`vS%oasv z;+dm$>POefdhUdhNlR+M1~?m@r66fdrt$~??)trKK|W6Q;#|kG3|A5hPs5K)DM)10=!M91}l{jzd3zIvgvp;YEaE2rQEA zdGw$cIF%MmIquvHL|Xi}75m@vKb(idiLb}}9$jAUtC)(1@+Vd-twIHT4irCYpglvc zU;@f)E{2{<>KQ-ACHB|!5s>!_@`ib6cmznVLis6H$?CFT&ip{39lwKQB)6XlLM0~u z3h)cbe#o@ev(2a%*K+pysHA~Lfd$A%z#c3Mzhn3MVj1R^)Mk_1I2irT+X?vUR|rKB z0{=;KN*TN*6qeT4I3``<7p^ZY{8ad$qGKtHmt(W-M%6I6iBPMnU-Rq%SCgBU1%1gz zupm%kzwsLvrl9twvXvXmv`Xg7HxRz17c8c@-TQ;Q11!fp`uO{HgE~h3nK((3yK!T2 zR}7%9LFGRAbSdLdY{VKgi6LBoZK6^py6X~q40%t5jZF(Bg%aJ9z`ZD=(Vnifp4&H? zY@vFT`gd1L;fe}tz=2YSJ|d&(1$1jpBDPzkowqT9V*ECSj)U24hQ-?MWJIn|@n9`- z!pG@d(M&=XAK_n$Ph(q=Ru7iYn<<+W(+14bk82vARU6(i zD^>8>26Essb#uVcafgKac|kh(`WT>H#B9sNl_|{hjzP5GbwBrrNU3SsD0o>ivWpTOWK=Ak||uz_C?<8gK@iz4e)ixbrP zsR!1U?z&$o0RKKhJi*g4vWe%PQ;7ary&3H0o+z%kQQUk$h3C1iys`Ts!ZUQ@`Eo+X zT6j^;mw&BYaUDxG2vvh?)CN7F)^M-!w?f3cb*-U-bFpdeG2*s}1Sah%%W{`*?!T$2 zcNH)}`1G#M#Lal6`dirTzGrNVmKZwaxn+yWrVZ4JS_G!|s&%g6m9!V8 z>w#S@RKL8XUP|RcxRmFQTig_8P<@s{(7NyDa`z9HwXaxTSg8I(rF+d6IfiLGEj(D< zi_DA>jC@Ijo(sQ3=BN4Xrv<5m@m0Lf9vTxAog;V95gHj0J9;2|=zjz_xAQs$<$M}x zqSh`GK>T4_$*z7;pxJn6=lNf*k9jQ?tv~wD4m2pe^r(e~*OTsh&MX9o3kG$9*0Jf3 z${AM1Ja<{}7h;V=&V2JNE;X7o{+54CL*m+CYCIAAG;D*3>MxpfZ{a=MV0s=V=Y(%Y zXa#N7z9|SAp65JR9H^u;G)_pa$bLLGGnUZ2UVf*+abV5TziAK5qnD^6atM3D39*2h z#X{zLZ|6w*NoQLqs#aBQA6xZm9~fHZi;W({!|7n9T2_s!q9twX{x67mp{lypezgDf za6odAA$%CDsH%k)sSPWjs6?)_H;4FS=e{;%+nSebzL(A*dJDOh1UR!`UUgKY=#^fR z@0N#vmvuh#&Y}M$%?q3doddTmJDqo)p4*nw0Ykn`Gd8GE#uW{J)N1+NJxuPiStSA2 zr)1=UTdbSl=+Ms7%VlhH{fb;%Lw+8z8PM|G9C}5>HnnrPu~GUa(P>A(^-BkIN+}`P zx)c!~cYCk7pua(SF<}y#aacJMaE{WC{XFFaGLOb^ltzrB^7m_)niKBD)dJS97&vrT z>A+Ne^yGKX8&Ct9;-JjQnGHGg#jvWn!>hD!jF8ip#Y$9a!>?s1=5wnZ*!EWE{L8>0 zP!8ZV*_*gs!0IgFAzyZ53mN&h*kj zoeK_NG-BsIab3VttSrxt$sm70XyYa$R88xZ^PJX21FiFnsjRo$3t`CSAVC8sFfWny zP~cNA{+Y}pf!%_8>8h$p9Zz@~3_lON7v49988C!AvXVl6MzuiMnh9;XTQsRB=e9Ld zYrnl3yUw$hr6Z}T^h~!9Do0I~(OP2gz1NMqyBDi7=x9Zq7~#+|WX@#og9nI$_x)Mp zX3C{Hs_|hP-safBw?VU)H%*^h49m_sAhF=t+C$5&4p&osrob8A4!#r}0jG*eq<=xg z3VnYbNE>~90ith_28P~(NJU44T?*ZWFu;O}m2Jo!)LD*;BekA*2f5OxhnE_(x6Fh( z$tUcQgTxK5UH_}=U}o^)(17!;Kg138cFyS5Qo#~S# zHq*lA$F_EhGDb~RpE|7Qxo4%wa({Cglszer;+905*Evo_DzCJgzrj&|OKpaj_XRhf(*4GcnbFAQ5M1~A@UtiyWq zMRgi|H{FBHgd5GS;0ySwBXuOe5m^q_=?KuJ4vc~Bz0qLoCeB%|l0!B;H7mY-I6e@l zLlhws2$k_qAU?~Vx{yi)dMi;$7rNk;j=7r~?>2KD^YWHveoQh+U&-lBqYeK(Gm=uH zNs*27cWR_=j0em>cK6WH>DqZnS_$=#uth^VI!vTx;9X8NTAQqu&_u~nlu5%NbJ_1u zB^mXii->(7rqiP=fREEf71w@BEdVp)9B{D$VXM1{WW<00k!=^F7IbUJ*o{e7nH0#D zmf5285#W|}!254l1x@R~r)l2q^72B1XTPJzmgCR}$5_EoQ%>9CQ?f-vf09s#fR^!M z_*xT)Ap1+qcoAo;e*_FisUWEh_vU75U82v%+FVvp8EXiYG^#1lXyCGq)VX%Smjfq~suszJb0dwrb&rX^42TkZRY%xPKkqt~oBcNr(=05^{ zRIQhx&#|=(9|-pJ2&_UgMvs8PDXlvG=Vdi~da3MFM1yUo#kV5nPuM&qPFpvAL0lI! za|q#7CvQS%`R_l3Eu@?R?zkr?xt#8%RK^;LV%{iUCgep77GR3^*3VQK_wU;j+1jT4h*QJ z@10h5Y)9X!IuxN!wc@-VJ;BYhTTn!AHWJl&I0^<$(%9OpAw;E{^@NIN zPz;`CO84Z-7|Ww$e+FEt+m`7NOA{eZa|LMVol(WQx_9L$IqpXNso-z6sxscmUZm(d zDMVv}#gn}<{C-SYM*f6Lh;oXs2G~+>H*(}skAO&M2m5a8;xcZAUygbN43_R8Cs1@Nw(qD|I!N6857XA@vr_t+BQc%S;oG7rH`Ib#K+TH4d+5OD49K>vnn8?QP_ zSgv~{(`wWO#Ei2~84c7*X7ScQ(>9w5g-bIQLln81T!JF|nWMPy)RaVRru!4~J^z5y zGMjCMbXR8eYz%-2LV#L(S)MGP^9CcCpWdjNQ~%SdKKSe3fm$S8O2IHVuqP z)Xat$t>kdv!tS|3ecvU$e}+c64TkGij)01P$_QV1%kKM%9Rbg~`_{pn_WYQ8@~jV= z?ORf(`>nKbq+7b2pSor!X*CTZ${vZO&h09x(MU;K%n*{$iKpz+{i3M>`V;vENLSgx zROUM9v<{`Rj(~-PHRy^DzX83!T0vkW+T7SE-S8Dwc8eR&Fr$knTg4}HCt%n2$JIJC zQ5dH&rGw72u#+b>WwWva({MkVQYRax)3ODRfHGfEb=poL=Mm6`IRcdK6!=8Qg3n!X z_=4Tmt-IilncH{Cas35K>3@ay@^Ilq`jbekyfYqTt$wNH0KSNeP4%DddmOb=IdiZTiR-Au8L{i#)$>qe}{oD>i zAJ&x3L#Aj|;k1#kEz)SrQL@xoe+}8YIJ^R?xnh%-5_egzul1xGpIqH54X1&PU#3RN%X18+YP!k z;5R`X8v84ca-fB9CF)aHAO8-CEhxd!93@{rI9|5Z)o4svJ50t@0$DoDZR!6Wr2VNY`mmtnD??=X4sWCRr>3<3bZ@7^1iTK;NLl3k zSdYk({`}J~`K9V~p&@3^j~@PGN$1hU-B<%p4{70gC`$6eR!aIa9_I`6pPb1`;l>uMa2>*ju` z!soOA-M zR4MN1{L_Ii_n=w@>tcuaNUhZfEq!LGR%QP6U*`2WJh__-JxL8QF9kQPGv}*shDM!~ zWQwRDkenCev~-y`V~lR)f}!~X=6b>aUi}vQk!sj`3fKZ4FsRpC8CMEyXP$nd04j`r zQkeRMmX4QPg4#@$L4v!jxNLMg9x3eoR$I90^X9&FQsP&%+gL2cDLJXsL?t%h%x-vr zRY8ZIonM@G-|q7xz)oR=6DeF^u&Z;k*pNusL7X3HXt_j8=zn_CGcUCDoOv=~G~qWz zzhfq;b2f?<_a~@Qz&iRL7_y)tjw4AOS6+~1irXHH{CFZUqC+yB7zjLV`XY%iF|ee4 zb_+A(`am8kZLH+aM+J!pyoZiGg}k_!k$S5LVv%iZyrTI8r0d!zw?};bSbySsSh{|R zQJ4Y;!`pq<&?~oo57xXsHw*XwTkZlk>k%0K0=X0yk3Z9W37Bxgk|3b)T%n#96_(Ki za*&`gdlNMq$EUOH2bKCfHaH_ zQBb-=a`TEJ7+hM~Ajd{-quC{uNk|OXXk|z@NZ#M^`}qCA85^*(vGaJpU$57*GA3B) zM{UNoJF3UG>SvZkQ@!J+vnJdBG=bMcT~J8fkdU&snoXq4$WNau6dLwrq7V+tZ_{?# zLUqq^k2*=aZp4s50Zfb25ap6_I79%`=+Kcw;&fQ<&l-(rLSaT*RO+LRT<;c?A9)xq zg4(-@;ke?pWwb5WV`C2e_8G)5M}CGIt{0uFmMTHH;wf#Yt$f+%f0hT~MqgcK8Dnuv zfzCQ#V7kp39>}SNLH1)}@nY9_^8X@hi;31j4vtaJh{q*Gx4l+!-P>UYE=DFg{=m!i z{!CuDE>N=GpQAj#R(Sj1vB^ZZPod=cdJbn-zjPv-0&;gOb?blN>4uRuU@R8+gHyQg z7uMsZ;(qQ<&Prs+&ir^!}5C8{_n!XQ-`6kJ)>T<-Rab@l4<8owP54Tvrk;0!- z+s9&;lAiD$gd5xGUO0C;GpwpHXLSL5yZ=bTGo)CLJBodp#(4^+MYQK02DdP}AN7=g zNbHmuJ4IXb$eYSjIW3}jqIC5`~D}UHYzLTF1O$l4Cd2p$-#>980!+(%Xe> zomD#Z|LRWuHfN@I8c0?+tppcw!Ptg{EytGXGgsNY0fmhCmUP$P*v299tpV_S{cqy7 z*5VyWebbG@-BX-8hmhmtQPYx|u-o9XZZG%9dcA%^Drg0t=h*-gQ(Uamv!UV1--yaF zjq+Axjzh#PiPlDnXLtO&hp4jSm8`7oiefo&_OWR4f51cIDus=fC&A>U5<(S|3-0he z5-TSig{mBj?iO{x>8j z8Ry2$muoM&@f~8t?1Lv8BUJ+qk(0ll6bomgrw2ecCt3eUE7-))sp)9sOU;trUz>d! z^u`MkIj}vUgYXT|g)pe2lUw_wo#5Bdl_4)Vjymbt;QI9Go7bUy$Oc$`-4}mLjjdmf zww`MWXAyR;Up@~Gp<}>nXKloT{=tL|C}?R+2?k@#QGL_+WgF9jD-Do>=@`>9gNZxP z4ZBd)TrZWH1diDCNH1^jrC;*3?2`T#haMbUdczVavFjUtR#F5x5$)n2+L&wWHEy zxqu(xF3Tp8S*__WNTbt*%S8LdtQNvPTEJx)!x=TFunaw$UG7b?U#k_@4cvRFj}3W5 z2i}c1*^i%{lD!L1+>=g*-j!u)L}R5f z!HK25z|q-L^X2KW!+etD*iKpH`iv%R3pjeGK|NdB-pK^?`h(y62Vg=KjlmjzG1!=} zCvV^nZ832#o>~V1%_bXXH#)6Xn1wt_Ntkb}WIef6g%6d<8~iAO2wb*sY$^0y_HxDT zqcheGL5tVfbNCtSZpxSA_WXz4?#HvtIWe)1C$^wl(@R~!$&&A}w*R_*W~m%|3L-7p zd!9eC_?DOZ9KgKA0=Qi#JWFUtB{(9iJPcBxbTX}7=&XGo=O1wA$LL{oFxRbD%kaPb z&q?xsDzfx^m?iMiy28)fCkp7OH6Id-1l6I-MZquIp(=mL3n<`G$mP3Sz!YI7w0GI& zDs0gS_a5S7kK5=Str~e?rivXc_^11@CqVCP+W@nUr?dgMqf3i7ORS^X@vyOy(a7|7 z3!{ydUap}>t&ztWMPt)*-(`!$wXCANQz z)1zKwo{##)EQzhiHMEHWhDKW@W}Mz1K;$Sv)w%xx=Tr}qQsccF;pqO;sKp%zzvU|a z9gSDU2M+9)VmFoXqY)o<%Ufr;Z4Brsep&MjJ91x(iH{g-&SZy>4b;tbNOXzJI-SX{ z+QlB*^stI6T7kuv@{bhP&B#XHxz$3UYCATEfE9%nhHJ+kX&0miw!X=VK2mQv4ram8 zvVFCp9N4t)=0=H%ho1Dx8O8gs{G=2S;a%&}+f0C{ zv8nkl2Q3ZXqMQKc9Lte|vbiCrHT3M*p03WBgQFawG*&!feNVueY&XI!UunDC_=XsB z5LTs?ojYURhRY({c8L%iS#okx z-56@I&2-3K8iRM{K+lF(eAS1% z;1ybkfi-!&>DurK8Bjj`ivog+&7WRD-chyv6Gh%;-G-H{i4w*BDItK4UEm*?(Z|O> zv$RN3HfQsO7-CabLJx(g`)Hfbk;7;9a|t2>pN?o}$kbjlzDR`%4b3NnI%d-eCnuSQ z#8+0V;U6Pg&4oeSP2p~+XSwlzKo(a)O=E}kob7*rBL9qDXh>W7e?U?KTZ;hYvQ(2F z%=CN9LwT^bMFNlaAKTA$g$0UBuHx_)=3Mq5a=ZTa!H1T;Rg9;Z6pQYei!j;m+Fmuo zGt!WvU-`A~_-fBwC0buD_j{}$zc$J+wYlwqr?D6%((f^^#$x^hBvQ{@FYhW?hlD$b z6#DN=T7oE2XA|PxV4kt=o?PBH_G5wap2b==dGQ;^+!c2MC&cJ5m#WCm`ZsQ<8(@Nfzp;<$$*BedRl(P=tPV=bYA;NVksMpygppQ289gGn^XS z-?$EQsfqNF7Y+Ok`L)n!Dm#=zG_vfsFHq?}U;YshSwWd6x{B#ycSLPWDW0FFnq|*Q zfTs28*5^DW)UCJ;cVCu(7(;pp_Kl)WoBl6tc*&OcHg$BWQ{P-&9#T>Z?TMQ=9ux>x6v5hv~)-%BgcsC~H7P+MrA5)v0$V~e1J+>D3Qt=EI zes>0OWp}1{UTe*8)zxdj=qRY!K2wKQ$QoXvt>P#2F7DfBwo~1uQiGXRVkKx?#XPic zu`<;+Cjgv+X_V)~O^lKNmE*W!gWD}*j9qK6oL{l;ZSG9}cnGC^*H-9Aihn+3k2)0? z+L1Zv$|+_3eyX6WGUiC3oEEyR=Pl{UO!z+J{Dvm)4wyKFU_$>gr${mF9)%axf1s7A z;ca0>lP`2-9J0cxZ%^ZV{cS_2sa)h-My5WRU6Xl}#L_!79d#pkwp;V&ittT33Y{RT-I|Bj zP1me(G)orvy4%l2e#~lPBK~j*>~MU+^L(Ak;-M{#Mo*PI(z?88y8%;h7&aSAtFP+h z*gEo)nDF{{QQV7ss+#R>;5a2oCkKe|P3MOV{Np!KdeOcGa`ohmn(cQoLY}Uzd(&U; zp^;Psqz(7W;zw$YQWnDK>H3S6aLE$CE+ta$vi5n$?F^Y8Y$&g@PlM~`e)(^j+)e92 zY)8y=Jom}?4}R>yeRNV}fx&;ko}Bl!c{fnGAM!qK{QFuHxcoQ&2k13sK3OvIx;zTF zl>ZuP+gfv?pkMawCOLTDHttseJCu<|5B}}O@b>KMwXlL^Z6)tsPd(m}z71H|o@nCO zKrd)F49X05ABRrb^V?_lS-{P>6mox^_|3?4*5o)xk=|u5O}fZ6y3prJBKW7Hk_k!r zcUPkLV+wtDoK%Xm@MVjaZcH-}bC&ouwi|;seN~SGp=TeyTaPZ(O=?&anvO)u`2U?g zlx|g(In6BR7yNm0sl+dBv{Rj0&NOEIi!xq8)N|fxioeDzIjZ4{^IZVvGH^~!#EQSo z60Wl~V>C>~8w?u4Y_Dy}2}?Ap@T|e?og6ae&*<+&8m?mRb9jfoH90jPW+16;9X`rU zfP7`~4Evzr*YxcQRA#7&;m?o#m+Bq$!ag%yUlFnoK5YuU9yfQ=5F6m2z4X&{W13JSa!L&+xc-IPFVkv?0#1ksv;d1#gkOJn@{V*- zNry_y?c!bLsD`)iVQ}*jwdfqXO?duXhlv#i4F}DKnedoNOtE#3aaAb9Rk4Qa zwOE>3j7}kmBV6`*3fY9ZhU8&4N@_Hv9cTOb zF(lCngV2aA`=>?z4@ zI(B8_u+l)IOXmc&vX{}hMa8 zryjs-UHQ+7$o6T|MYCz)=$y!s>ol*lytg8N)4-Bb^c349Ed+J*2$y(hy+4ePLM7!b zlq9_|>e7CX{vN7hf55ijxR1tyQ6i@F7I;gUtHaTY*L_6uIkve9GvyXJ%r0Z{95k=XbbHO81^J=#}ZD|qRVt#{aZI+?xpXGWJI25e+ z92}eK3D?gFo&Fe!%c0Z!*t;P8U^}fIVxIC9lD;BeTQkV4Q^= z6zE{OKmhc3Aiu}Ym;e$z`sRngO%;pJlQ$?z14RU;$iU43g%ShLlZhW$LPUzteh+7M zU<#7(qXn#5?RJa~v6gR?piV1(9&H+V$qdeTitXgd1>JLEEe#1h^mUZt>Epg;nf!f7 zWKT|@Uc9g`=m-Ns-FUn(yI#jHcT;#+4Y`z|fIilo(d!n)Mhl$5&!J1!(cbJj>WQ%+n-Q zj%jt?Hy=tpsN$wflDn^j{02^!usqUXlY-ZaoN2#m3SgH?HMnGM4Mt&R=c|uOH1Il?-FmLP0z89Y4vknqo}xw;hr{skUuy;o$mDAE_epj zbqID;A-13krkiOvQz^*taNhj$6mQkcD#q5TWv)dy=HVp)5Yb(p28)UP{o4W14Vz_Y zV_NW8iEm5=hpz2TplVlx6yY2=7+aJu`5g9eoJlugM?M0t(5Q&42T3{24eyN-*OsQl zA6rr`^)|@*1qwSrj>);7=5q^Sd+$_uRR+Wo_-AZ)&KFP1^~VfBH*AAd-YDmfs>F&v zIBLobO_&nbU+xc7_q<-*c22BW7 zl+MZrVTlczIEa7g;VW$Y&-Es;bKE12ow3&3pqGeP{nLqf-;9kHCo-@DF^%dlc`^ty zE}I0F$dAcfvag0^?{RCUdhxR&gq=SxOP>%`L{Pdxp@NyEBeRRH7f-(qG(A{$+!q%J zT?fq}>VJTp-%CovD&Km8Dk&DU_U#m5=k(p5BO58c!n~yEVs?WkZlwqqOA|}D5!Gs`c_+^aG&Fdm&+pY#Wo^6WIOSH60Dg6Nx^ zu1&IdlUVNym5cRQRqzB|b8+6xQOJr~n5lzvpx=!khMmK*lP?_DSa+iQ1^PY^-Q9K} zQ1={sPpdr%%#@C#o~VpejKViyaBuHE1y4Q4pU~;YNTDC`ISBo4UMgppbszRU`)Y3F zZwH{j0<+`fNXic#X{1aITt`qxTcvXEU5q@!FwR4#Vys5WRc9HgDXA4vf*0pas-;*L zUHljDQ44_mQU5adQTq4OJv^F}JW)HLPo_q{f_@+_Vc~b3P!Ewq*Ik zjO@>Z%ITod(cKGQ(ejj*aus_pSm{P+HP3gUb4iP!s zL1v`Da*Ri?_cX(fSS>T+QiP2R#CO!npzgqoYd_dV_TjhpRAycE`SlsrE<{g3aMMe{ zfBOOCn&*6pxVr3QV|_2mkG7r#2|fBp*rlFhYdSwJfPW=B)L{RV^!QtZ`K?fBi_zS* z6Geo_TikNSAL z=d|j67=(X3h|xx6nkju8#xso10{RC@lq^DGJcr4uqs3UD}>Za;G zACorxZ^R7W_&0}2+TA``FCtQZ*JFu{;Wdc8i4heEZT+s`e(F_bZ;j&Y!w5fo-{nBq zMDL#Wb+1roI_bz>JKU68Nj3=)xqC!NC#y{eBW6Z_Ij$(JoI|~{yt~Lo*)N4QRUrFn z{d^G@!K-zwr5xR1dE20*tH_;v_~8-rvvr%)e@py2Q=LeGBTLyfw9}^iiJlr%#QIOU zY_G0iRtr?%=t5}4zs1?BB~LDCdnWMBV4hF4sZg2xnyj^)VhY_hl%$my ze~cs=9U%C;yyu+Af(W%eVo?fIbIrtq{^pV4Jisux(e&N z$I4&$~s$P)Cd0#n9d=~U3Kt#aY9xZ2cL$#h^b+|={}Gkh4yqn~ie#n1s* z2sgxhf~B8I+br~)Q*#;0wGJa)!Yx_M(OBo1g&Xx0SMPI$hvc*szlZd$zmHbV1iPz{ z72@zVI;SyaX_oZ!kkjq}qVu=8iUA_{NLw#YM8Jb|V>i=3h)L_EHx`FTq-2Pau7c80 zye$Q#{}8Y7;BQyI%a+fNWl+~BY+?1N%R3@U&f*T%{SmoQDBQRDvYa(&0F9utDJJ8+ zh~r=lXAhck7Dj#ZoD)eV8Ad21Fh`GuLHMra;i=FuFWUz;`>rAf){=iPX1?D|<(t_M z>%QBDG4<&5kSS0nS-w#7Hjm_GVP$Q*52S56B{1_^{Eof}4Rk_gC{Ku6hZPBhkgM3? zaVlGm1c(S_E$SFZssA`aM+`Oo`uNHgEpl*OaY7KalkPqWL1N=n!hoH$HK5XX(Ron0 zve*;1DalJcD22ekNzYK3sEv|IjVO0}I!?k6T8fP#;g-y1syK|(jZJ##P#X5BwB0?X z5sK;;=1>P~Dr6@1iwhum$$i2Jv`|<~_f~bFZB?ab z@s!+MtQD8730{Z%TaDVLKBvC6i8w!DX@$ZaQ%yXwNay{Y*SDY?LKO_kLUDlio6*)~ zi2O((psy)0Wp;(I;M2SVVB18cqJUhL_lFh<2;1uWKXf+b$|t<9qj0+!!F4P+N10SL zbb9n;N=Y5}FjmtyGHh>oYLn3bC6h zS@I=>+yH`3WAq$CAIA9MJn4u#&yT(c2n1)UoF{@psSTB%+izsNmS?LzLw@Il#vDu; z0yW)31c?g7SSHUM%BDi5kCCehr)EHfUKBvU$j3^Pcwd@akQ5L0?Bj(w!i_EPYf#5B zO=kpOS?TM5%ql37nkQYyFcw4tRpMk3lsh5sTD=59Oal|dkZ}x*WtMfwGT*z$%vHjr z_LE_IeFF4s7tJpcvdA9=!&fOGA=8YJk*_q}HJ#1=_fGyGNsrHSPR@!`PSz_W>MG|Z zwJrS`D!JIgK>SMM&(c&2NXwnn0HV11RB*=Te*g=={Be$}A6fo0eWd{sDp&0|^=fl% z6>2|K%MZp^Iz2P+xyPmOR`zc!fO)l`GkYa19WJgy+A+Kd?Ml2%$)7~LUHSNG9pO%n zQ~Er}+D=PH{s*9hBK@w29HNnV$2FZCEwVXZwa4%APibO9Lw`fsayTpxAn$UL1Yb)k zaT+yR9uny0T8;M`z-qlSf2^LfZ+z-)SKTwXuluOq2YtAT9VEgTWH`zTX%9qjIFe!N zqXedU-^9K!v{Od2K-i!JrR22@%OnEnI`{E)3p~YD-#gPr6BrswY}_ER)s^Yp_#5VJ zNH5dNh45uqUOYhK*y}CKj!6Md|C-5s_g5xJB=9XcE-8mvL4G=}sPv&RA-QDVTjels zlf)=e>;1az-kY$Z#y}s}6<(BZWVE4BhsX?yJ%sx5?}k!Dq?!QPG@DxA=fP|&yMh0q z|Mu57^Tbd81)T9@MrS?=UfSTcPLaRLt$VX~;!%rw<9ow>2z@x3 zqADq?nTz0G_s^@L|Hcuf3f)7sBM#RC4hA`KwQRhBpmy>Ii<6;@N5p|tGGv#u?GU14 zd5kdBP*!+JVNtEv{^7VT)bw!!5 zJ9q_njS}dx?nyhTqTJ^q?0k>U=$CGH_1@!8Jw*O8xVy=}$9_I3z2zN;S1DwVlY0pd z={*boUr%qQ#~R8>KF&aw?Azx$k6(kixR5J<<~5jc`R5OJd4AqMt%06>tbgD+A=qGu z7+y?0DW{2=jI`7+I4=LIOc4}UYUow1>P-R*D|$uHK; zeNG{Yb#hP9`bLemJ}`SfZ8jc_`A!xnl5%aS;84uSWso41-+_wp>ebQy5NF*HTa&KXJC>%cO#PLm)ke$|NQid3G-tOpUm;_XYxV@fgKH}f#_#NLlj2oRkh&%L#ed_<#ZY_`9!51-v{1E{<|WgJ^S)iu5qF|csolk+*I7G90&F>3}6)Y1EbEk{?m%qvM#v`;& zaQR7NHPcs9Jzc<$@1S`aU+a;da@PMQvF~ctbJVu+xUo_cY3khzzb1BXAQp5dYHS{W z2fs(wb9MA@-zI_tAHLWw_K49XE2tZ^zZL$dw6pW|FJ+uE?zi)j-XXn8YFH6*hu zc!qcla&20>>6nC7E-%aVg^4~iY0G?cPsFD|({XVf=Cv17e=QyKEEgfHTG8q_2x;wB z$5xz{@pp7LSE)=+1TFi-`;SnX+2x+=Fj5_u{CL5*r=J7qD zIX_Vu?LzAk8C=5|;Au-y;H-yWLdtB+_(4 zpnVNMCL>S!sFsSc=)m-Z0(4^Bzq8XhD_>q>L)9S#>ZKauwJG7gcfu zeB-u}8`&7LJU=gFho0KA$-!ya;WpJ{%Yx?d8d?v)BlYN}FIW&&@24x#yc6dYZkZKA!+6n#=hT zMETDV$#KRP9~F;L9faSpn@V%!`HwJ_4uLF&<89Q{$teIAIR6{X8?f>Mu*)WDy0Q=+ zp-i_&pc@VcXx`8LfcLA1Sr;=CSbH^5r@qZAw0E!3l# zwlRA8bBD@o^mH_}P{eo9^7A~nUEuXpKzL$^%JV(=o~%(8n4Ox@=6aV>LuI0rvkkR? zL(#%Z_n6NKBru@=7~g}|J6soFXo%W5ZaIi(_BcbNk?X}sDVAN{FC0!}`)ydxg4{Sb zA(eG21zpMcwW^^yx5n;m!W2rV(;{^&No$Kcj4|X$OGNbh2L+h{q+Gh-oQQcipK41uHDse7;(}P0F z|E&CIP|V~affdV8%6=Z)^v~%0NtxEIPYWt@=9VlhCcc*msWF6)J*irBk0&6RAoQ;(6+zW__ z#0_P;lUoXW{~WBd_3-_}gHYhB%T2!h%IGEafYG`XV@Fh!7+bt^0+x`35+ziwztqb~ zi1X5htBcZ7m&xFpr|Z}TgmvYkuCU%xW~{Gu)Uz7APqtxbDRTv}+X3;k_j0V+D?06& zQN4_uLr^?Q$eu99d;ukyT{Ko*b!B9ZtkHrJ#hCo#2n6;YG3b?@OLZQ^Vm+|$o%SGm z0(dQ>5Y$o5Nbxc_WN9TSHzcW9!Q;(t57gM_&Dlj#PWQfDcKGICBREfSASKyPP5mw` z$*CsY2`|Yuss$?J^z%$2C&f5g1#%%r+A1CJa=G_aNp!fp1+sXCe_+s%BhNq|muR?&9Fn*US60Twy%b-xdpF!-msjmzh+VB9z9_y8xO1s2h_KJ7S5cq#ePO%_=os-G z!j7JRInlRXQMXyhBNIHy=F{aZ1?OIC8)Pbx0_#n@=r+>~;no$8ThZo|^qMAAu0t3K zM5CRhgVj8EiV`cs235S%az~Mco*LbM*?|^U98dOe^)OWqOIT5XETbt((+>TeDGX;Q=;s;nV3zV*PHHv z8Yng@O=q83uWvG6JcO7#^VW#UGzJfeeeC$yRV3t)TBO_#tUcCNZ$O0C=6RO(NP_04 zl${lL3z%jYSwHM5BA}6a48{%g7jNMPHE8{EpbvhomE|P39TX9pIdPuqU@A+QgLf6D zyt`T@n7`Z(^L8Fo#U~5fC$M*;%6DO}5CREo*`Q4UL|h7bjsR)bmiyjgqh@bhgz_F`sp*yBxXAN z6s-Cy{v3SH@@H>`-><>_8DtSXX2GT3_iGr~X1wtu-3ZqwD0)INzo$zviQOSQskR1t z2kUd#bLcEDFh=+e5EXLdLGs*w**9?yZ+mm=7Q;yTw2fGsKi8-i_ zUxsm4wVY!~N=KZ!a{S*A?>n! z3-!Jq#;5f?VTie6*J<JUM-%QHI?g+@?zXhG4yCs1(4BSB{T(jmS$2yL!h#ktSu#z#U2hH-Ncra zZW6w{9n{G?8Q?pSHnf^1t;CQ`lj$ls_^FN#pStCPFvy|yqtOxAB{}WNi?H=bm+VN# zDJoo_3){-+%Dn2^^kQ1G3U3UYxRk1|9OB&0Sdb3eMRnZ&EI7S35+B5yQ2 zst-`xw1ARk=ujYEZa%{1Q^?RHKpqY(2oSOtP-MSt(NqRl|FJm(b~ob4?B^~IwtTC| z;tM+HvwWE)uoP-pjw8t|;DrKZ!JNaf_U9x_`baQa4uDB(DDS(0&`F_h#?0!0jd(yU z%qX={+Dv8Fm(qmD*!Jx#X$ufO63#dT8sL9hWT-=IuF|i7$jq(zq?(utwgyQ-ytvDU zCc)L!FVLUVPlQ-;fy;A*CRtDuDxn~4ZYB_9k|=419aX<8Q&!-d^h>I1vDsR z2!Y9Qj(Jh4X6(Bm?b6BIpM7;R-b0Fd;r9{-&Z#!2ZokLEfg>aLtEEhtQYq!x;W5eF z#RTDhEkMSHKDnp?+XPhp@9Kyd%Wgz~ReM_i^T(oodF-$lXyZmb==kjO1vX^3$j$*g zHJfu~+)j->B(Y!(?hHwMVf6ZZP_hNA8drE5K%1+3JV;#QX3Oc04{FcJTG>^QB=O&D zenW&0vj-Hf`AQi{L$=B9$b4}TJWWqZnqdiMlM%-Va--L=O#o|rI&WtBO<+BPu2}^Q zB);IWVu(yUZ|K}tXj7(aniv4a&U|jRDqd8)L6<*xF#%Jsz`^kcB;I~IOV^7#qST-J z661`K!<-r>;_fRT@2B5apZ}=`gMHeoa;UZq56BBviFHDOFp5r37S9yE7b;7i0+r!~ zYM3gHEfckRXrLvBDq1abyH5$Kx|#iGkZ&vq8Ytc2whH7)BOw~r3t8vg_t6m_hTa43 zqq2le0<3a@p~l*qThmM!g(QV1?Ab5dHcFX(r3BC`I2==pxhD`3N>9xpqTFGK`!X}J zn^zjK6=AE;g9U^fPN-rX=6fr|!N!$Kz&aoBqd5+2LF+PtSJz=m)mc$hP>Tm}(1_+l zk*Ki&b#hb;g-W)$@;MGoXaWvV1fd8BCRDicWi#jDTiN{BLAd|TAmSQIMBrwK`TW*6 z(;6L@>-dN?)%+tN|FqC=I{ad2A7r{e5X>14yi{_UyW2~si|x=|s- zQW>Nq050|$C3py>KiC_F&7Ci(2M1UVU?yixvT8$7&L(Ja>42cp`G6q`csqKZ4R0p! z4dLkwdjp9`<`TbzP(v9)i&6J30Bm5=H5(LV3t?dpS73v*qjJU7&heswS%iKu0g_pa z8b~8Rw9>1XQ&8IMVGwW+faqjTf!;@UU`(4FXp%hiXS!tAT?LKdYm#9&0(Tb3QEYCb z73)!G!4PcsI1D(|m`Zmw9rnqUg6;Nc=gpc9EAK^eXl(q$L`!j{BR=edN!YFi9-Kk%s-$B`9IelD zPs%-$d3O0ub$TJ0gT9sIw}D(hK_N9=ZY_<;27y-~^Tc1}XfvU?{PT!J-rd6-Ua0sh zhGf*DTf7pQi1)jJ&|g|pPMOO~O9r={^g}+NfVgIjj6jPa{9>(J-$Q6=OR7&a)2Uiv zE?f&*ipO>l#b=EfHpZ9Ccj?>jpLbakfNc= zp%J2`$(P14X;3Z=IpXqk+BKkQ*<2?U>S~AIH7;IjFVwy5GOX};p2oO6T$uI z+K9t_V(X^Z$NSNEDySw)KXfe1-&8)5z?KmkATIj<0c;^JlP|{bnd?7;DSW!F=5}Km z5)x&Ru-+~xbgIDEVcY~@K6lmMT_Zi%eczb)crAf%x51hqZQZht&>t!;A2qaS9TY24 z^Q8%o3+!fW8*D(19CBsSfr26e?RCIF9jAPb+?G9c9JV_u_%IXHJ}f0y`MQ!YMc<<9 zZC<#t9mT{c(4Jg$QjN<>~aEJ~%JhL#BB9Z~kvklj72 zw4pSV!r=9NH%Jhlmtt3;a)absS%CYWcMJ35H5*VJliqrZ(psS5&NOvy4SK5Z7BDTw z;|1xH;b<4x3yJdw;b<{AAN4-VdB*70uD2Ikz!h}7X6rZVDDr#9Q$@+-GX2~lq7UjQ z!aAs>Wg8N-b(k5!{K?nd+*}p1bY0#-b_=o~1p*Ba(bV_(oh>9SyslhXHz+nE&^Aiy zS=&=qd%$Vyb={nr8!(wmrztS`RKfnio1tP<;Jtn#R5LaAS`0~_yH=}^fyhH#czuj` zySt$A`BU&JRI^0guXYnAv$pm{DE&gPx(CC2d(&sONovaLUl@c^WnfMHEOh0AdZ?WT zh*vT!;UA%rEaRn(yc|%u0cmAV*P+@DQI;Li5Ycka;)6fdRrJ#Laj1(sVjR~mw}GH2 zkpiX`;8Uz4%)$BTH|xrH#6XTuIXbg6y~){MTyan5v0aly7SD`#6yGCO`nA+oRL01+Qj>F&2V1ui#vCvgKZIb&=jQ#$yl@Y=Wu1&$bC`+=}ZZJZVlvSqRv}8 zc0wXj68?Q$&z0#_a%Z6^3F?4(^g}$n)iS#xBLv%-rFfd%D7}NDO3XJfe*+~Cu zj;2nB&dwx=_$=~0;-Ul-eG5edw5m;Tl672|yf-)yXxL+m8WbkxhgjBgWd!#I9;Ag5 z^GZCKt@~*?&)(3$qxi3eu63Aqj`=%3SMTMF(R$7p!@xBvQ)`hg6!$;_H!B0KvGgs7OK?^dYHg01l{`hMwV3m^M4VjW`rH$D!ceq22}ESC?W-g0u(QRDc=YdgcL&| z3s~1PB0+1ncwi3Ud;#|72AujZ!2YcCYW$>2EX>e9)jrs_K79hh*CbK<|GSH?V%bgR z?R5b1Eg%bWD6OKZ|LX)WVZgQ-?^m!Fknn;0q=64QRR7rRERQ ztA)HvzIKqzUfKm>7)i{?%M4p~2R!{9(5A0X9{+Na4P znU>j083jzGZt0_{KOa%c{oU!l=>U(lK1k#q6Sx)E=W-4ZW^K{6xbysoc^ z2RDLBh0D^ix?zJ{yVy6wRV*9Ni^vJVE==a>(TjQ(&{0gqV@=XKq=)@G0!eAl+G^}- z4Wzr)0!#YDZQ(8Vcped%GSoqQ+{tm+v;&3|HrccTS~b1QTx7IJ$(FsIe1TYfgGr|ftTcoF6b`e;JpE|z0Hk*+>lp^qYy310T6yt zvlt5GMg>D1N7PC{{@#4{!wWOwCh{P+^ID2gBt*GCD5lUL%CnhmN_vtZTsMI|yZoyg zgAG!37L_s;_@*F&X<8JJxC2CNd|~-jRZjY-#ruJ=pca@YxKIdjbn>fCTlEws_w1nF*rE%Ln8kjQk9RYe~;M%#Vz){c5%1eUyg7@B`Fr1b!Sf zz=(k-;taG%Wc=nD4RE`;nJdH45IwZAT6*#Mu@V_j+P|yf>OMI)*2%W+4H~#x|1LfO zs4aZ~f%j2k=X*Li;bB&;`eBn94nVVYWgRr4+io9?yM{SIv_ACn2!=WP_Da-SgWQ*T zOs}NetV?NFY3L-=S@n0KCW){^RA}Z(DrnODU>x)Y)RxCx3_z{;n+Mxh!5^E)zX=}( zYspXvzC5rTEi8V^uf(i3;`^_Yf*Q=b^uf+Ul!)9iJC_Wdsm;t_w5J;VJ@}Y@16ICR zy4A_)FyJ6(8YT!9uVHR&2||tw_?HS#gdC^p`+(7A>Kb)W2#gndBJ4OQM0{_70YV(x zT|K?Ag)W-EK^QZO&#O=&8zpy#)anKXPaxDNgO^IzDU3e}#Ka7XbpMxkDVQD^}92@EDzu>))shX7NE-q!)gpE-zc6rpNQ z!yv+TBP{c%yQhU7fl#>5kG3k7X#*SWLJuU!G-sLq1%-P}X7r-6hyCkdUYDg%Z%)7r z*?EE)t4v^zGJ`Lu?GzzvnO#hu%i?`y;|7yor0sXIN~`RHz>o=_E%qGGbQwrj8#teK z_7%-HnBh6640Q@JHh#?3y?LW^{;f4mhDkWeTfMRMbk?YmFn=kz@bF|3r^W`}`(aoFm#@p7tH#?bmgKZ~* z+W~TGO4)^Co-OFp#B0HCXO-|e6JK~%%&%^Ryh~s%^J7y&-K2MYZTar!|Huc%ZKvPH zwW+jM4N8%+K^M1g?FGu_J_xrpkQQ-sH685Zq`ayVcvfVHFTjbX?{pK1Ofk3%vZn0+ z5gfaYAXA*)T>|ga&z)n6UaYL3xgSq(KDk|eZ zO`#wUUb#cN@EqGc&Q{>EA_*2lQfe25?UDsOd*d$-i{DgSKn5E5k$9%ZB6uo9O}XO% zvaxzQqHA0$L)d$o8O~J+e<7~#U2gr2EBxwYzl=NMgOb`y{Qu*bdaj{xj`d_X|0~QB zuL3jq6>l$oy|c*ap9DTQLdNrZOd+(DFA!!Kf*i#Ugp#UoH<7>SRHc9tRha zl&(Ma#v_)Yfz@X~sFQ(4)U`sd=}+EW_*D$-<1(A|LpQKe;veZXx6wo``CD|`7gLzl zmJRXbPA)LF=ppEl61FGo%zX)%YCh?yDfA6qwnktI@MYPMq5L$(4sWsepWl^fCth#@+1P=58@I+KY%wW;^L1p;3~&fp`_r&@IuM+P)(3f#3i)`4MeNWvTV@y zlO@mbzVn7^VnBixb%>S$CJHwht)V60P1wv8jAgEdC z)hJzxIjf=s0V$y)L8KFkD8xl@;z|{eqx8OXLXjT2kuF_&H-MlbRgWMG`hD~MBo`N% zA(LmGxvOJfbW7EFot6fA%Kd@^H=%4bX71nZjoT=Fm#r}1#>2b*$S)sQNWnT2rJEtd zOyb$R)YLkj#lMVA#DVO4q?{L3xSnG7AizY^st|buP!8mNP7@ef5{hKB2&+BHjr#FK z)vpGrR%#fR7DDM$D<{7jWx_AW3WK*O*vA8&YJn@*vz6lR>$al`kKT1AiTUhtB3&n` zH$}g{x}Vt}HNWp{_H}Jl?3rnaS?tfdSlu6bV-r(4tn9#`fk2g*T$iDW$8Iqi^cVWk zOvfhrxr=}MaYWok+ihE3ea&=dVdgd*`x&``-!mQ@Rii`@ziXvg=K=h0-0KsE(4+qq ze()E2-2WhIE0^#4DMak-+sni1oH{}nmY`;11x7#7ZOg5A348YS6Qq{9%tkvh9=Oh! zxSDexrGKAxHyQbSE2;JEr}#G1RkK5ov)X;1d+CQ{?-}@x!ZapZfEMsRZV$MhqqPxBjPpn-~S_5U3Bn;RW=5pmPNJmn5 ze=A{R#5d#EhGprxaNOY5AoL(4jLn}oEKXE9@102x{W^oDg6@|!vnkhura4hw&(UDr zd?xXg@gZnmsk08Gg|h!(e}kIJD|XQdV$;GP;p}V|M=NI*P|QTh6sq*Y1fA%AsdrfD+2rLu?;JaVJqS<{-z%BvTQaZvu>fF<_K0?D9Sq{aO4Kw zd1V=!Ja`c$@kFVmObdMeI#96llqz*H^X$nh6dt?S`%VwQ$oun-Y_~&3?f#(WF-QiT7LgPZ08M%F`VdtYsy)GiNk!$xB3pRGi*h4uFc>zj zT(Co_#?Y>;pjXWr%zJZJxe~(lyn+IN%}j+*Nq`fSz_c zqf<8OwDr~5Kqr{rL5-1$2}w;o4g`A^tb%hDCYrb34RWtL`WPKnReEr*cP4&DZKyn( zFu~EO>wdRiC6?@=oUBcUGW$d}c9Jge0Mk=yuc%03&Df{P#F^6$zxpet#T$@artrb) zjq1@8&r6-;_laky8^Ll8X5I|M*URX4x)gi5O2yZu2p=RiZaTqU-m!kurkQi)GA1~MogPwr3PA~+&{d5 z3zMZ?(0cM5@`@WaMXh0i`-cWHElG1=pd4@LpYyko5qVNC4AA`_YJ_O2cWs2mQCU5M zcIcSHeBXjX|73B0Duo_>=KfTXtNWvlBO8A7RSbIc)z3p@xDNLTFFD2Z1#Te9w6o2~ zt^8{v?vrx;4EFK=kh-Zw_KnJ753#@caG&IRfqBzqk^r@HVRF&`*ZkrC-w zs?p_c;z$l@ypE^sL7w*&4@lqDS*wp@P^M(>Ef_gCm9>~k*DtklyzhbgVd#FN7JoAJ zfm!yb|bOPU%*nZ^86>rw01*eEMZGW+Q4n@z%gh=HS*6)=5<{ym_#Pt?f9Z zy<%-*>cKHsT6sYT@ePv0Yi}CMck$?Z#nddF+=W7o1)WpH999E^9{IwmKkO%v65ewf z|1{;7D5JO|gu z?DGdG{EPOCmt`Tyf8(aF9$R-o-@{DPbROx|)(EbWo{LX4_z=an;nK8vKoo2ESR^qJ zfr^$rD3`;Mhh0lj*qD_^nz21wxh8VS)ldBS7A7q!LUX0iT~>h6d=&0=E2_{RmZWMh z3NS+&2{<#vEDrwdGy%tZ9RmnFrkxjli>18-1^BYKZ=Itd0vo+3?n`T>vr4#2;NmS} zi>CpaTp`H`Di=BcMsf4Ntx{;4VmN}SLwVzQi`H)A2;db#(5F)a+}gDTj*P$>D{Z*! zr~wqdkD7y=fz2a=(v%JQ%WNIG{yHG5oema!hyVo3y|AwekWPSUUITl0j4HJvx3C2( z^qluU3_xkB3*1Cy&F-Lh8(IRO5I7BV_rM}o4$LvT01^OUQGf;?fCuEmd$nmW*FnZ` z4$QZq)vFykbDQTw8JAVa62*CA;Gv&F>m9&(V<#n%F7%_=U);LH&}#b79_8RwZ;=5L zwtJGi{`+zPtI`x^TPXy3_nf(e@~X^Yc}xEt5Wg$cWum=xADvHI4ZlbcC(m9KXT9<= z6I^WN?mmUoRg~tW=&{6mg8)OX+L+$UI}|sEb_rJN?a$u}Twvj|@`Ge5N+EEYB48}@ z`I|j!5J@p4@G*5l0>G|iMvqYyx-jzIJ;>q#jea_MU@Ji&hViPAr>Vd>aUGN9Ii_(; zEFHh>19m@+CB9sFxil(4qYHTM#Own}d@XfQ6SIXN*@CX=AwVr_%C$((RMC}(fZ}%M z9mP`@(jLgNzh}Wt8f$)yypQ5KE$pOzvn8jRisnroBXHP-dCluO!1FtYJ%}s$ujon< zQWyMqD&LS9=jg>M%V?YS`%Wc#1_sv)=+B~zvZpAx)vFNQ?9%LW%Gnu|9 zs(uHlbx(R9b=uMVVdWGGa()+g7RjS$Zw4SwBZTDRY?flNQhhTmH(j zTQyu~2E$B7fG&c>Wf|8W7FiB2z@MA*Nwjv~1$eT};#_*_Y8_W|-tVFuUkddhS*WGD z2=mL#8D^Y1T=^FRML&-@dh+(t$+co>zN z#IREJ*;wG0v4QGKgmo?s_k8vd^UKWm%wwS8hmmC11onwVeK+w9!=@JcUFx>@zLopm zq|&bCCGeq6AZ_`!!bZ^g=W5Coi*cIf0USyibk~A@=li@#FOz3LD*3)2V2%l@?LMfo zLAvOPyg@$o52Vm`L!&rO5(!i`=(SB+y)C<;aV4pN*(dhPV4BOhwFvQqQ)8_};n4qF zE)TSayB7@>>$-R<|IyNYOjP;Dr`wsj3H}?N8p=Tbdt|l$eG}3Tn|+vcYT9>~aCqdk zo^b$%wG$qWG;$t(e3_!UJcsq$;1W-@@r6SFD(kR*j#v3BTNRhCxUBX`Yl&Je*3H<)9Zt8+w7JOr*@!7@_64IG6(wJh96}SZVP8aP(M=|jy zvme^bzhGm-+9-RZ*Z1i9Z5^hvk;qh@xA%+5_=cF_-fHp!|JDlzcn=X&e}p)mId!tk z^0f=1(#t%|^IE(75&dsZ)mBP2(dDb$a$^UvcgkAKNSWpO-B7!}!^RhNAuT{vktdX8 zTKXYg4Ad`etTf*TGn&d89hx$2#9Gptm=C%(QCG+hN<25WSh!C;7cYn|pdWYmikBTB zuC`%n`0NYhHsdZ|d~1f|bs*!Rjc)2m4b5wPV&Cl%MWM7`zE~ z8`^$EQ)LY>eN{(mJA!EOA=D3UC)Z_;iB+X!uwd zPbRd%!b(`ryGs<4Q9PGsggTh$hG96iv(KagF97a2kVY?<6ef~e0JUs2KRFiW+yZON zRP%&n3+;3a>0uZF>+5}*$*kjXG+aVtAR?MkVn?^JLi@l7O^9*J!h=VXuDVcyG5RY( zz;0Cwu%QbWHwS?V9J90Gge78dY5xC-=9>mv11+jfSbO=r-Re7GN~TAa%t!a4vB`z{ z)+w#LWs|~Z*YJLWNt=~|g#crJAsA_;fdVeSztV|UYssEqP4}<56tgilnzgtk41;Qw zYZ#-pQupK(Zt?H%aNmst3n`t+J7w$F2 zz=i$~^ZL+*-}PCNnNPMtU?V@=2blM3DALhG>XzSv$ga0sJop@AUpJzB6(azXKF1t@ z`?1xFuu=Zn%}r?V8rek-eNb4-1~rbVpr#Hl$Ro4M`GCl$_l;#VJKKuuJ{Bg*oZew^ z2$Vg9xz^Nuf0bY4wy`1{tfF8!Q5MHQ{;Nku;&9v5%+S@}J<-C#A2J1&Q z0;HAetuT&%{;CMFgf>JVR8%6e(1Pyk}jgQFy$f=ehM?x zFg4pfjh9?kiMhOH3!k&FKB<1eU(D`*RJl2pUdVim&rOaAU3X;MA)>u5@NC=Uz5xOK zSCgzjB3<{BS`UCI8w2a{+~od19buxA27hF8uIq;fB^7_P53zIy{;{$Og|&`#i2?Nw zdPKp@tdL7xya6&_jo*ub&$gfC+@nG*l};g$!?#RTuq&_L56imMmtzuZQNlsx#8-+mfoLlNaG<7O(jK?U>&jXWA0|ng^g#HZA9#uXo7FvL_Vt~!9GLrrrH-6*Av@Pmki^Uud zOt;Oz7Jvj6#%V2KEJ!zkh>F+HISqJ&>9G3{5hlmUVG{@GcLj=lx7inXe2*w-uXsF9 z{c}k3()Sr~)c~@xrJD~z#_($jPY56dPckMZ8Y%YPHzFpm<8SYND-n_}RE!^H<4j#p zW|{A&GKhl}Rkb%yP|F2=f>CCX_q(vPtB~3rIR?l(V!f#15MG2OTo*uv10WYEXKl+3 zWqQ9I13FDkd)Xb(dk#&JWC|~H2{M_Wd*HCV*>ee{vU$#5ByRm67;JecDWZ}~^->Zn zd`5qNry7A{)O#}3rug(n)hg{}YLQ4Oot$4+JXqY`0rFRAO2AmeKWxyip^+eIJjf`w z?``GWeYDF!bOma|CJC4r>i%mF{^+8KVT%KRYPQzoUI#5LYrXaz<>RWiEFxfEK1gaZ z&&m~_TUVu!23N^e&{zB3Ht>wA$+=(_PxC;y7EsJ{bqkWo$Pa8qw>s4lE^T*HNAL=U z%0Kt%GSd+>IIuRnhjPv}5c;$HvchegHgyOT+}ejLMLWCM(57PiO`d_QXdwPqEGjD8 z`1NTxN&enmU;?M7&k+M-vrmvcQ+lf(RL~m+T1|93MPaIfTN_(e^z{PTSXX}@CTqQ= z%eq8tU=~+B_?ib8J^E~cZ-{RaTPgC z0=|Uao-A1P1#S>kMpfB?`5OK75MEzYVOjKQiO71P`ABUQx}*$Yl6||Q_!f0uzFj4X z$x%Uzm_g|%%ma<_xL2**)Da)$ojFir+kB73HJ3_b;JG$j>IVincOE+?MyB%5=WRN? z31cg^lU+Z)Id$Gw5B755Sgb?mO13PaVgj>APkc5zLH=VOvDL~dhXLd#{k8QW_c+9# zHH5HqGNmKi;FWN(tr^fMSdam)MEqm2E8+k7pCMxFgY^B6sCegcxBt#ytQ(4Za!oZ( z_gn&dmetl|FbziyQ}rFdtH8>k3^-tus-6r8O_XKL6Lkvze0>sOAmX!k&$|9d{)k(5 zY>0medMyhg-kVp^J>Hq1ZqJ7rVzvuOb%1ao!@>C{{z6)ym&cMl(C+e>x6RoV_tX!>@gVoy-e$gSPA)S^fT&_SyrbU9R z(0k?gMvHjc#kpj>iPZ8|K613Z1ERi=e2gX^;*kmN!h|MZGAz6ZeW(@@Au>gLmN!?b z=*!doAkgy0;u6}oS{)Qx`1}eei@Xc0xXH?(uK0kyX_osGv~S{Z6A@=)EM7Lp3b^`g zQFWN8MT)-*8&a7UwU~9>$`*ceut5gtqOJQJDyGHp9_Q~djN!Q866MTr6Eho-mvJAMk?ii@>EGeY_5V zg>!7AlQsPyjWW9sVrl6Se0OR0RMmq=R~k_P+!VeWC%V9EQ;DF%9?T_Uu?foIZq-0L zogOC^ez6?R1VXpUrSF3EfI|8$91@|m>W}_rvYwn3u(Bv2U z%zP^`t}(8iB~=oc%GWbI%8xv(&2Si54hB-1ML`K<~ zW&Rxa+ml>5s0z%P1cm?uxAR3iX<|*} zuVA_X-o}vhmhzFOD7*jO0o}QtFWjBNXI@&ofcC~tiLIcU=>lkf>xgAMdL)y$01hi;=n(x( zBi;8vzKh1Qz;25Mx}N}YoesZf2NT?U`+!DAPvk-h`iPFT z@AW1AVM18Rl@aXL_zjR=`wK~1XOyqN$|G%{{|@kFwjpo^r0jo$q?vCT(WNOd1#4{7 z`pB843f8UP6F^aL;T;yQT^0en2y+weumpZ0cy+EV;_0ejkrZ^N@YY?`6?z%0q0>pq zO7I({?h2&?*tEXhaI=f+^8y>zSY~MaVhIcxu+RJb1gxdfyFtCOAWn?s6mr;!q$nt= zy8~1nc;~VQBC>kHd-JhGV=@6z4qSiF6$ILwT_j317sJA|cH6iNmXhY1I-p-BJ^}Jp zy)9MrHQ|IT7jj@t>kzfm6W;wIaS~?QwN^dS%S>Xl%4Un%HkJU z0v*U8P90})cNJy50|F!b0NypVMVLd|SQWHgOx%KQup$3DZ1Mf~J0O2?XUGC3KeWk* zqLNhys>p<}G))1iCUOZTc@wC3>;Ya2a{dW-gzZYpRXrjo21}cEVNcxVmkst1iR3x} z&o1T)55jV9%UR}n8Lh;ZFBe#pWP8@#BVwcD4BAurB-3A5-<8A$%afJikg`V3s)wB? zM#aBFJH2%{#a67%vOub^MEwUqd~*>v`$<6#YS9HUfQinClD$5jww4l1>Ts@UT7NuS zT==BNi8(7^*l!1z5g@3$5MhSyGz(JX;Sm3O!BZTD3^Hz z$4Twe$(6$D`#@QdxH+`CUBb_AW;?PA6=tL(b>n|hhlh6+EEbd55+rx39L6j{LDHY= z@{s491-PQQRXD^0(-AXmK9Vqyi(kV zJ^)sBAfr8~xbftImq)=`$wTit#qP5Fli|0z-qiae5tJHToe|Xm_{YEQt!>&8bf>iu z0n3sF{(8I?=ytyh8sF{XRV>+G*{8oZ^6aIE8+Yj{v;ZFj)kRxtP!8n4CF}(ARK_Fc zW8!38z27DZiC2Xu+MQ-Ws6Y^7zv4~cZ?MfKE<0BCn`cP*I=1xPjI zDjxb<#;}BC)(gjHQu(B^hnl9JP&lZkV}4aZ7MJVe*^>9?qhnQHuy|IoB&C?}gDBb& zvZdZ4fPB}pG~nbFSCdEEFwU(Ef4)A?zw#$&-@7!!@0*z9(wuufZtJZtB(2qT`6gds z$La&RwLD<((@#rYwU@@h^sN8BnD^kYPac()Fp5A<6ueYxTNow0`9;gu1B&w0wztB| zYUPt{LB()qOLVifj|K|U0!T|6v7p$=O9?W(+97s8QR&*9UB!nM3s9XsKr+5&qCoVo z*YSr2r-f>ng!4fiW{yp{3XwK@kXGF6@DPjN%rUwEomjF6)D&~AiGrsKl-WWGQ;Ktt z#|An85f=>H&k|-SMnzuNW1p8>#B+YametnbxGI*@QI=_`cNSqn;s&_<=IrPRA^%O_ zOWYX=P`dZYF0d{XA^;89#oR-8`hLXFb_TATk}4=tMBL9olQbYjZ5wjfHOJ&T+)lOx zRnj&gYccc0+nkdcJ_s&eUOnK?am%U?VrTchgs!ITgaGnsz8awPfvXt(e$;Q^X5J8- z5isH*r1iE?M*&T{#^M_GbQcFIl>A9}RHT+k7Z`Lm52!e<)^-+x^u>HfPk3rEInfFg zNwCuU&awdww0Wk|eekKLTahSwyz&uTU;1tfOx9@!ZbUv|wqhquX*{ERW^SK^!5(Ct zTDP=APt7m3V7FWYoDyzfuK~)7-dMocmZzRfs;C>mOHDnG z^bk2cscCsC-)5@j=_{vtfb!|h3g)d=UaBK(xVQj1>yJwR0Y|DvP|o-dax2!a!!PIa zGvT@yG~WNsVO}KPdEzf{`d}Z(7D-29^~8 zXYm9h`H?Ef*X>pNMamz>3TWTTiyeS3O6&47QlGg8Hl>!JW3#V7Uz*4ha-3YiE}O~X zns)kl8s;igY(`H6E3J*YFJbNS-|8m+51I^i)wi+P20nlP(9nWBEwwiRLE9m#f?%`! zz)w(!PzJoTBzdl2`rAe*r}GRyLKfLd=MmP~VWFU859;^NC^E&X-kNk&z^YW2WS;xB z&LQhJkC)}-*N`UzBT#kq{v8aX||8wu_ zw~i-`b>YLz{@$&Kd*@VBjVTR<+1iPZD;^oIwK#JmfA(`Dx{*$v7?pVl^_{Mt9J9K1 zM#(Rx@{D2~@7%cMV7^tkd8hsuuk5u##No9?E!J1nq8EG-E3sb&>$?`;D>wD-${Cq> z6^`pRBv^lI3TY*sT96t`ALz;GJF)gRV~B!TiL_Rn!9)s1-bAeK2Kz{E14T%Ymh){q zTnk}+Zf<6>8oPp9rFT#&bq`T09&;U%=fmG=7twSn@YLX>OVFs4c3w~QrTvLKt)Aev zE?h)nkWb|Vx=>Qw)1SYBVMGgNo82ex;H}-rrbraTjTb}D5(TdY@{!;N_I;POA79+R z&Yg9UF`tHhp46XnX~|6UdT856E>eSHs_-0BM@AeB;yRz|fIc)Y5^G)h0vVYf;W?|! zOEBA1pT0b$Bk$&$+%9HuVSvGJJBBVmmZ_cldg=CqXa-&`@IX+jaM>{@iYxSk2R`O> zZx^bQ2dGUCeSO+jF3rLf`Bq>1RYDB;_VAeN)FOWoQIHBPYBz0z*AWp(P|z>-#e(#`JJO&3zp+MgB;Pw)bom%RX5G`?Z_S}hLDEk)#<*E__a25% zxpM*I>Bx;;5+jP4F~jh7g*tcafdB)+c|e3K;F6ie$X)z@y=WmNW-@W@-_XU{ZETH^ zC=P@DF``w5ULsVekl{W1-~ZGTjs4B~Pu~+|NH{ zWUv2+R%PjZA&PGsS`{6M!hLY~m^){Wys7Km1`LNN25m#*v@QW8HbG$ z-?cUf5(1tN{rbpXUm<)7Y^i<(ue@&M@YOyau4rg_3F=9ddv!eSSRf*FJwgdheJf~v zG4{F{I_&=e{wISycm!-%!c%KKr_lM9fo-EuX7vMTckzCVu1h0lk?c8tuBMmIZ3_ju zFQK~2H9&ET8>h^II=7%}ldW<@V}{Jff1d6KC7iDxAt;1=)k-ud`Hw@_8$hxXz2t*# zdKoeE4>%>hGY7S>N{{5wG1N^~9>c_sB4#yqR)2+Cxo8D2mh8qW`r%6WEB5!@Fz?1> zV3Lvuaz_?u@pTpIq^Tz_s6tY2{)WayvTr_B-vmnLSlE!NIh5`hdGo&mxI7O4nhCdb zBV0N6kFT8aAscPeb^GZ+=r5Mxvls>6bFG|Jcul+21dIY;bUe_Ocup|reS(aoxlh1W%e<4!a{C$JMO=u{h^u|3qc872kID=eiE*!4j9(u|(1={XZI5gwy2k zmYHaIqS%#RU(5A1^Uv4uYAD8XPMTKlWl$wvtnA&I;-wS8I(EZ%RKjM!h(&KLg%z_u z${KpOA&h{1Ky-!Ke@uB8TSt8-CMqf)+H>(;-}JtOa|n2Z=W^IQNVH=YZv8xEwZdRh z`*INnd&S_>Zg1-svx#zzo+R!50ZOpvIJOC(B)mM>?*Z>KxYM~&~` zwbFs0Y41lcd5gs@aNiJdrIGwW+fG2)C1@;~o64z}&ARf>#$vZl@U<%aV8XhyLc zOVQWg{)pI6H&>zBcOv-%ss95Wq0O_-b}wMJ+I^D?-5{sC@0#yo?q~r`9g%iNqNk1b z$0V%0@pjDa_z~pm?$xc-G9xTed_#;$qQ@(3cJEU#%?<`46g0^*`vG*VHFz7ov@w>L z{44iH_c?{oC>)>P+fAtJ#^1JZQg4eqXm{kY#EQF;rh9Sr4PKU)pJ7P(jWV8e*-OXo%CFSpbu}HgJ zW=FS$OoeSIXydB`>h1pr1u355HT$!Gg`2-KDylt#*@f!HBNO^%J zga&Qzu05sfUMp$~YE>6H%YfQ@yWsUWjcE_g#I_7pa(`Z@Jg5+|!O|EnTY~I|ZMZrO z$p>t#Mc_V*Tlvo+AW#9>l$)h$x-hb*L64U$bB}z}%k?IYU90 zKJ0G+NQM4D`h$n{Rz#huelM8B*xw{xPd9CV$!8981tXs$)YtQX+EaqNmPGp^#nl;f zMJRHk6;zKnSw9I}(L=HX$z|WH9eWQXJQ0K8IPoA?Ss%2&+ z*D71f2lOs?*#xEsNU)9C-ae=y8`;X0wOEGHqDwq>TEe6~7$Hd66}sijf*upjO;li^ z`!1Q7y~XyH@VA5|DdOc!gIXZz4;Ms0@9`xGc;N*s3iQ&1#&m7?eyU-Nhjkd}VGX*F z&K~iOay)RE0lY;lyf+*!-r|n4jsf@WHR+eU&*Yp^5#FZJ6;r4{Qzy+0)++@kPj8{FRG2d|t%aX0)E$upy9-yH4p8k2fsY^QI zT^_2CS~!AORgESEZdc~{sjx2oMJ^~GpZ`f8`t4!sd<>+H%^{oIWAy^rr_h@3G0T-N z_D{qorE^CP%Em>fn~Vx|_{YH6zxucpobQ%$*KvCrHk7Q`4~ueJKkdfj7IDkkcT+{1 zI-f06vP4>F4Ha-8qs4m$F5PO@xW|&oJ3D%5ZxNBFD9`$Vf16#ah81Gy;)iAA6j-Pu zy}yGv;MWW&w3VQqL}6Z{4uU@@hM5Aot)1z;-b3Jb?MU3OHu!P7bHVy#glahPo70Q- zAbNsi7XT>iEnvqjo>Qo1MxDIhFlOqZuXj)nP1-kMhTzzxO1#&6)zCV2{qCkfcM#~h ztmt(_(5R$Nt{f3dI|Dx8$yy3NMfR+Tl9Y=rT%e_HE#nw?)1Ry!1sWvk%GGWIW4_N% z)}mCL|15OSoy+Cxk9-;f+Xs%Yc-{a_E!%yl@S6qe=;r95K#If5+uvJg+?8Gxh#%PL-U|9z^0BZFDgf8{=t?^f*Koi* za|oL(oV~~eR7Oi}?Af#8{tOpr<(yC317odPG`1ll+hL-ugGG8DXLi_Du6TxjTsexV zwFRu4^P>r=H&bxRA?6<3UAs1pIUYFAM*m-xKy>@kAZ+w3&tkw|sHjX=$3|>F$OlyS z&!=#e%s#hb`4=v3LN6EWHB`tnesm zQ~Y8qWKZaV_H}`4{cl^Dut`>ZEE7C`x1LBi?!#RFz<@DYl@^6{d$k`wL{Mn~S(nY@-67_P4R^Hj~*L{6ifJP<3o!3^I|7|8KLF zBtS(i%qU)crGEgH&%8d04m0WR^) z3ZT`}RHCdEijK~L4UN9Bzk}T(>Uy!<*`c}om1Ici>>xrK(Ji7E>oELBaLmOK3Ws5T z*eLWw^oUjOX)W;$rQEgw3yIHRjLSU%O{M#O9>P|S@KUcMJ-BWN)4&kB$`BxXEqPZc ziu4k*lG7XjRHZU)%bLlEz50k z7H@0QdX{*eP&q32?maS6-|hgbzFbl0%?F(nE`#pOQS|Y=qz9KYZY>H&Rf6voc~{{A zeFX-VSf>4Dw*~BO=@M(!hBWV?bCzKt&6xZ_o^{MYuRqr{-GFVValkbMmRYh-6*kmfCZQkI6hGwY?2$e?)yiWcE6uXM9qu#lR1y(Namu z<$COghZWY2)cR*ND4jNTU~|F}j*Y#WdKHqr@A+Q+&|{A%mtLStY^djsHtGJq5GuN+ zi5k?goR7eSrpev{sUp$NHy^RMUAtY*u495VCT?}u-i0#BUWrVFj~MbJ|3M@fdiQ+@ zacJmM%sv?9S&;^NeThsydG3^8(61d2{x8PmcRaYNwmXruj@36a*b0kyKqYxRWZVWe zbByK8pT`lD?JBG&u5do7wi-N=?LGPYTMsc{df3hFRo%&)isxwq&+ATR426ZXtX7?r zp6wAlA`qauIrIok7*<}{>EkVZy@dU$;__Vizl~gt!K*0?*oi%hCorb>ik}a?tp>aO zVr}-exa|A^*gCNp$V9Mv``*|$XV|_AiWd*)^{R%k`J4;r!k}NXVDVwf$$57+I`HIK zrTt(NwyNQi-vI}HP`g-zdrX+^=5LDXag!gk5wP2O7ttPU?U~DXBg48l`iMpxU;glx zyCxV+>J*GZ$i=NOa8SD^^9qgQZKv%@vG5XGvU^z7269Xs5ejj^pVQq*cLv<5f6KjQ zqYl5cOM39Ps_(A?@?CkdGho0#53xV)eex1ssQd_lR$bORp1>UEeuT2^dL9k;K}w44 zK#%>G7rwg+X@RN^8rwkLtN1TRLGaK41>M4vmwP2SLl1Vn)fj8-Ar#5%cZ70k z`XJIinzooo7go_9s-@WyRd`YtTd-vf-N+Ut+Q0zST77)ibNFy9~z^h-MmA*O)Ex{gbI zOV}sHb==>Fp6E^mAs%rRh1CS1W#gG6VvhRVU;|M)V<2q@r8wKlYgHY|#s-F*e+6?5 z5ppqSCJ$hXR56QWyfs3*(uABHRN)g^*+XOKK%0&*Uly|7uhns`%SSd49lP~@%)=te zY~{>+E_hbU5A<9Aki&+)yl4R2zElE!(#&?^kas66TsOObQecTaNeME?9D3Khy5i3v zbcC1oj-@}zSV5_51i}4PXxgXF%oWVx>J}_?ZF>Rq+d-)pEURy#On;|70|zS)A^GqK zJh_|uU*dDQ4q`i>&>Y(0W~Xu$CQzJ}q2Y!pm)`+jWg-shmGmPb!__%Q!~=6NCk$`o zR?U~n9Wd|)2d%v!h^@5~pT|@S>?bH>wogLGtxn5GSnv}1x8pcbF%3yMlJ?y1<9+Ns zhaTz;V@cN@*S_)KGyR6N$&{`vfnly^M`gc1p81u_^|P$|@SmT+NF3dq~ED7T)fQl1RWhdOVlMYmbO+aH-gAE6C!A?XGW;Cls z*E^eJ74Rq;?WmH5F11*uIsMd#r@q_#sh7ka782F#V#~`*p{J+Qd=(mrH}VY zjFnx_)M2T}?UaLOg?d#GX;z*p%1KQtha$8BSGpRg!>VNxz66x!%KkkLbqR8Ua&_0y zCr;?`WM`xb>*Q14tXxImS!ZYt{k&HjI)1l@=L literal 17129 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 diff --git a/customize.dist/bkwhat.jpg b/customize.dist/bkwhat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3549f7acb5fa66b36b632743e5e711ce58132e2c GIT binary patch literal 103964 zcmbTd2T;>Z*C?KZ5JFF*3i2Zq0cnYJQ4>fI1T=wwfS`0CbO?eClYp2|LlFT{LntOe zQBg!<11Sb<1c^QtEEGi^#RiIqdU@XOeZTvk|9o@j&fS?zHoNESIcImz?w*oAfBkt4 z(k166#)CjUK2{(&=zo_#??FgXc1+S<5EukqX?6#J{=8N=l)zx5yV=>LX4yu?rbWlu z#-yd#7H>3iQfA% z;{x|>rN!(^igAs_ZrTXA=ep&lq^HC&qJZ3#Tq^ki7m)YvtDl?7xM|$;q+JS!bJ;x!Vrs>gsA|kGI3)ZB`&`vi7DjqH=9g zvrPUo0x2#lCNnXek(ibW{4-)ybXqoJ19pYe{}Lf3-QWMe2mT*tD<$P0x&8&6#R!P| z-)#Jk&{?#->2Y=eaan2EnK5xI;3oe;UYWc9GogO~SGaN8mYKM+C{f9zw3zIaxKxG@ zX#;kp$2K-G)-4VnP?3&feA4!6rI7KFY?$#Ua+lCDt)!rREqN?-ISvF&4KX|9|1f z{=d$j-3n87|7@23&*u40)5?zgr~2Qju=3--l_@TDMT0U|ROQbcNaJ4$0E+EdQAzNh zXP^QQ3<`xRLSc%EFlE@vM->i(!By2%R8&<|)DUX_DhM?VO@yX~8XAqkpwR}aR;@BH z`tJf(Qc_Y^Q`bcxboI56TKfOp@qb(S(*r`n!ShN?1uzl>L4p;K;6MGKV<7Oq%YV)k zFbJXmRfH-1|Jzpt`Bw)51XfT`fczs37zzP{mBAn+#2BYQq@toSSXeZ)qVWR$Q?JZJ z@%85bj7fCnIv88avog>@&~a;Y;)^MAWnhrPKUDttTB(D;NMi^JM^vCjp)*)eOhu!| zpQj+Te?X85NYEzG?*q|yKHkJ{MyMWf12pf!#L>z;n^xgzqtmkr75AOL!e92vEF3c} z&l4%U{=&<1EXPBA{78jsr#M&OB>1!pBNraPC?w{I8?6U071MK#+_oFDjZFusgp41D zNF?$}se{sO2JI;nq1DBPUIia4f~U!$jtc$!kJi7R|4a@s{_tAue059JxO>T$4T8Z6 z_+=`0(p~ zTGDZ&E8m`I7%aAxMRh|w(P1I{Y1gSC%dMH~hVXHg((dX~AJgajy3#}$SQyEQ&#Sfo z9Gx5(hPb|`Y7h1B4`d~=8TS!;w-WYxqNky=L9e-~2d1T)Hp{CM#ua7AHVrE#Xz&?%gT=*;w?dVt_?*vc2y_5*@XY z1zQj);II!JXZV{7rAOVYNFGNCi~I^?wxI_FY6uw4C!|q(w zSn4;q&K+E${6(48MI%~n37E!CJ+C(PbI`tEp$CrapOx}B(#drS!>Z^<@3m~ELb@H^ zb!Z#`$^xq}2kljF*-qsiZx+ShJ-dI~-cwJfT@Q96hGQ(0&zoaoxVZh=&Vi1ir?-K- z!yR$Xiy5Cjd62gxu-$+J`J;Yu!SI?!Pn{%H{uzxW;BqV`B$msYMnoJoP)96dM=Oj4 zQ#IP~)&4&O3Wx4#@soCGQS3*u$6-A}Ic+qg605)FgT6os_@b3^W=2~xZ+v8Qb-^D{ z5QX*XCC0H)gKGLNaA_^~T8GKzPlnYI3FP?+=GMYvSR|7A#~ zA4o(}Y{}QOM0&^+i;dT55dmBP7QfGUEs8t((oz4YYvUnx-Zl@td^yNJ1S{DeRk*iN zty{w#w(HB3LIqM@cv?{N`PM?!-mEG;?F=RB?UL2e{_@qT;TDdnkVQrzsh2 zr|Jv&s|BnXwynViXQFGQgPSo|;4M>6b8edTm7x%k&z}rInmbEl;N%8!Jaj&ZEbOiX>Cko-xsgx1Lq36Oz zs5}_5;ndl3Ti&4KE0p_v+p6`Ndo*J;f*vXKFZ&l~z>Y zR&Orm%*=SD(hpFB^dQgpSB*tCEa+4ZYi~()gtuvEpoOF&NEW~*Vp~^X4{PBe?Sf_+ zC9wL9iL?^y(x2=UJihgM?2jMulTuQ#TF^;IDe{r00?DQ+Fn3wNoGr_mKh}C}RPu)JkCxtNd8##>88J&$5sIiPE z+Q;58bUqxMCwcaCSR!F|(oDsILk>i*WnPf+?II=LG2klTncpNhEmmeauB-^Gi{#%V zsKF4pj-3KXLGn|ezWgiUad9$p8LFIf_8al>v;E;O-n!dIb85oN`}N>ON~XgkFrm=( zic{B+;!_q>(W_~mP*a5gS^KZYS{0yGQohpM4q}45_Fpa%D8xbU8;V3U+94T&)uGt1 zr<)Y^J3+_<3HOFx3Npj`>ycSg_q~Pmk(qa*-ELf^zJoFk^3rzfS%tR zdP+9o1&34EBKKkB3NEL|>Bcnll&q^Lz$2&eoW0Sdgt|J7fDuh#~g#}cU zdDdES>s%JwbZ<&QCs#Ft!B!p6gz!zHcI0-vuGR;Cl}{KiveptrAzMyjvqc2CTWC(qQ~ zaZLSA-bT{+DX^j7CSnbr1xvI7zRfV5Ewm@@3q}6bS~)=)N89Kub0%=uLp|o6dvvt* zS1!xcbt3hdn_A&7q&kK61OjazH;SY1c6=pKWDA=7~y4zIsj&PUX>r6anu(H>Bmnw$hv`UG@-MxGM1(BwU?@SzR>F>BgR#R0?X5r3j zzohkrVwX!5yQtY0T^quc|A0zXdzdV9c3U+m_F1&u-Nn#3#pO60UF!7Yt8b!dt!k8m zhn5F_exnjLu53NCZdU=Y;VNI*P1v>txSWzhNR(-4!L>gXjBWiXWA0?$W(iW~)3u{jKuR!6K7#8ck|d4JSGYwO>fB z08Xk@A+H{)%_$}ILtYQ@+i=gtNAqs!1`EPiT}63%%VpBhy|pZ?U-Se}N!VBOOstFq zEDq7M%ZkD{TwX9SJE%G@L4>rrM zd#{G0BX@NmH&}7r9Ygl5D~i<{d}HbjAvYxFgEpRR5+exv!lXqTlC2M?+T&3ZW+q0s z$PdU&O4Q*OQiqpMXztl<`o_&=>t0I(DtSWE((ayE-k{ImTduY8s+w_3=*e#+?BNqX z#jC>-Pm}zz=bJa2$!V0>3(j}(B(GEn}@IGJ~GhRr%z3+p19qM2)G%fMOVB` zGvl5s@FhnU2pcXRdA zCG2J(C+_AseKnJ#O4|BUqFG6x{&AWdNPaL;5SPF3gp5-xJb`E`TNqa-tbSU6Xuj6MTQ;|gKe(Bu zvl>4#dL1ziWqAe@?O~U@wRC>bf?hDiYcMI4fUehL0OjP{yD!v01_Pbgq3#Jw zt{Ac)47)TvFl9DJzjEC$T& zC?HO_FmE-7^A*3j0YsC_El~FIh^eFd0R%{AzeI6{VnYI5{aXxIAMrM!JWXY3z7$w; zHGYNwo7Q7I)<fNL1d`~ypdXE&)JGDJ=%jy4*)K>l)=FZi!e-gg zGj=^B|3$cp%JPW$aI2|)jbCMniKX6VT@wR;rrDibfe$dzqm%O^$Ionv3VAG3S7Qlf zNQ`z7x58ojHt6=#<9&Z^mX%*R>38Iz((?zv#D>5O@NZlv_1FouwADV7)BT$6xs?P{ zrF|RDh(pgDGp))Dul7b}V58+~ukp%gE!~d-QL#X`LQ~lw{?H{PYG1W;13X4{>`D4J zmxE94aT*?0D|Mj`TtHrHR%EBS+{k@=Dq8HgP5~dO7WFgQm*-Y3(j)st%sr+gAo~ly z9;$jN5#ASD5w(9&cwF78e%>qgSbC~vHrXqueH^K%N5&SU^4fMbcL!-*LnJ8oqpU%w z&p{gtPsc$YJ%1;MU6^d(@{A&9P0kdaZ=V%R?`dl+{bJky(Eq3((s57YIkJk6O?=ut+P3Rs zK@sxJ$vYo!+?ICydH6Vr&>rGn4k{^p5y$lKl5aS#YxHSQj%0AsYL=#mbCp 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 Z71JN6@#5TEv$oZL1R2R;ZjE+9|Jf`*7%>0< literal 0 HcmV?d00001 diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 480538800..0691bd2f4 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -292,32 +292,54 @@ define([ Pages['/what-is-cryptpad.html'] = function () { return h('div#cp-main', [ infopageTopbar(), + h('div.container-fluid.cp-what-is',[ + h('div.container',[ + h('div.row',[ + h('div.col-12.text-center', h('h1', Msg.whatis_title)), + ]), + ]), + ]), h('div.container.cp-container', [ - h('center', h('h1', Msg.whatis_title)), - setHTML(h('h2'), Msg.whatis_collaboration), - setHTML(h('p'), Msg.whatis_collaboration_p1), - h('img', { src: '/customize/images/pad_screenshot.png?' + urlArgs }), - setHTML(h('p'), Msg.whatis_collaboration_p2), - setHTML(h('p'), Msg.whatis_collaboration_p3), - setHTML(h('h2'), Msg.whatis_zeroknowledge), - h('div.row', [ - h('div.col-md-4.align-self-center', [ - h('img#zeroknowledge', { src: '/customize/images/zeroknowledge_small.png?' + urlArgs }), + h('div.row.align-items-center', [ + h('div.col-12.col-sm-12.col-md-12.col-lg-6', [ + setHTML(h('h2'), Msg.whatis_collaboration), + setHTML(h('p'), Msg.whatis_collaboration_p1), + setHTML(h('p'), Msg.whatis_collaboration_p2), + setHTML(h('p'), Msg.whatis_collaboration_p3), + ]), + h('div.col-12.col-sm-12.col-md-12.col-lg-6', [ + h('img', { src: '/customize/images/pad_screenshot.png?' + urlArgs }), ]), - h('div.col-md-8', [ + ]), + h('div.row.align-items-center', [ + h('div.col-12.col-sm-12.col-md-12.col-lg-6.push-lg-6', [ + setHTML(h('h2'), Msg.whatis_zeroknowledge), setHTML(h('p'), Msg.whatis_zeroknowledge_p1), setHTML(h('p'), Msg.whatis_zeroknowledge_p2), setHTML(h('p'), Msg.whatis_zeroknowledge_p3), ]), + h('div.col-12.col-sm-12.col-md-12.col-lg-6.pull-lg-6', [ + h('img#zeroknowledge', { src: '/customize/images/zeroknowledge_small.png?' + urlArgs }), + ]), + ]), + h('div.row.align-items-center', [ + h('div.col-12.col-sm-12.col-md-12.col-lg-6', [ + setHTML(h('h2'), Msg.whatis_drive), + setHTML(h('p'), Msg.whatis_drive_p1), + setHTML(h('p'), Msg.whatis_drive_p2), + setHTML(h('p'), Msg.whatis_drive_p3), + ]), + h('div.col-12.col-sm-12.col-md-12.col-lg-6', [ + h('img', { src: '/customize/images/drive_screenshot.png?' + urlArgs }), + ]), + ]), + h('div.row.align-items-center', [ + h('div.col-12', [ + setHTML(h('h2.text-center'), Msg.whatis_business), + setHTML(h('p'), Msg.whatis_business_p1), + setHTML(h('p'), Msg.whatis_business_p2), + ]), ]), - setHTML(h('h2'), Msg.whatis_drive), - setHTML(h('p'), Msg.whatis_drive_p1), - h('img', { src: '/customize/images/drive_screenshot.png?' + urlArgs }), - setHTML(h('p'), Msg.whatis_drive_p2), - setHTML(h('p'), Msg.whatis_drive_p3), - setHTML(h('h2'), Msg.whatis_business), - setHTML(h('p'), Msg.whatis_business_p1), - setHTML(h('p'), Msg.whatis_business_p2), ]), infopageFooter(), ]); diff --git a/customize.dist/src/less2/pages/page-login.less b/customize.dist/src/less2/pages/page-login.less index cbde08893..050064348 100644 --- a/customize.dist/src/less2/pages/page-login.less +++ b/customize.dist/src/less2/pages/page-login.less @@ -73,6 +73,6 @@ } } .cp-container { - padding-top: 0; + padding-top: 3em; min-height: 66vh; } \ No newline at end of file diff --git a/customize.dist/src/less2/pages/page-what-is-cryptpad.less b/customize.dist/src/less2/pages/page-what-is-cryptpad.less index b57517a01..cf127e1ec 100644 --- a/customize.dist/src/less2/pages/page-what-is-cryptpad.less +++ b/customize.dist/src/less2/pages/page-what-is-cryptpad.less @@ -4,6 +4,40 @@ .infopages_main(); .infopages_topbar(); -img#zeroknowledge { - width: 100%; +.cp-what-is { + padding-top: 3em; + padding-bottom: 3em; + background-image: url(/customize/bkwhat.jpg); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + color: #fff; + h1 { + font-weight: 700; + } } +#cp-main { + background: #fff; +} +.cp-container { + padding-top: 3em; + padding-bottom: 3em; + h2 { + margin-top: 0; + font-weight: 700; + color: @cryptpad_header_col; + } + p { + color: @cryptpad_text_col + } + #zeroknowledge { + width: 65%; + } + .row { + margin-bottom: 1.5em; + } + img { + display: block; + margin: 0 auto; + } +} \ No newline at end of file From 4881f8d0308b63747cff90a7ef09edb28517ba9b Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Thu, 17 Aug 2017 12:12:40 +0200 Subject: [PATCH 10/63] Remove X-Frame-Options because it cannot work with a cross-domain iframe. --- config.example.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config.example.js b/config.example.js index 43438aea6..73444b94c 100644 --- a/config.example.js +++ b/config.example.js @@ -17,8 +17,7 @@ module.exports = { httpHeaders: { "X-XSS-Protection": "1; mode=block", - "X-Content-Type-Options": "nosniff", - 'X-Frame-Options': 'SAMEORIGIN', + "X-Content-Type-Options": "nosniff" }, contentSecurity: [ From fb512c89230e942e4f578d5fae871be6f7483213 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 17 Aug 2017 14:55:44 +0200 Subject: [PATCH 11/63] Toolbar in pad2 --- www/common/common-inner.js | 8 + www/common/metadata-manager.js | 22 +- www/common/toolbar3.js | 1044 ++++++++++++++++++++++++++++++++ www/pad2/main.js | 19 +- 4 files changed, 1085 insertions(+), 8 deletions(-) create mode 100644 www/common/common-inner.js create mode 100644 www/common/toolbar3.js diff --git a/www/common/common-inner.js b/www/common/common-inner.js new file mode 100644 index 000000000..921d10e88 --- /dev/null +++ b/www/common/common-inner.js @@ -0,0 +1,8 @@ +define([ + 'jquery' +], function ($) { + var common = {}; + + + return common; +}); diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 930a7c138..8a87b7f0a 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -20,18 +20,21 @@ define([], function () { } var mdo = {}; // We don't want to add our user data to the object multiple times. - var containsYou = false; + //var containsYou = false; //console.log(metadataObj); + console.log(metadataObj.users); Object.keys(metadataObj.users).forEach(function (x) { if (members.indexOf(x) === -1) { return; } mdo[x] = metadataObj.users[x]; - if (metadataObj.users[x].uid === meta.user.uid) { + /*if (metadataObj.users[x].uid === meta.user.uid) { //console.log('document already contains you'); containsYou = true; - } + }*/ }); - if (!containsYou) { mdo[meta.user.netfluxId] = meta.user; } + //if (!containsYou) { mdo[meta.user.netfluxId] = meta.user; } + mdo[meta.user.netfluxId] = meta.user; metadataObj.users = mdo; + dirty = false; changeHandlers.forEach(function (f) { f(); }); }; @@ -74,8 +77,15 @@ define([], function () { checkUpdate(); return metadataObj; }, - onChange: function (f) { changeHandlers.push(f); } + onChange: function (f) { changeHandlers.push(f); }, + getNetfluxId: function () { + return meta && meta.user && meta.user.netfluxId; + }, + getUserlist: function () { + var list = members.slice().filter(function (m) { return m.length === 32; }); + return list; + } }); }; return Object.freeze({ create: create }); -}); \ No newline at end of file +}); diff --git a/www/common/toolbar3.js b/www/common/toolbar3.js new file mode 100644 index 000000000..7b5cd287a --- /dev/null +++ b/www/common/toolbar3.js @@ -0,0 +1,1044 @@ +define([ + 'jquery', + '/customize/application_config.js', + '/api/config', +], function ($, Config, ApiConfig) { + var Messages = {}; + var Cryptpad; + + var Bar = { + constants: {}, + }; + + var SPINNER_DISAPPEAR_TIME = 1000; + + // Toolbar parts + var TOOLBAR_CLS = Bar.constants.toolbar = 'cryptpad-toolbar'; + var TOP_CLS = Bar.constants.top = 'cryptpad-toolbar-top'; + var LEFTSIDE_CLS = Bar.constants.leftside = 'cryptpad-toolbar-leftside'; + var RIGHTSIDE_CLS = Bar.constants.rightside = 'cryptpad-toolbar-rightside'; + var DRAWER_CLS = Bar.constants.drawer = 'drawer-content'; + var HISTORY_CLS = Bar.constants.history = 'cryptpad-toolbar-history'; + + // Userlist + var USERLIST_CLS = Bar.constants.userlist = "cryptpad-dropdown-users"; + var EDITSHARE_CLS = Bar.constants.editShare = "cryptpad-dropdown-editShare"; + var VIEWSHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-viewShare"; + var SHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-share"; + + // Top parts + var USER_CLS = Bar.constants.userAdmin = "cryptpad-user"; + var SPINNER_CLS = Bar.constants.spinner = 'cryptpad-spinner'; + var LIMIT_CLS = Bar.constants.lag = 'cryptpad-limit'; + var TITLE_CLS = Bar.constants.title = "cryptpad-title"; + var NEWPAD_CLS = Bar.constants.newpad = "cryptpad-new"; + + // User admin menu + var USERADMIN_CLS = Bar.constants.user = 'cryptpad-user-dropdown'; + var USERNAME_CLS = Bar.constants.username = 'cryptpad-toolbar-username'; + var READONLY_CLS = Bar.constants.readonly = 'cryptpad-readonly'; + var USERBUTTON_CLS = Bar.constants.changeUsername = "cryptpad-change-username"; + + // Create the toolbar element + + var uid = function () { + return 'cryptpad-uid-' + String(Math.random()).substring(2); + }; + + var createRealtimeToolbar = function (config) { + if (!config.$container) { return; } + var $container = config.$container; + var $toolbar = $('
', { + 'class': TOOLBAR_CLS, + id: uid(), + }); + + // TODO iframe + // var parsed = Cryptpad.parsePadUrl(window.location.href); + var parsed = { type:'pad' }; + if (typeof parsed.type === "string") { + config.$container.parents('body').addClass('app-' + parsed.type); + } + + var $topContainer = $('
', {'class': TOP_CLS}); + $('', {'class': 'filler'}).appendTo($topContainer); + var $userContainer = $('', { + 'class': USER_CLS + }).appendTo($topContainer); + $('', {'class': LIMIT_CLS}).hide().appendTo($userContainer); + $('', {'class': NEWPAD_CLS + ' dropdown-bar'}).hide().appendTo($userContainer); + $('', {'class': USERADMIN_CLS + ' dropdown-bar'}).hide().appendTo($userContainer); + + $toolbar.append($topContainer) + .append($('
', {'class': LEFTSIDE_CLS})) + .append($('
', {'class': RIGHTSIDE_CLS})) + .append($('
', {'class': HISTORY_CLS})); + + // TODO + /* + var $rightside = $toolbar.find('.'+RIGHTSIDE_CLS); + if (!config.hideDrawer) { + var $drawerContent = $('
', { + 'class': DRAWER_CLS,// + ' dropdown-bar-content cryptpad-dropdown' + 'tabindex': 1 + }).appendTo($rightside).hide(); + var $drawer = Cryptpad.createButton('more', true).appendTo($rightside); + $drawer.click(function () { + $drawerContent.toggle(); + $drawer.removeClass('active'); + if ($drawerContent.is(':visible')) { + $drawer.addClass('active'); + $drawerContent.focus(); + } + }); + var onBlur = function (e) { + if (e.relatedTarget) { + if ($(e.relatedTarget).is('.drawer-button')) { return; } + if ($(e.relatedTarget).parents('.'+DRAWER_CLS).length) { + $(e.relatedTarget).blur(onBlur); + return; + } + } + $drawer.removeClass('active'); + $drawerContent.hide(); + }; + $drawerContent.blur(onBlur); + }*/ + + // The 'notitle' class removes the line added for the title with a small screen + if (!config.title || typeof config.title !== "object") { + $toolbar.addClass('notitle'); + } + + $container.prepend($toolbar); + + $container.on('drop dragover', function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + return $toolbar; + }; + + // Userlist elements + + var getOtherUsers = function(config) { + var userList = config.userList.getUserlist(); + var userData = config.userList.getMetadata().users; + + var i = 0; // duplicates counter + var list = []; + + // Display only one time each user (if he is connected in multiple tabs) + var uids = []; + userList.forEach(function(user) { + //if (user !== userNetfluxId) { + var data = userData[user] || {}; + var userId = data.uid; + if (!userId) { return; } + data.netfluxId = user; + if (uids.indexOf(userId) === -1) {// && (!myUid || userId !== myUid)) { + uids.push(userId); + list.push(data); + } else { i++; } + //} + }); + return { + list: list, + duplicates: i + }; + }; + var arrayIntersect = function(a, b) { + return $.grep(a, function(i) { + return $.inArray(i, b) > -1; + }); + }; + var updateDisplayName = function (toolbar, config) { + // Change username in useradmin dropdown + var name = Cryptpad.getDisplayName(); + if (config.displayed.indexOf('useradmin') !== -1) { + var $userAdminElement = toolbar.$userAdmin; + var $userElement = $userAdminElement.find('.' + USERNAME_CLS); + $userElement.show(); + if (config.readOnly === 1) { + $userElement.addClass(READONLY_CLS).text(Messages.readonly); + } + else { + if (!name) { + name = Messages.anonymous; + } + $userElement.removeClass(READONLY_CLS).text(name); + } + } + }; + var avatars = {}; + var updateUserList = function (toolbar, config) { + // Make sure the elements are displayed + var $userButtons = toolbar.userlist; + var $userlistContent = toolbar.userlistContent; + + var userList = config.userList.getUserlist(); + var userData = config.userList.getMetadata().users; +console.log(userList, userData); + var numberOfUsers = userList.length; + + // If we are using old pads (readonly unavailable), only editing users are in userList. + // With new pads, we also have readonly users in userList, so we have to intersect with + // the userData to have only the editing users. We can't use userData directly since it + // may contain data about users that have already left the channel. + userList = config.readOnly === -1 ? userList : arrayIntersect(userList, Object.keys(userData)); + + // Names of editing users + var others = getOtherUsers(config); + var editUsersNames = others.list; + var duplicates = others.duplicates; // Number of duplicates + + editUsersNames.sort(function (a, b) { + var na = a.name || Messages.anonymous; + var nb = b.name || Messages.anonymous; + return na.toLowerCase() > nb.toLowerCase(); + }); + + var numberOfEditUsers = userList.length - duplicates; + var numberOfViewUsers = numberOfUsers - userList.length; + + // Update the userlist + var $editUsers = $userlistContent.find('.' + USERLIST_CLS).html(''); + Cryptpad.clearTooltips(); + + var $editUsersList = $('
', {'class': 'userlist-others'}); + + // Editors + // TODO iframe enable friends + //var pendingFriends = Cryptpad.getPendingInvites(); + editUsersNames.forEach(function (data) { + var name = data.name || Messages.anonymous; + var $span = $('', {'class': 'avatar'}); + var $rightCol = $('', {'class': 'right-col'}); + var $nameSpan = $('', {'class': 'name'}).text(name).appendTo($rightCol); + //var proxy = Cryptpad.getProxy(); + //var isMe = data.curvePublic === proxy.curvePublic; + + /*if (Cryptpad.isLoggedIn() && data.curvePublic) { + if (isMe) { + $span.attr('title', Messages._getKey('userlist_thisIsYou', [ + name + ])); + $nameSpan.text(name); + } else if (!proxy.friends || !proxy.friends[data.curvePublic]) { + if (pendingFriends.indexOf(data.netfluxId) !== -1) { + $('', {'class': 'friend'}).text(Messages.userlist_pending) + .appendTo($rightCol); + } else { + $('', { + 'class': 'fa fa-user-plus friend', + 'title': Messages._getKey('userlist_addAsFriendTitle', [ + name + ]) + }).appendTo($rightCol).click(function (e) { + e.stopPropagation(); + Cryptpad.inviteFromUserlist(Cryptpad, data.netfluxId); + }); + } + } + }*/ + if (data.profile) { + $span.addClass('clickable'); + $span.click(function () { + window.open('/profile/#' + data.profile); + }); + } + if (data.avatar && avatars[data.avatar]) { + $span.append(avatars[data.avatar]); + $span.append($rightCol); + } else { + Cryptpad.displayAvatar($span, data.avatar, name, function ($img) { + if (data.avatar && $img) { + avatars[data.avatar] = $img[0].outerHTML; + } + $span.append($rightCol); + }); + } + $span.data('uid', data.uid); + $editUsersList.append($span); + }); + $editUsers.append($editUsersList); + + // Viewers + if (numberOfViewUsers > 0) { + var viewText = '
'; + var viewerText = numberOfViewUsers !== 1 ? Messages.viewers : Messages.viewer; + viewText += numberOfViewUsers + ' ' + viewerText + '
'; + $editUsers.append(viewText); + } + + // Update the buttons + var fa_editusers = ''; + var fa_viewusers = ''; + var $spansmall = $('').html(fa_editusers + ' ' + numberOfEditUsers + '   ' + fa_viewusers + ' ' + numberOfViewUsers); + $userButtons.find('.buttonTitle').html('').append($spansmall); + + updateDisplayName(toolbar, config); + }; + + var initUserList = function (toolbar, config) { + // TODO clean comments + if (config.userList) { /* && config.userList.list && config.userList.userNetfluxId) {*/ + //var userList = config.userList.list; + //userList.change.push + var metadataMgr = config.userList; + metadataMgr.onChange(function () { + var users = metadataMgr.getUserlist(); + if (users.indexOf(metadataMgr.getNetfluxId()) !== -1) {toolbar.connected = true;} + if (!toolbar.connected) { return; } + //if (config.userList.data) { + updateUserList(toolbar, config); + //} + }); + } + }; + + + // Create sub-elements + + var createUserList = function (toolbar, config) { + if (!config.userList) { /* || !config.userList.list || + !config.userList.data || !config.userList.userNetfluxId) {*/ + throw new Error("You must provide a `userList` object to display the userlist"); + } + var $content = $('
', {'class': 'userlist-drawer'}); + $content.on('drop dragover', function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + var $closeIcon = $('', {"class": "fa fa-window-close close"}).appendTo($content); + $('

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

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