From 723ecc8bd6bdffcb6685f3871283ef3aa603f3aa Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 16 Jan 2023 21:22:57 +0100 Subject: [PATCH] Integration API prototype --- lib/defaults.js | 2 +- lib/env.js | 2 + lib/historyKeeper.js | 5 + lib/hk-util.js | 6 + server.js | 4 + www/common/sframe-app-outer.js | 16 ++- www/common/sframe-common-outer.js | 11 +- www/cryptpad-api.js | 196 +++++++++++++++++++++++++++++ www/integration/app-nextcloud.less | 28 +++++ www/integration/index.html | 15 +++ www/integration/main.js | 153 ++++++++++++++++++++++ www/nextcloud/app-nextcloud.less | 28 +++++ www/nextcloud/index.html | 26 ++++ www/nextcloud/main.js | 48 +++++++ 14 files changed, 534 insertions(+), 6 deletions(-) create mode 100644 www/cryptpad-api.js create mode 100644 www/integration/app-nextcloud.less create mode 100644 www/integration/index.html create mode 100644 www/integration/main.js create mode 100644 www/nextcloud/app-nextcloud.less create mode 100644 www/nextcloud/index.html create mode 100644 www/nextcloud/main.js diff --git a/lib/defaults.js b/lib/defaults.js index f22277538..d0ba96805 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -26,7 +26,7 @@ Default.commonCSP = function (Env) { if you are deploying to production, you'll probably want to remove the ws://* directive */ - "connect-src 'self' blob: " + (/^https:/.test(domain)? 'wss:': domain.replace('http://', 'ws://')) + ' ' + domain + sandbox + accounts_api, + "connect-src 'self' localhost blob: " + (/^https:/.test(domain)? 'wss:': domain.replace('http://', 'ws://')) + ' ' + domain + sandbox + accounts_api, // data: is used by codemirror "img-src 'self' data: blob:" + domain, diff --git a/lib/env.js b/lib/env.js index ce0a7e7b8..ba829f190 100644 --- a/lib/env.js +++ b/lib/env.js @@ -220,6 +220,8 @@ module.exports.create = function (config) { curvePrivate: curve.secretKey, curvePublic: Nacl.util.encodeBase64(curve.publicKey), + + selfDestructTo: {}, }; (function () { diff --git a/lib/historyKeeper.js b/lib/historyKeeper.js index 44afe0ae0..b874aa074 100644 --- a/lib/historyKeeper.js +++ b/lib/historyKeeper.js @@ -63,6 +63,11 @@ module.exports.create = function (Env, cb) { error: err, }); } + + if (Env.selfDestructTo && Env.selfDestructTo[channelName]) { + clearTimeout(Env.selfDestructTo[channelName]); + } + if (!metadata || (metadata && !metadata.restricted)) { // the channel doesn't have metadata, or it does and it's not restricted // either way, let them join. diff --git a/lib/hk-util.js b/lib/hk-util.js index a48358cc3..342bd4e70 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -133,8 +133,14 @@ const expireChannel = function (Env, channel) { * cleans up memory structures which are managed entirely by the historyKeeper */ const dropChannel = HK.dropChannel = function (Env, chanName) { + let meta = Env.metadata_cache[chanName]; delete Env.metadata_cache[chanName]; delete Env.channel_cache[chanName]; + if (meta && meta.selfdestruct && Env.selfDestructTo) { + Env.selfDestructTo[chanName] = setTimeout(function () { + expireChannel(Env, chanName); // XXX add new function? + }, 30*1000); // XXX CONSTANT XXX XXX XXX + } }; /* checkExpired diff --git a/server.js b/server.js index d8ae1d034..9a4cb175f 100644 --- a/server.js +++ b/server.js @@ -164,6 +164,10 @@ app.get(mainPagePattern, Express.static(Path.resolve('customize.dist'))); app.use("/blob", Express.static(Env.paths.blob, { maxAge: Env.DEV_MODE? "0d": "365d" })); + +app.head("/datastore", Express.static(Env.paths.data, { + maxAge: "0d" +})); app.use("/datastore", Express.static(Env.paths.data, { maxAge: "0d" })); diff --git a/www/common/sframe-app-outer.js b/www/common/sframe-app-outer.js index 3d3cfaf28..38e21f108 100644 --- a/www/common/sframe-app-outer.js +++ b/www/common/sframe-app-outer.js @@ -6,21 +6,29 @@ define([ '/common/sframe-common-outer.js' ], function (nThen, ApiConfig, DomReady, SFCommonO) { + var isIntegration = Boolean(window.CP_integration_outer); + var integration = window.CP_integration_outer || {}; + var hash, href; nThen(function (waitFor) { DomReady.onReady(waitFor()); }).nThen(function (waitFor) { - var obj = SFCommonO.initIframe(waitFor, true); + var obj = SFCommonO.initIframe(waitFor, true, integration.pathname); href = obj.href; hash = obj.hash; + if (isIntegration) { + href = integration.href; + hash = integration.hash; + } }).nThen(function (/*waitFor*/) { SFCommonO.start({ - cache: true, + cache: !isIntegration, noDrive: true, hash: hash, href: href, - useCreationScreen: true, - messaging: true + useCreationScreen: !isIntegration, + messaging: true, + integration: isIntegration }); }); }); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 23e776218..54ae897ba 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -15,6 +15,7 @@ define([ 'pad', 'slide', 'whiteboard', + 'integration' ].map(function (x) { return `/${x}/`; }); @@ -695,7 +696,8 @@ define([ fromContent: Cryptpad.fromContent, burnAfterReading: burnAfterReading, storeInTeam: Cryptpad.initialTeam || (Cryptpad.initialPath ? -1 : undefined), - supportsWasm: Utils.Util.supportsWasm() + supportsWasm: Utils.Util.supportsWasm(), + integration: cfg.integration }; if (window.CryptPad_newSharedFolder) { additionalPriv.newSharedFolder = window.CryptPad_newSharedFolder; @@ -1966,6 +1968,8 @@ define([ placeholder.remove(); } + + var replaceHash = function (hash) { // The pad has just been created but is not stored yet. We'll switch // to hidden hash once the pad is stored @@ -2059,6 +2063,11 @@ define([ var rtConfig = { metadata: {} }; + + if (cfg.integration) { + rtConfig.metadata.selfdestruct = true; + } + if (data.team) { Cryptpad.initialTeam = data.team.id; } diff --git a/www/cryptpad-api.js b/www/cryptpad-api.js new file mode 100644 index 000000000..ba6cb3bc5 --- /dev/null +++ b/www/cryptpad-api.js @@ -0,0 +1,196 @@ +(function () { + 'use strict'; + var factory = function (/*Hash*/) { + + // This API is used to load a CryptPad editor for a provided document in + // an external platform. + // The external platform needs to store a session key and make it + // available to all users who needs to access the realtime editor. + + var getTxid = function () { + return Math.random().toString(16).replace('0.', ''); + }; + + var makeChan = function (iframe, iOrigin) { + var handlers = {}; + var commands = {}; + + var iWindow = iframe.contentWindow; + var _sendCb = function (txid, args) { + iWindow.postMessage({ ack: txid, args: args}, iOrigin); + }; + var onMsg = function (ev) { + if (ev.source !== iWindow) { return; } + var data = ev.data; + + // On ack + if (data.ack) { + if (handlers[data.ack]) { + handlers[data.ack](data.args); + } + return; + } + + // On new command + var msg = data.msg; + var txid = data.txid; + if (commands[msg.q]) { + console.warn('OUTER RECEIVED QUERY', msg.q, msg.data); + commands[msg.q](msg.data, function (args) { + _sendCb(txid, args); + }); + return; + } + + }; + window.addEventListener('message', onMsg); + + var send = function (q, data, cb) { + var txid = getTxid(); + if (cb) { handlers[txid] = cb; } + + console.warn('OUTER SENT QUERY', q, data); + iWindow.postMessage({ msg: { + q: q, + data: data, + }, txid: txid}, iOrigin); + setTimeout(function () { + delete handlers[txid]; + }, 60000); + }; + var on = function (q, handler) { + if (typeof(handler) !== "function") { return; } + commands[q] = handler; + }; + + return { + send: send, + on: on + }; + }; + + var start = function (config, chan) { + return new Promise(function (resolve, reject) { + setTimeout(function () { + var key = config.document.key; + + chan.on('SAVE', function (data) { + config.events.onSave(data); + }); + + var onKeyValidated = function () { + chan.send('START', { + key: key, + document: config.document.url, + }, function (obj) { + if (obj && obj.error) { reject(obj.error); return console.error(obj.error); } + console.log('OUTER START SUCCESS'); + resolve({}); + }); + }; + + chan.send('GET_SESSION', { + key: key + }, function (obj) { + if (obj && obj.error) { reject(obj.error); return console.error(obj.error); } + if (obj.key !== key) { + key = obj.key; + config.events.onNewKey(key); + } + onKeyValidated(); + }); + + }); + }); + }; + + /** + * Create a CryptPad collaborative editor for the provided document. + * + * @param {string} cryptpadURL The URL of the CryptPad server. + * @param {string} containerID (optional) The ID of the HTML element containing the iframe. + * @param {object} config The object containing configuration parameters. + * @param {object} config.document The document to load. + * @param {string} document.url The document URL. + * @param {string} document.key The collaborative session key. + * @param {object} config.events Event handlers. + * @param {function} events.onSave The save function to store the document when edited. + * @param {function} events.onNewKey The function called when a new key is used. + * @param {string} config.documentType The editor to load in CryptPad. + * @return {promise} + */ + var init = function (cryptpadURL, containerId, config) { + return new Promise(function (resolve, reject) { + setTimeout(function () { + + if (!cryptpadURL || typeof(cryptpadURL) !== "string") { + return reject('Missing arg: cryptpadURL'); + } + var container; + if (containerId) { + container = document.getElementById('containerId'); + } + if (!container) { + console.warn('No container provided, append to body'); + container = document.body; + } + + if (!config) { return reject('Missing args: no data provided'); } + ['document.url', 'document.key', 'documentType', + 'events.onSave', 'events.onNewKey'].some(function (k) { + var s = k.split('.'); + var c = config; + return s.some(function (key) { + if (!c[key]) { + reject(`Missing args: no "config.${k}" provided`); + return true; + } + c = c[key]; + }); + }); + + cryptpadURL = cryptpadURL.replace(/(\/)+$/, ''); + var url = cryptpadURL + '/integration/'; + var parsed; + try { + parsed = new URL(url); + } catch (e) { + console.error(e); + return reject('Invalid arg: cryptpadURL'); + } + + var iframe = document.createElement('iframe'); + iframe.setAttribute('id', 'cryptpad-editor'); + iframe.setAttribute("src", url); + container.appendChild(iframe); + + var onMsg = function (msg) { + var data = typeof(msg.data) === "string" ? JSON.parse(msg.data) : msg.data; + if (!data || data.q !== 'INTEGRATION_READY') { return; } + window.removeEventListener('message', onMsg); + var chan = makeChan(iframe, parsed.origin); + start(config, chan).then(resolve).catch(reject); + }; + window.addEventListener('message', onMsg); + + }); + }); + }; + + return init; + }; + + + + if (typeof(module) !== 'undefined' && module.exports) { + module.exports = factory(); + } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { + define([], function () { + return factory(); + }); + } else { + window.CryptPadAPI = factory(); + } +}()); + + diff --git a/www/integration/app-nextcloud.less b/www/integration/app-nextcloud.less new file mode 100644 index 000000000..6ecaf206c --- /dev/null +++ b/www/integration/app-nextcloud.less @@ -0,0 +1,28 @@ +@import (reference) '../../customize/src/less2/include/framework.less'; + +&.cp-app-openin { + .framework_min_main(); + margin: 0; + + header { + height: 100px; + padding: 20px; + display: flex; + align-items: center; + justify-content: flex-end; + border-bottom: 1px solid white; + box-sizing: border-box; + } + + & > iframe { + position: fixed; + top: 100px; + left: 0; + right: 0; + bottom: 0; + border: none; + width: 100%; + height: calc(100% - 100px); + box-sizing: border-box; + } +} diff --git a/www/integration/index.html b/www/integration/index.html new file mode 100644 index 000000000..3d8a2afc4 --- /dev/null +++ b/www/integration/index.html @@ -0,0 +1,15 @@ + + + + + CryptPad: Collaboration suite, encrypted and open-source + + + + + + + + + + diff --git a/www/integration/main.js b/www/integration/main.js new file mode 100644 index 000000000..fa74a5394 --- /dev/null +++ b/www/integration/main.js @@ -0,0 +1,153 @@ +define([ + '/common/sframe-common-outer.js', + '/common/common-hash.js', + '/common/cryptget.js', + '/bower_components/nthen/index.js', +], function (SCO, Hash, Crypt, nThen) { + + var getTxid = function () { + return Math.random().toString(16).replace('0.', ''); + }; + var init = function () { + console.warn('INIT'); + var p = window.parent; + var txid = getTxid(); + p.postMessage(JSON.stringify({ q: 'INTEGRATION_READY', txid: txid }), '*'); + + var makeChan = function () { + var handlers = {}; + var commands = {}; + + var _sendCb = function (txid, args) { + p.postMessage({ ack: txid, args: args}, '*'); + }; + var onMsg = function (ev) { + if (ev.source !== p) { return; } + var data = ev.data; + + // On ack + if (data.ack) { + if (handlers[data.ack]) { + handlers[data.ack](data.args); + delete handlers[data.ack]; + } + return; + } + + // On new command + var msg = data.msg; + var txid = data.txid; + if (commands[msg.q]) { + commands[msg.q](msg.data, function (args) { + _sendCb(txid, args); + }); + return; + } + + }; + window.addEventListener('message', onMsg); + + var send = function (q, data, cb) { + var txid = getTxid(); + if (cb) { handlers[txid] = cb; } + p.postMessage({ msg: { + q: q, + data: data, + }, txid: txid}, '*'); + setTimeout(function () { + delete handlers[txid]; + }, 60000); + }; + var on = function (q, handler) { + if (typeof(handler) !== "function") { return; } + commands[q] = handler; + }; + + return { + send: send, + on: on + }; + }; + var chan = makeChan(); + + var isNew = false; + // Make a HEAD request to the servre to check if a file exists in datastore + // XXX update nginx config + var checkSession = function (oldKey, cb) { + var channel = Hash.hrefToHexChannelId(Hash.hashToHref(oldKey)); + var prefix = channel.slice(0,2); + var url = `/datastore/${prefix}/${channel}.ndjson`; + + var http = new XMLHttpRequest(); + http.open('HEAD', url); + http.onreadystatechange = function() { + if (this.readyState === this.DONE) { + console.error(this.status); + if (this.status === 200) { + return cb({state: true}); + } + if (this.status === 404) { + return cb({state: false}); + } + cb({error: 'Internal server error'}); + } + }; + http.send(); + }; + chan.on('GET_SESSION', function (data, cb) { + var getHash = function () { + isNew = true; + return Hash.createRandomHash('integration'); + }; + var oldKey = data.sessionKey; + if (!oldKey) { return void cb({ key: getHash() }); } + + checkSession(oldKey, function (obj) { + if (!obj || obj.error) { return cb(obj); } + cb({ + key: obj.state ? oldKey : getHash() + }); + }); + }); + + chan.on('START', function (data) { + console.warn('INNER START', data); + nThen(function (w) { + if (!isNew) { return; } + + // XXX initial content TBD + var content = JSON.stringify({ + content: data.document, + highlightMode: "gfm" + }); // XXX only for code + + console.error('CRYPTPUT', data.sessionKey); + Crypt.put(data.sessionKey, content, w(), { + metadata: { + selfdestruct: true + } + }); + }).nThen(function () { + var href = Hash.hashToHref(data.sessionKey, data.application); + console.error(Hash.hrefToHexChannelId(href)); + window.CP_integration_outer = { + pathname: `/${data.application}/`, + hash: data.sessionKey, + href: href + }; + require(['/common/sframe-app-outer.js'], function () { + console.warn('SAO REQUIRED'); + delete window.CP_integration_outer; + }); + }); + }); + + }; + init(); + /* + nThen(function (waitFor) { + }).nThen(function () { + }); + */ + +}); diff --git a/www/nextcloud/app-nextcloud.less b/www/nextcloud/app-nextcloud.less new file mode 100644 index 000000000..6ecaf206c --- /dev/null +++ b/www/nextcloud/app-nextcloud.less @@ -0,0 +1,28 @@ +@import (reference) '../../customize/src/less2/include/framework.less'; + +&.cp-app-openin { + .framework_min_main(); + margin: 0; + + header { + height: 100px; + padding: 20px; + display: flex; + align-items: center; + justify-content: flex-end; + border-bottom: 1px solid white; + box-sizing: border-box; + } + + & > iframe { + position: fixed; + top: 100px; + left: 0; + right: 0; + bottom: 0; + border: none; + width: 100%; + height: calc(100% - 100px); + box-sizing: border-box; + } +} diff --git a/www/nextcloud/index.html b/www/nextcloud/index.html new file mode 100644 index 000000000..202aa1ee7 --- /dev/null +++ b/www/nextcloud/index.html @@ -0,0 +1,26 @@ + + + + + CryptPad: Collaboration suite, encrypted and open-source + + + + + + + +
Pew pew pew
diff --git a/www/nextcloud/main.js b/www/nextcloud/main.js new file mode 100644 index 000000000..113b1b7ca --- /dev/null +++ b/www/nextcloud/main.js @@ -0,0 +1,48 @@ +var url = 'http://localhost:3000'; +define([ + 'jquery', + url + '/cryptpad-api.js' +], function ($, Api) { + if (window.top !== window) { return; } + $(function () { + + console.log(Api); + var permaKey = '/2/integration/edit/X3RlrgR2JhA0rI+PJ3rXufsQ/'; // XXX + var key = window.location.hash ? window.location.hash.slice(1) + : permaKey; + +// Test doc +var mystring = "Hello World!"; +var blob = new Blob([mystring], { + type: 'text/markdown' +}); +var docUrl = URL.createObjectURL(blob); + + + var onSave = function (data) { + console.log('APP ONSAVE', data); + }; + var onNewKey = function (newKey) { + window.location.hash = newKey; + }; + + + Api(url, null, { + document: { + url: docUrl, + key: key + }, + documentType: 'code', // appname + events: { + onSave: onSave, + onNewKey: onNewKey + } + }).then(function () { + console.log('SUCCESS'); + }).catch(function (e) { + console.error('ERROR', e); + }); + + + }); +});