mirror of https://github.com/xwiki-labs/cryptpad
Integration API prototype
This commit is contained in:
parent
a5d5dba9f2
commit
723ecc8bd6
|
@ -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,
|
||||
|
|
|
@ -220,6 +220,8 @@ module.exports.create = function (config) {
|
|||
|
||||
curvePrivate: curve.secretKey,
|
||||
curvePublic: Nacl.util.encodeBase64(curve.publicKey),
|
||||
|
||||
selfDestructTo: {},
|
||||
};
|
||||
|
||||
(function () {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}));
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}());
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 () {
|
||||
});
|
||||
*/
|
||||
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue