From 5081d5d3c05a5de5223cb4d11c6508018c8e53f0 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 19 Jan 2023 16:10:55 +0100 Subject: [PATCH] Integration API: initialize an app from a Blob --- www/common/common-ui-elements.js | 32 +++++++++++------ www/common/sframe-app-framework.js | 58 +++++++++++++++++------------- www/common/sframe-app-outer.js | 3 +- www/common/sframe-common-outer.js | 28 +++++++++++++-- www/common/sframe-common.js | 3 ++ www/cryptpad-api.js | 36 ++++++++++++++++--- www/integration/main.js | 40 +++++++-------------- 7 files changed, 129 insertions(+), 71 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 1a574cc4c..4127fb568 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -128,14 +128,8 @@ define([ }; var importContent = UIElements.importContent = function (type, f, cfg) { - return function () { - var $files = $('', {type:"file"}); - if (cfg && cfg.accept) { - $files.attr('accept', cfg.accept); - } - $files.click(); - $files.on('change', function (e) { - var file = e.target.files[0]; + return function (_file) { + var todo = function (file) { var reader = new FileReader(); var parsed = file && file.name && /.+\.([^.]+)$/.exec(file.name); var ext = parsed && parsed[1]; @@ -144,7 +138,19 @@ define([ reader.readAsArrayBuffer(file, type); } else { reader.readAsText(file, type); - } + } + }; + + if (_file) { return void todo(_file); } + + var $files = $('', {type:"file"}); + if (cfg && cfg.accept) { + $files.attr('accept', cfg.accept); + } + $files.click(); + $files.on('change', function (e) { + var file = e.target.files[0]; + todo(file); }); }; }; @@ -627,12 +633,16 @@ define([ }); var handler = data.first? function () { - data.first(importer); + data.first(function () { + importer(); // Make sure we don't pass arguments to importer + }); }: importer; //importContent; button .click(common.prepareFeedback(type)) - .click(handler); + .click(function () { + handler(); + }); //} break; case 'upload': diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 04821ce15..98f66463d 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -71,6 +71,7 @@ define([ var evStart = Util.mkEvent(true); var mediaTagEmbedder; + var fileImporter; var $embedButton; var common; @@ -545,7 +546,8 @@ define([ contentUpdate(newContent, waitFor); } } else { - if (!cpNfInner.metadataMgr.getPrivateData().isNewFile) { + var priv = cpNfInner.metadataMgr.getPrivateData(); + if (!priv.isNewFile) { // We're getting 'new pad' but there is an existing file // We don't know exactly why this can happen but under no circumstances // should we overwrite the content, so lets just try again. @@ -558,11 +560,16 @@ define([ onCorruptedCache(); return; } + if (priv.initialState) { + var blob = priv.initialState; + var file = new File([blob], blob.name); + UIElements.importContent('text/plain', fileImporter, {})(file); + } title.updateTitle(title.defaultTitle); evOnDefaultContentNeeded.fire(); } }).nThen(function () { - // We have a valid chainpad, reenable cache fix in case with reconnect with + // We have a valid chainpad, reenable cache fix in case we reconnect with // a corrupted cache noCache = false; @@ -698,31 +705,32 @@ define([ var setFileImporter = function (options, fi, async) { if (readOnly) { return; } - toolbar.$drawer.append( - common.createButton('import', true, options, function (c, f) { - if (state !== STATE.READY || unsyncMode) { - return void UI.warn(Messages.disconnected); - } - if (async) { - fi(c, f, function (content) { - nThen(function (waitFor) { - contentUpdate(content, waitFor); - }).nThen(function () { - onLocal(); - }); + fileImporter = function (c, f) { + if (state !== STATE.READY || unsyncMode) { + return void UI.warn(Messages.disconnected); + } + if (async) { + fi(c, f, function (content) { + nThen(function (waitFor) { + contentUpdate(content, waitFor); + }).nThen(function () { + onLocal(); }); - return; - } - nThen(function (waitFor) { - var content = fi(c, f); - if (typeof(content) === "undefined") { - return void UI.warn(Messages.importError); - } - contentUpdate(content, waitFor); - }).nThen(function () { - onLocal(); }); - }) + return; + } + nThen(function (waitFor) { + var content = fi(c, f); + if (typeof(content) === "undefined") { + return void UI.warn(Messages.importError); + } + contentUpdate(content, waitFor); + }).nThen(function () { + onLocal(); + }); + }; + toolbar.$drawer.append( + common.createButton('import', true, options, fileImporter) ); }; diff --git a/www/common/sframe-app-outer.js b/www/common/sframe-app-outer.js index 38e21f108..c04a99237 100644 --- a/www/common/sframe-app-outer.js +++ b/www/common/sframe-app-outer.js @@ -28,7 +28,8 @@ define([ href: href, useCreationScreen: !isIntegration, messaging: true, - integration: isIntegration + integration: isIntegration, + initialState: integration.initialState || undefined }); }); }); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 54ae897ba..ca05bfdfb 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -353,6 +353,13 @@ define([ delete sessionStorage.CP_formExportSheet; } + // New integrated pad + if (cfg.initialState) { + currentPad.href = cfg.href; + currentPad.hash = cfg.hash; + return void todo(); + } + // New pad options var options = parsed.getOptions(); if (options.newPadOpts) { @@ -697,7 +704,6 @@ define([ burnAfterReading: burnAfterReading, storeInTeam: Cryptpad.initialTeam || (Cryptpad.initialPath ? -1 : undefined), supportsWasm: Utils.Util.supportsWasm(), - integration: cfg.integration }; if (window.CryptPad_newSharedFolder) { additionalPriv.newSharedFolder = window.CryptPad_newSharedFolder; @@ -718,6 +724,12 @@ define([ additionalPriv.isChannelMuted = true; } + // Integration + additionalPriv.integration = cfg.integration; + additionalPriv.initialState = cfg.initialState instanceof Blob ? + cfg.initialState : undefined; + + // Early access var priv = metaObj.priv; var _plan = typeof(priv.plan) === "undefined" ? Utils.LocalStore.getPremium() : priv.plan; var p = Utils.Util.checkRestrictedApp(parsed.type, AppConfig, @@ -729,6 +741,7 @@ define([ additionalPriv.earlyAccessBlocked = true; } + // Safe apps if (isSafe) { additionalPriv.hashes = hashes; additionalPriv.password = password; @@ -744,7 +757,7 @@ define([ Utils.LocalStore.setPremium(metaObj.priv.plan); } - sframeChan.event('EV_METADATA_UPDATE', metaObj); + sframeChan.event('EV_METADATA_UPDATE', metaObj, {raw: true}); }); }; Cryptpad.onMetadataChanged(updateMeta); @@ -1994,6 +2007,15 @@ define([ }); }); } + + // Make sure we add the validateKey to channel metadata when we don't use + // the pad creation screen + if (!rtConfig.metadata && secret.keys.validateKey) { + rtConfig.metadata = { + validateKey: secret.keys.validateKey + }; + } + var cpNfCfg = { sframeChan: sframeChan, channel: secret.channel, @@ -2021,6 +2043,8 @@ define([ Cryptpad.getMetadata(waitFor(function (err, m) { cpNfCfg.owners = [m.priv.edPublic]; })); + } else if (isNewFile && !cfg.useCreationScreen && cfg.initialState) { + console.log('new file with initial state provided'); } else if (isNewFile && !cfg.useCreationScreen && currentPad.hash) { console.log("new file with hash in the address bar in an app without pcs and which requires owners"); sframeChan.onReady(function () { diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 87054a887..f1705d694 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -426,6 +426,9 @@ define([ funcs.handleNewFile = function (waitFor, config) { if (window.__CRYPTPAD_TEST__) { return; } var priv = ctx.metadataMgr.getPrivateData(); + if (priv.isNewFile && priv.initialState) { + return void setTimeout(waitFor()); + } if (priv.isNewFile) { var c = (priv.settings.general && priv.settings.general.creation) || {}; // If this is a new file but we have a hash in the URL and pad creation screen is diff --git a/www/cryptpad-api.js b/www/cryptpad-api.js index 840311567..7e70a4c0f 100644 --- a/www/cryptpad-api.js +++ b/www/cryptpad-api.js @@ -78,11 +78,30 @@ config.events.onSave(data); }); - var onKeyValidated = function () { + var getBlob = function (cb) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', config.document.url, true); + xhr.responseType = 'blob'; + xhr.onload = function(e) { + 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 (blob) { chan.send('START', { key: key, application: config.documentType, - document: config.document.url, + document: blob, }, function (obj) { if (obj && obj.error) { reject(obj.error); return console.error(obj.error); } console.log('OUTER START SUCCESS'); @@ -90,6 +109,14 @@ }); }; + var onKeyValidated = function () { + getBlob(function (err, blob) { + if (err) { reject(err); return console.error(err); } + blob.name = `document.${config.document.fileType}`; + start(blob); + }); + }; + chan.send('GET_SESSION', { key: key }, function (obj) { @@ -113,6 +140,7 @@ * @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 The save function to store the document when edited. @@ -137,7 +165,7 @@ } if (!config) { return reject('Missing args: no data provided'); } - ['document.url', 'document.key', 'documentType', + if(['document.url', 'document.fileType', 'document.key', 'documentType', 'events.onSave', 'events.onNewKey'].some(function (k) { var s = k.split('.'); var c = config; @@ -148,7 +176,7 @@ } c = c[key]; }); - }); + })) { return; } cryptpadURL = cryptpadURL.replace(/(\/)+$/, ''); var url = cryptpadURL + '/integration/'; diff --git a/www/integration/main.js b/www/integration/main.js index 9775809b4..ce7aceb1f 100644 --- a/www/integration/main.js +++ b/www/integration/main.js @@ -99,7 +99,7 @@ define([ isNew = true; return Hash.createRandomHash('integration'); }; - var oldKey = data.sessionKey; + var oldKey = data.key; if (!oldKey) { return void cb({ key: getHash() }); } checkSession(oldKey, function (obj) { @@ -112,33 +112,17 @@ define([ 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.key); - Crypt.put(data.key, content, w(), { - metadata: { - selfdestruct: true - } - }); - }).nThen(function () { - 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 - }; - require(['/common/sframe-app-outer.js'], function () { - console.warn('SAO REQUIRED'); - delete window.CP_integration_outer; - }); + 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: isNew ? data.document : undefined + }; + require(['/common/sframe-app-outer.js'], function () { + console.warn('SAO REQUIRED'); + delete window.CP_integration_outer; }); });