Integration API prototype

This commit is contained in:
yflory 2023-01-16 21:22:57 +01:00 committed by Wolfgang Ginolas
parent a5d5dba9f2
commit 723ecc8bd6
14 changed files with 534 additions and 6 deletions

View file

@ -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,

View file

@ -220,6 +220,8 @@ module.exports.create = function (config) {
curvePrivate: curve.secretKey,
curvePublic: Nacl.util.encodeBase64(curve.publicKey),
selfDestructTo: {},
};
(function () {

View file

@ -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.

View file

@ -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

View file

@ -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"
}));

View file

@ -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
});
});
});

View file

@ -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;
}

196
www/cryptpad-api.js Normal file
View file

@ -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();
}
}());

View file

@ -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;
}
}

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
<head>
<title data-localization="main_title">CryptPad: Collaboration suite, encrypted and open-source</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script src="/customize/pre-loading.js?ver=1.1"></script>
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
<script async data-bootload="/integration/main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link href="/customize/src/outer.css?ver=1.3.2" rel="stylesheet" type="text/css">
</head>
<body class="html cp-app-integration">
<iframe-placeholder>

153
www/integration/main.js Normal file
View file

@ -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 () {
});
*/
});

View file

@ -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;
}
}

26
www/nextcloud/index.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
<head>
<title data-localization="main_title">CryptPad: Collaboration suite, encrypted and open-source</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/favicon/main-favicon.png" id="favicon"/>
<script async data-bootload="/nextcloud/main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
#main {
color: white;
}
#cryptpad-editor {
border: none;
width: 100%;
height: calc(100% - 100px);
position: absolute;
top: 100px;
left: 0;
box-sizing: border-box;
}
</style>
</head>
<body class="html cp-app-openin">
<div id="main">Pew pew pew</div>

48
www/nextcloud/main.js Normal file
View file

@ -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);
});
});
});