cryptpad/www/cryptpad-api.js

367 lines
14 KiB
JavaScript

// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: MIT
(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 getInstanceURL = () => {
var scripts = document.getElementsByTagName('script');
for (var i = scripts.length - 1; i >= 0; i--) {
var match = scripts[i].src.match(/(.*)web-apps\/apps\/api\/documents\/api.js/i);
if (match) { return match[1]; }
}
};
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 makeIframe = function () {}; // placeholder
let onDocumentReady = [];
var start = function (config, chan) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
var key = config.document.key;
var blob;
var getBlob = function (cb) {
var xhr = new XMLHttpRequest();
let url = config.document.url;
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = function () {
if (this.status === 200) {
var blob = this.response;
// myBlob is now the blob that the object URL pointed to.
cb(null, blob);
} else {
cb(this.status);
}
};
xhr.onerror = function (e) {
cb(e.message);
};
xhr.send();
};
var start = function () {
config.document.key = key;
chan.send('START', {
key: key,
application: config.documentType,
document: blob,
ext: config.document.fileType,
autosave: config.autosave || 10
}, function (obj) {
if (obj && obj.error) { reject(obj.error); return console.error(obj.error); }
resolve({});
resolve = function () {};
reject = function () {};
});
};
var onKeyValidated = function () {
if (config.document.blob) { // This is a reload
blob = config.document.blob;
return start();
}
getBlob(function (err, _blob) {
if (err) { reject(err); return console.error(err); }
_blob.name = `document.${config.document.fileType}`;
blob = _blob;
start();
});
};
var getSession = function (cb) {
chan.send('GET_SESSION', {
key: key,
keepOld: !config.events.onNewKey
}, function (obj) {
if (obj && obj.error) { reject(obj.error); return console.error(obj.error); }
// OnlyOffice
if (!config.events.onNewKey) {
key = obj.key;
console.error(key, obj);
return void cb();
}
if (obj.key !== key) {
// The outside app may reject our new key if the "old" one is deprecated.
// This will happen if multiple users try to update the key at the same
// time and in this case, only the first user will be able to generate a key.
return config.events.onNewKey({
old: key,
new: obj.key
}, function (_key) {
// Delay reloading tabs with deprecated key
var to = _key !== obj.key ? 1000 : 0;
key = _key || obj.key;
setTimeout(cb, to);
});
}
cb();
});
};
getSession(onKeyValidated);
chan.on('DOCUMENT_READY', function () {
if (config.events.onAppReady) {
config.events.onAppReady();
}
if (config.events.onReady) {
config.events.onReady();
}
if (config.events.onDocumentReady) {
config.events.onDocumentReady();
}
onDocumentReady.forEach(f => {
try { f(); } catch (e) { console.error(e); }
});
});
chan.on('ON_DOWNLOADAS', blob => {
let url = URL.createObjectURL(blob);
config.events.onDownloadAs({
data: {
fileType: config.document && config.document.fileType,
url
}
});
});
chan.on('SAVE', function (data, cb) {
blob = data;
config.events.onSave(data, cb);
});
chan.on('RELOAD', function () {
config.document.blob = blob;
if (!config.editorConfig) { // Not OnlyOffice shim
document.getElementById('cryptpad-editor').remove();
}
makeIframe(config);
});
chan.on('HAS_UNSAVED_CHANGES', function(unsavedChanges, cb) {
if (config.events.onHasUnsavedChanges) {
config.events.onHasUnsavedChanges(unsavedChanges);
}
cb();
});
chan.on('ON_INSERT_IMAGE', function(data, cb) {
if (config.events.onIntertImage) {
config.events.onInsertImage(data, cb);
} else { cb(); }
});
});
});
};
/**
* 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.fileType The document extension (md, xml, html, etc.).
* @param {string} document.key The collaborative session key.
* @param {object} config.events Event handlers.
* @param {function} events.onSave (blob, callback) The save function to store the document when edited.
* @param {function} events.onNewKey (data, callback) The function called when a new key is used.
* @param {function} events.onInsertImage (data, callback) The function called the user wants to add an image.
* @param {string} config.documentType The editor to load in CryptPad.
* @return {promise}
*/
var init = function (cryptpadURL, containerId, config) {
// OnlyOffice shim: don't provide a URL
if (!config && typeof(containerId) === "object") {
config = containerId;
containerId = cryptpadURL;
cryptpadURL = getInstanceURL();
}
// OnlyOffice shim
let url = config.document.url;
if (/^http:\/\/localhost\/cache\/files\//.test(url)) {
url = url.replace(/(http:\/\/localhost\/cache\/files\/)/, getInstanceURL() + 'ooapi/');
}
config.document.url = url;
if (config.documentType === "spreadsheet") {
config.documentType = "sheet";
}
if (config.documentType === "text") {
config.documentType = "doc";
}
let chan;
let ret = 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'); }
if(['document.url', 'document.fileType', 'documentType',
/*'events.onSave', 'events.onHasUnsavedChanges',
'events.onNewKey', 'events.onInsertImage'*/].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];
});
})) { return; }
cryptpadURL = cryptpadURL.replace(/(\/)+$/, '');
var url = cryptpadURL + '/integration/';
var parsed;
try {
parsed = new URL(url);
} catch (e) {
console.error(e);
return reject('Invalid arg: cryptpadURL');
}
makeIframe = function (config) {
var iframe = document.createElement('iframe');
iframe.setAttribute('id', 'cryptpad-editor');
iframe.setAttribute('name', 'frameEditor');
iframe.setAttribute('align', 'top');
iframe.setAttribute("src", url);
iframe.setAttribute("width", config.width);
iframe.setAttribute("height", config.height);
if (config.editorConfig) { // OnlyOffice
container.replaceWith(iframe);
container = iframe;
} else {
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);
chan = makeChan(iframe, parsed.origin);
start(config, chan).then(resolve).catch(reject);
};
window.addEventListener('message', onMsg);
};
makeIframe(config);
});
});
ret.downloadAs = (arg) => {
if (!chan) {
return void onDocumentReady.push(() => {
ret.downloadAs(arg);
});
}
chan.send('DOWNLOAD_AS', arg);
};
return ret;
};
init.version = () => { return '7.3.0'; };
init.DocEditor = init; // OnlyOffice shim
window.DocsAPI = init;
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();
}
}());