Merge pull request #1797 from cryptpad/ooapi-dl

Integration API
This commit is contained in:
yflory 2025-03-12 17:57:40 +01:00 committed by GitHub
commit d55d360eb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 341 additions and 84 deletions

View File

@ -38,11 +38,10 @@ var getStoredLanguage = function () { return localStorage && localStorage.getIte
var getBrowserLanguage = function () { return navigator.language || navigator.userLanguage || ''; };
var getLanguage = Messages._getLanguage = function () {
if (window.cryptpadLanguage) { return window.cryptpadLanguage; }
try {
if (getStoredLanguage()) { return getStoredLanguage(); }
} catch (e) { console.log(e); }
var l = getBrowserLanguage();
// Edge returns 'fr-FR' --> transform it to 'fr' and check again
try {
l = getStoredLanguage() || getBrowserLanguage();
} catch (e) { console.log(e); }
return map[l] ? l :
(map[l.split('-')[0]] ? l.split('-')[0] :
(map[l.split('_')[0]] ? l.split('_')[0] : 'en'));

View File

@ -67,7 +67,7 @@ module.exports.create = function (Env, cb) {
}
if (metadata && metadata.selfdestruct && metadata.selfdestruct !== Env.id) {
HK.expireChannel(Env, channelName);
HK.removeChannel(Env, channelName);
return void cb('ESELFDESTRUCT');
}

View File

@ -136,6 +136,13 @@ const expireChannel = HK.expireChannel = function (Env, channel) {
});
};
const removeChannel = HK.removeChannel = function (Env, channel) {
if (!Env.store) { return; }
Env.store.archiveChannel(channel, void 0, () => {});
delete Env.metadata_cache[channel];
delete Env.channel_cache[channel];
};
/* dropChannel
* cleans up memory structures which are managed entirely by the historyKeeper
*/
@ -145,7 +152,7 @@ const dropChannel = HK.dropChannel = function (Env, chanName) {
delete Env.channel_cache[chanName];
if (meta && meta.selfdestruct && Env.selfDestructTo) {
Env.selfDestructTo[chanName] = setTimeout(function () {
expireChannel(Env, chanName);
removeChannel(Env, chanName);
}, TEMPORARY_CHANNEL_LIFETIME);
}
if (Env.store) { Env.store.closeChannel(chanName, function () {}); }
@ -698,6 +705,7 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) {
}, (err, reason) => {
// Any error but ENOENT: abort
// ENOENT is allowed in case we want to create a new pad
if (err && err.error) { err = err.error; }
if (err && err.code !== 'ENOENT') {
if (err.message === "EUNKNOWN") {
Log.error("HK_GET_HISTORY", {
@ -713,7 +721,7 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) {
stack: err && err.stack,
}); }
// FIXME err.message isn't useful for users
const parsedMsg = {error:err.message, channel: channelName, txid: txid};
const parsedMsg = {error:err.message || 'ERROR', channel: channelName, txid: txid};
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(parsedMsg)]);
return;
}

9
package-lock.json generated
View File

@ -18,7 +18,7 @@
"chainpad": "^5.2.6",
"chainpad-crypto": "^0.2.5",
"chainpad-listmap": "^1.1.0",
"chainpad-netflux": "^1.2.0",
"chainpad-netflux": "^1.2.2",
"chainpad-server": "^5.2.4",
"ckeditor": "npm:ckeditor4@~4.22.1",
"codemirror": "^5.19.0",
@ -1443,9 +1443,10 @@
}
},
"node_modules/chainpad-netflux": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/chainpad-netflux/-/chainpad-netflux-1.2.0.tgz",
"integrity": "sha512-j3qzrL/tugpTNQTk1I7VMZuQJHAYRNmFaiAxLbEFre/gLIPPJqM4gHXuyTU2OFzIFSd5m5i4G3+GwZ32jLV3cA==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/chainpad-netflux/-/chainpad-netflux-1.2.2.tgz",
"integrity": "sha512-fcKugW29BE4wo4l3WYLc56yeFznu15bGi6tU3uPtsLGSExXGqNwS+3kmpzI0AFAsuoI0OKPeT4uLX4YzKT/D+A==",
"license": "LGPL-2.1",
"dependencies": {
"netflux-websocket": "^1.2.0"
}

View File

@ -21,7 +21,7 @@
"chainpad": "^5.2.6",
"chainpad-crypto": "^0.2.5",
"chainpad-listmap": "^1.1.0",
"chainpad-netflux": "^1.2.0",
"chainpad-netflux": "^1.2.2",
"chainpad-server": "^5.2.4",
"ckeditor": "npm:ckeditor4@~4.22.1",
"codemirror": "^5.19.0",

View File

@ -398,6 +398,10 @@ define([
};
var onUploaded = function (ev, data, err) {
if (!ev && err) {
console.error(err);
return void UI.warn(Messages.error);
}
if (ev.newTemplate) {
if (err) {
console.error(err);
@ -555,7 +559,7 @@ define([
var saveToServer = function (blob, title) {
if (APP.cantCheckpoint) { return; } // TOO_LARGE
var text = getContent();
var text = !blob && getContent();
if (!text && !blob) {
setEditable(false, true);
sframeChan.query('Q_CLEAR_CACHE_CHANNELS', [
@ -949,7 +953,9 @@ define([
const realParticipants = Object.entries(content.ids).map(([id, user]) => {
const nId = id.slice(0,32);
const username = (users[nId] || {}).name || Messages.anonymous;
const username = Util.find(privateData, ['integrationConfig', 'user', 'name']) ||
(users[nId] || {}).name || Messages.anonymous;
return {
id: String(user.ooid) + user.index,
idOriginal: String(user.ooid),
@ -1469,6 +1475,20 @@ define([
});
}
break;
case "forceSaveStart":
if (APP.integrationSave) {
APP.integrationSave(obj => {
if (obj?.error) {
console.error(obj.error);
return void UI.warn(Messages.error);
}
content.integrationSave = `${myUniqueOOId}-${+new Date()}`;
APP.integrationSaved = content.integrationSave;
APP.onLocal();
UI.log(Messages.saved);
});
}
break;
case "getLock":
handleLock(obj, send);
break;
@ -1677,16 +1697,24 @@ define([
var lang = (window.cryptpadLanguage || navigator.language || navigator.userLanguage || '').slice(0,2);
let username = Util.find(privateData, ['integrationConfig', 'user', 'name'])
|| metadataMgr.getUserData().name
|| Messages.anonymous;
let integrationConfig = privateData?.integrationConfig?._;
//let ec = integrationConfig?.editorConfig;
let dc = integrationConfig?.document;
// Config
APP.ooconfig = {
"document": {
"fileType": file.type,
"key": "fresh",
"title": file.title,
"url": url,
"permissions": {
"download": false,
"print": true,
document: {
fileType: file.type,
key: "fresh",
title: dc?.title || file.title,
url: url,
permissions: {
download: dc?.permissions?.download || false,
print: dc?.permissions?.print || true,
}
},
"documentType": file.doc,
@ -1703,8 +1731,8 @@ define([
},
"user": {
"id": String(myOOId), //"c0c3bf82-20d7-4663-bf6d-7fa39c598b1d",
"firstname": metadataMgr.getUserData().name || Messages.anonymous,
"name": metadataMgr.getUserData().name || Messages.anonymous,
"firstname": username,
"name": username
},
"mode": "edit",
"lang": lang
@ -2135,6 +2163,30 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
}, void 0, common.getCache());
};
let copy = (a, b) => {
Object.keys(b).forEach(k => {
if (k === "user") { return; } // Don't change user values
if (a[k]) {
if (typeof(a[k]) === "object" && typeof(b[k]) === "object") {
copy(a[k], b[k]);
}
return;
}
a[k] = b[k];
});
};
if (integrationConfig) {
let ec = integrationConfig.editorConfig;
let c = APP.ooconfig.editorConfig.customization;
copy(APP.ooconfig.editorConfig, ec);
// Open "goback" in new tabs because of csp and
// iframes
if (ec.editorConfig?.customization?.goback) {
c.goback.blank = true;
}
c.forcesave = true;
}
// Always hide right menu
localStorage?.original.removeItem('sse-hide-right-settings');
localStorage?.original.removeItem('de-hide-right-settings');
@ -3180,9 +3232,9 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
return void UI.errorLoadingScreen(Messages.error);
}
var blob = new Blob([bin], {type: 'text/plain'});
var file = getFileType();
resetData(blob, file);
//saveToServer(blob, title);
//var file = getFileType();
//resetData(blob, file);
saveToServer(blob, title);
Title.updateTitle(title);
UI.removeLoadingScreen();
});
@ -3193,13 +3245,17 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
let cfg = privateData.integrationConfig || {};
common.openIntegrationChannel(APP.onLocal);
integrationChannel = common.getSframeChannel();
let hasUnsavedChanges = false;
var integrationSave = function (cb) {
var ext = cfg.fileType;
var upload = Util.once(function (_blob) {
integrationChannel.query('Q_INTEGRATION_SAVE', {
blob: _blob
}, cb, {
}, obj => {
if (!obj?.error) { hasUnsavedChanges = false; }
cb(obj);
}, {
raw: true
});
});
@ -3220,21 +3276,46 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
};
var inte = common.createIntegration(integrationSave,
integrationHasUnsavedChanges);
if (inte) {
if (inte && cfg.autosave) {
evIntegrationSave.reg(function () {
inte.changed();
});
} else {
APP.integrationSave = integrationSave;
APP.integrationSetSaved = () => {
hasUnsavedChanges = false;
};
evIntegrationSave.reg(function () {
hasUnsavedChanges = true;
});
}
$(window).on('beforeunload', function (ev) {
if (hasUnsavedChanges) { return false; }
ev.returnValue = '';
});
integrationChannel.on('Q_INTEGRATION_NEEDSAVE', function (data, cb) {
if (!cfg.autosave) { return; }
integrationSave(function (obj) {
if (obj && obj.error) { console.error(obj.error); }
cb();
});
});
if (privateData.initialState) {
/* test button
if (!cfg.autosave) {
let $save = common.createButton('save', true, {}, function () {
$save.attr('disabled', 'disabled');
integrationSave(() => {
$save.removeAttr('disabled');
});
});
$('body').prepend($save);
}
*/
if (privateData.initialState && (!content || !content.hashes || !Object.keys(content.hashes).length)) {
var blob = privateData.initialState;
let title = `document.${cfg.fileType}`;
console.error(blob, title);
return convertImportBlob(blob, title);
}
}
@ -3369,6 +3450,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
var wasMigrating = content.migration;
var myLocks = getUserLock(getId(), true);
//var integrationSave = content.integrationSave;
content = json.content;
@ -3379,6 +3461,15 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
checkCheckpoint();
}
// Integration: mark the current content as saved
// if manually saved by someone else
if (content.integrationSave !== APP.integrationSaved) {
APP.integrationSaved = content.integrationSave;
if (APP.integrationSetSaved) {
APP.integrationSetSaved();
}
}
var editor = getEditor();
if (content.hashes) {
var latest = getLastCp(true);

View File

@ -467,6 +467,19 @@ define([
});
};
var initTempRpc = (clientId, cb) => {
if (store.rpc) { return void cb(store.rpc); }
var kp = Crypto.Nacl.sign.keyPair();
var keys = {
edPublic: Crypto.Nacl.util.encodeBase64(kp.publicKey),
edPrivate: Crypto.Nacl.util.encodeBase64(kp.secretKey)
};
Pinpad.create(store.network, keys, function (e, call) {
if (e) { return void cb({error: e}); }
store.rpc = call;
cb(call);
});
};
var initRpc = function (clientId, data, cb) {
if (!store.loggedIn) { return cb(); }
if (store.rpc) { return void cb(account); }
@ -1830,7 +1843,7 @@ define([
Store.leavePad(null, data, function () {});
};
var conf = {
Cache: Cache, // ICE pad cache
Cache: store.neverCache ? undefined : Cache,
onCacheStart: function () {
postMessage(clientId, "PAD_CACHE");
},
@ -3160,7 +3173,7 @@ define([
// If we load CryptPad for the first time from an existing pad, don't create a
// drive automatically.
var onNoDrive = function (clientId, cb) {
var onNoDrive = function (clientId, cb, initRpc) {
var andThen = function () {
// To be able to use all the features inside the pad, we need to
// initialize the chat (messenger) and the cursor modules.
@ -3171,9 +3184,16 @@ define([
store.messenger = store.modules['messenger'];
// And now we're ready
initAnonRpc(null, null, function () {
cb({});
});
let getAnon = () => {
initAnonRpc(null, null, function () {
cb({});
});
};
if (initRpc) {
return initTempRpc(clientId, getAnon);
}
getAnon();
};
// We need an anonymous RPC to be able to check if the pad exists and to get
@ -3256,7 +3276,8 @@ define([
// First tab, no user hash, no anon hash and this app doesn't need a drive
// ==> don't create a drive
// Or "neverDrive" (integration into another platform?)
// ==> don't create a drive
// ==> don't create a drive BUT create temp RPC (we may need to upload)
if (data.neverDrive) { store.neverCache = true; }
if (data.neverDrive || (data.noDrive && !data.userHash && !data.anonHash)) {
return void onNoDrive(clientId, function (obj) {
if (obj && obj.error) {
@ -3270,7 +3291,7 @@ define([
}
Feedback.send("NO_DRIVE", true);
callback(obj);
});
}, !!data.neverDrive);
}
initialized = true;

View File

@ -17,7 +17,9 @@ define([
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
var obj = SFCommonO.initIframe(waitFor, true, integration.pathname);
let lang = integration && integration.config && integration.config.editorConfig
&& integration.config.editorConfig.lang;
var obj = SFCommonO.initIframe(waitFor, true, integration.pathname, lang);
href = obj.href;
hash = obj.hash;
if (isIntegration) {

View File

@ -842,6 +842,13 @@ define([
additionalPriv.initialState = cfg.initialState instanceof Blob ?
cfg.initialState : undefined;
if (cfg.integrationConfig) {
if (metaObj?.user && !metaObj.user.name) {
metaObj.user.name = cfg.integrationConfig?.user?.name ||
cfg.integrationConfig?.user?.firstname;
}
}
// Early access
var priv = metaObj.priv;
var _plan = typeof(priv.plan) === "undefined" ? Utils.LocalStore.getPremium() : priv.plan;
@ -2242,7 +2249,7 @@ define([
}
};
// on server crash, try to save to Nextcloud
// on server crash, try to save to the outer platform
if (ready) { return integrationSave(reload); }
// if error during loading, reload without saving

View File

@ -18,7 +18,9 @@
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);
var match2 = scripts[i].src.match(/(.*)\/cryptpad-api.js/i);
if (match) { return match[1]; }
else if (match2) { return match2[1]; }
}
};
@ -86,6 +88,7 @@
var start = function (config, chan) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
var docID = config.document.key;
var key = config.document.key;
var blob;
@ -109,14 +112,31 @@
xhr.send();
};
let serializedConfig = () => {
let _config = {};
_config.editorConfig = config.editorConfig;
_config.document = {
permissions: config.document?.permissions,
title: config.document?.title,
info: config.document?.info,
referenceData: config.document?.referenceData
};
return _config;
};
var start = function () {
config.document.key = key;
//config.document.key = key;
chan.send('START', {
key: key,
application: config.documentType,
name: config.document.title,
url: config.document.url,
documentKey: docID,
document: blob,
ext: config.document.fileType,
autosave: config.autosave || 10
autosave: config.events.onSave && (config.autosave || 10),
editorConfig: config.editorConfig || {},
_config: serializedConfig()
}, function (obj) {
if (obj && obj.error) { reject(obj.error); return console.error(obj.error); }
resolve({});
@ -130,8 +150,17 @@
blob = config.document.blob;
return start();
}
// NOTE: Nextcloud will log us out if we try from the client
// TODO: make sure the server plugin is installed if we don't
// call getBlob()
if (!config.events?.onSave) {
return void start();
}
getBlob(function (err, _blob) {
if (err) { reject(err); return console.error(err); }
if (err) { // Can't get blob from client, try from server
console.warn(err);
return void start();
}
_blob.name = `document.${config.document.fileType}`;
blob = _blob;
start();
@ -188,6 +217,7 @@
chan.on('ON_DOWNLOADAS', blob => {
let url = URL.createObjectURL(blob);
if (!config.events.onDownloadAs) { return; }
config.events.onDownloadAs({
data: {
fileType: config.document && config.document.fileType,
@ -198,6 +228,7 @@
chan.on('SAVE', function (data, cb) {
blob = data;
if (!config.events.onSave) { return void cb(); }
config.events.onSave(data, cb);
});
chan.on('RELOAD', function () {
@ -242,22 +273,28 @@
*/
var init = function (cryptpadURL, containerId, config) {
// OnlyOffice shim: don't provide a URL
if (!config && typeof(containerId) === "object") {
if (typeof(config) !== "object" && typeof(containerId) === "object") {
config = containerId;
containerId = cryptpadURL;
cryptpadURL = getInstanceURL();
}
config.events = config.events || {};
// 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") {
if (config.documentType === "spreadsheet" || config.documentType === "cell") {
config.documentType = "sheet";
}
if (config.documentType === "text") {
if (config.documentType === "slide") {
config.documentType = "presentation";
}
if (config.documentType === "word" || config.documentType === "text") {
config.documentType = "doc";
}
@ -308,8 +345,8 @@
iframe.setAttribute('name', 'frameEditor');
iframe.setAttribute('align', 'top');
iframe.setAttribute("src", url);
iframe.setAttribute("width", config.width);
iframe.setAttribute("height", config.height);
iframe.setAttribute("width", config.width || '100%');
iframe.setAttribute("height", config.height || '100%');
if (config.editorConfig) { // OnlyOffice
container.replaceWith(iframe);
container = iframe;

View File

@ -3,10 +3,13 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
define([
'/api/config',
'/common/sframe-common-outer.js',
'/common/common-hash.js',
], function (SCO, Hash) {
'/components/tweetnacl/nacl-fast.min.js'
], function (Config, SCO, Hash) {
let Nacl = window.nacl;
var getTxid = function () {
return Math.random().toString(16).replace('0.', '');
};
@ -95,9 +98,17 @@ define([
};
http.send();
};
let sanitizeKey = key => {
try {
Nacl.util.decodeBase64(key);
return key;
} catch (e) {
return Nacl.util.encodeBase64(Nacl.util.decodeUTF8(key)).replaceAll('=', '');
}
};
chan.on('GET_SESSION', function (data, cb) {
if (data.keepOld) {
var key = data.key + "000000000000000000000000000000000";
if (data.keepOld) { // they provide their own key, we must turn it into a hash
var key = sanitizeKey(data.key) + "000000000000000000000000000000000";
console.warn('KEY', key);
return void cb({
key: `/2/integration/edit/${key.slice(0,24)}/`
@ -118,12 +129,6 @@ define([
});
});
var save = function (obj, cb) {
chan.send('SAVE', obj.blob, function (err) {
if (err) { return cb({error: err}); }
cb();
});
};
var reload = function (data) {
chan.send('RELOAD', data);
};
@ -152,36 +157,122 @@ define([
chan.send('ON_DOWNLOADAS', blob);
};
chan.on('START', function (data) {
console.warn('INNER START', data);
var href = Hash.hashToHref(data.key, data.application);
console.error(Hash.hrefToHexChannelId(href));
window.CP_integration_outer = {
pathname: `/${data.application}/`,
hash: data.key,
href: href,
initialState: data.document,
config: {
fileType: data.ext,
autosave: data.autosave
},
utils: {
onReady: onReady,
onDownloadAs,
setDownloadAs,
save: save,
reload: reload,
onHasUnsavedChanges: onHasUnsavedChanges,
onInsertImage: onInsertImage
let getInstanceURL = function () {
return Config.httpUnsafeOrigin;
};
let getBlobServer = function (documentURL, cb) {
let xhr = new XMLHttpRequest();
let data = encodeURIComponent(documentURL);
let url = getInstanceURL() + '/ooapidl?url=' + data;
xhr.open('GET', url, true);
xhr.responseType = 'blob';
//xhr.setRequestHeader('Content-Type', 'application/json');
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);
}
};
let path = "/common/sframe-app-outer.js";
if (['sheet', 'doc', 'presentation'].includes(data.application)) {
path = '/common/onlyoffice/main.js';
xhr.onerror = function (e) {
cb(e.message);
};
xhr.send();
};
let saveBlobServer = function (cfg, blob, cb) {
let {callbackUrl, name, key} = cfg;
let xhr = new XMLHttpRequest();
name = encodeURIComponent(name);
callbackUrl = encodeURIComponent(callbackUrl);
key = encodeURIComponent(key);
let query = `name=${name}&cb=${callbackUrl}&key=${key}`;
let url = getInstanceURL() + `/oosave?${query}`;
xhr.open('POST', url, true);
xhr.responseType = 'blob';
//xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
console.error(this.status);
if (this.status === 200) {
cb();
} else {
cb(this.status);
}
};
xhr.onerror = function (e) {
cb(e.message);
};
xhr.send(blob);
};
chan.on('START', function (data, cb) {
console.warn('INNER START', data);
// data.key is a hash
var href = Hash.hashToHref(data.key, data.application);
if (data.editorConfig.lang) {
var LS_LANG = "CRYPTPAD_LANG";
localStorage.setItem(LS_LANG, data.editorConfig.lang);
}
require([path], function () {
console.warn('SAO REQUIRED');
delete window.CP_integration_outer;
let fileName = data.name || `document.${data.ext}`;
var save = function (obj, cb) {
let cbUrl = data.editorConfig.callbackUrl;
if (!data.autosave && cbUrl) {
saveBlobServer({
callbackUrl: cbUrl,
name: fileName,
key: data.documentKey
}, obj.blob, cb);
return;
}
chan.send('SAVE', obj.blob, function (err) {
if (err) { return cb({error: err}); }
cb();
});
};
console.error(Hash.hrefToHexChannelId(href));
let startApp = function (blob) {
window.CP_integration_outer = {
pathname: `/${data.application}/`,
hash: data.key,
href: href,
initialState: blob,
config: {
fileName: data.name,
fileType: data.ext,
autosave: data.autosave,
user: data.editorConfig.user,
_: data._config
},
utils: {
onReady: onReady,
onDownloadAs,
setDownloadAs,
save: save,
reload: reload,
onHasUnsavedChanges: onHasUnsavedChanges,
onInsertImage: onInsertImage
}
};
let path = "/common/sframe-app-outer.js";
if (['sheet', 'doc', 'presentation'].includes(data.application)) {
path = '/common/onlyoffice/main.js';
}
require([path], function () {
console.warn('SAO REQUIRED');
delete window.CP_integration_outer;
cb();
});
};
if (data.document) { return void startApp(data.document); }
getBlobServer(data.url, (err, blob) => {
if (err) {
return void cb({error: err});
}
startApp(blob);
});
});