diff --git a/.eslintignore b/.eslintignore index e4b7ea216..8a1297856 100644 --- a/.eslintignore +++ b/.eslintignore @@ -16,6 +16,8 @@ www/accounts www/worker www/todo +#lib/plugins/ + www/common/hyperscript.js www/pad/wysiwygarea-plugin.js diff --git a/customize.dist/login.js b/customize.dist/login.js index 870736a33..349e69145 100644 --- a/customize.dist/login.js +++ b/customize.dist/login.js @@ -22,11 +22,13 @@ define([ '/common/outer/login-block.js', '/common/common-hash.js', '/common/outer/http-command.js', + '/api/config', '/components/tweetnacl/nacl-fast.min.js', '/components/scrypt-async/scrypt-async.min.js', // better load speed ], function ($, Listmap, Crypto, Util, NetConfig, Login, Cred, ChainPad, Realtime, Constants, UI, - Feedback, h, LocalStore, Messages, nThen, Block, Hash, ServerCommand) { + Feedback, h, LocalStore, Messages, nThen, Block, Hash, ServerCommand, + ApiConfig) { var Exports = { Cred: Cred, Block: Block, @@ -218,6 +220,11 @@ define([ proxy.edPublic = result.edPublic; } + if (ApiConfig && Array.isArray(ApiConfig.adminKeys) && + ApiConfig.adminKeys.includes(proxy.edPublic)) { + localStorage.CP_admin = "1"; + } + setTimeout(function () { Realtime.whenRealtimeSyncs(result.realtime, function () { proceed(result); diff --git a/customize.dist/messages.js b/customize.dist/messages.js index b644fd936..845a8541b 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -45,6 +45,7 @@ var getLanguage = Messages._getLanguage = function () { (map[l.split('_')[0]] ? l.split('_')[0] : 'en')); }; var language = getLanguage(); +window.cryptpadLanguage = language; // Translations files were migrated from requirejs modules to json. // To avoid asking every administrator to update their customized translation files, diff --git a/customize.dist/pages/index.js b/customize.dist/pages/index.js index 1f2608792..3b919643f 100644 --- a/customize.dist/pages/index.js +++ b/customize.dist/pages/index.js @@ -15,7 +15,8 @@ define([ '/common/outer/local-store.js', '/customize/pages.js', '/common/pad-types.js', -], function ($, Config, h, Hash, Constants, Util, TextFit, Msg, AppConfig, LocalStore, Pages, PadTypes) { + '/common/extensions.js' +], function ($, Config, h, Hash, Constants, Util, TextFit, Msg, AppConfig, LocalStore, Pages, PadTypes, Extensions) { var urlArgs = Config.requireConf.urlArgs; var checkEarlyAccess = function (x) { @@ -164,9 +165,19 @@ define([ }; + let popup = h('div.cp-extensions-popups'); + let utils = { h, Util, Hash }; + Extensions.getExtensions('HOMEPAGE_POPUP').forEach(ext => { + if (typeof(ext.check) === "function" && !ext.check()) { return; } + ext.getContent(utils, content => { + $(popup).append(h('div.cp-extensions-popup', content)); + }); + }); + return [ h('div#cp-main', [ Pages.infopageTopbar(), + popup, notice, h('div.container.cp-container', [ h('div.row.cp-home-hero', [ diff --git a/customize.dist/src/less2/pages/page-index.less b/customize.dist/src/less2/pages/page-index.less index 7873f5f55..859e376b1 100644 --- a/customize.dist/src/less2/pages/page-index.less +++ b/customize.dist/src/less2/pages/page-index.less @@ -297,6 +297,29 @@ } } + .cp-extensions-popups { + width: 100%; + .cp-extensions-popup { + background-color: @cp_alertify-bg; + border-radius: @infopages-radius-L; + padding: 10px; + box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2); + width: 400px; + max-width: 100%; + color: @cryptpad_text_col; + margin-left: 40px; + } + + } + @media (max-width: 700px) { + .cp-extensions-popups { + max-width: 90%; + .cp-extensions-popup { + margin-left: 0; + } + } + } + @media (min-width: 576px) and (max-width: 767px) { .container { padding-left: 0; diff --git a/lib/http-commands.js b/lib/http-commands.js index 54d0474ed..04edd5ff9 100644 --- a/lib/http-commands.js +++ b/lib/http-commands.js @@ -78,15 +78,26 @@ COMMANDS.TOTP_REVOKE = TOTP.TOTP_REVOKE; COMMANDS.TOTP_WRITE_BLOCK = TOTP.TOTP_WRITE_BLOCK; // Password change only for now (v5.5.0) COMMANDS.TOTP_REMOVE_BLOCK = TOTP.TOTP_REMOVE_BLOCK; -try { - // SSO plugin may not be installed - const SSO = plugins.SSO && plugins.SSO.challenge; - COMMANDS.SSO_AUTH = SSO.SSO_AUTH; - COMMANDS.SSO_AUTH_CB = SSO.SSO_AUTH_CB; - COMMANDS.SSO_WRITE_BLOCK = SSO.SSO_WRITE_BLOCK; // Account creation only - COMMANDS.SSO_UPDATE_BLOCK = SSO.SSO_UPDATE_BLOCK; // Password change - COMMANDS.SSO_VALIDATE = SSO.SSO_VALIDATE; -} catch (e) {} +// Load challenges added by plugins +Object.keys(plugins || {}).forEach(id => { + try { + let plugin = plugins[id]; + if (!plugin.challenge) { return; } + let commands = plugin.challenge; + Object.keys(commands).forEach(cmd => { + if (COMMANDS[cmd]) { return; } // Don't overwrite + COMMANDS[cmd] = commands[cmd]; + }); + } catch (e) {} +}); +/* +const SSO = plugins.SSO && plugins.SSO.challenge; +COMMANDS.SSO_AUTH = SSO.SSO_AUTH; +COMMANDS.SSO_AUTH_CB = SSO.SSO_AUTH_CB; +COMMANDS.SSO_WRITE_BLOCK = SSO.SSO_WRITE_BLOCK; // Account creation only +COMMANDS.SSO_UPDATE_BLOCK = SSO.SSO_UPDATE_BLOCK; // Password change +COMMANDS.SSO_VALIDATE = SSO.SSO_VALIDATE; +*/ var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-'); diff --git a/lib/http-worker.js b/lib/http-worker.js index 227122765..b93120d74 100644 --- a/lib/http-worker.js +++ b/lib/http-worker.js @@ -513,6 +513,11 @@ app.use("/block", (req, res, next) => { next(); }); +Object.keys(plugins || {}).forEach(name => { + let plugin = plugins[name]; + if (!plugin.addHttpEndpoints) { return; } + plugin.addHttpEndpoints(Env, app); +}); app.use("/customize", Express.static('customize')); app.use("/customize", Express.static('customize.dist')); @@ -626,6 +631,35 @@ var serveBroadcast = makeRouteCache(function () { app.get('/api/config', serveConfig); app.get('/api/broadcast', serveBroadcast); +(function () { +let extensions = plugins._extensions; +let styles = plugins._styles; +let str = JSON.stringify(extensions); +let str2 = JSON.stringify(styles); +let js = `let extensions = ${str}; +let styles = ${str2}; +let lang = window.cryptpadLanguage; +let paths = []; +extensions.forEach(name => { + paths.push(\`optional!/\${name}/extensions.js\`); + paths.push(\`optional!json!/\${name}/translations/messages.json\`); + paths.push(\`optional!json!/\${name}/translations/messages.\${lang}.json\`); +}); +styles.forEach(name => { + paths.push(\`optional!less!/\${name}/style.less\`); +}); +define(paths, function () { + let args = Array.prototype.slice.apply(arguments); + return args; +}, function () { + // ignore missing files +});`; +app.get('/extensions.js', (req, res) => { + res.setHeader('Content-Type', 'text/javascript'); + res.send(js); +}); +})(); + var Define = function (obj) { return `define(function (){ return ${JSON.stringify(obj, null, '\t')}; @@ -719,7 +753,7 @@ app.post('/api/auth', function (req, res, next) { }); app.use(function (req, res /*, next */) { - if (/^(\/favicon\.ico\/|.*\.js\.map)$/.test(req.url)) { + if (/^(\/favicon\.ico\/|.*\.js\.map|.*\/translations\/.*\.json)/.test(req.url)) { // ignore common 404s } else { Log.info('HTTP_404', req.url); diff --git a/lib/plugin-manager.js b/lib/plugin-manager.js index 8f51120b6..2643bbe0d 100644 --- a/lib/plugin-manager.js +++ b/lib/plugin-manager.js @@ -4,6 +4,8 @@ const fs = require('node:fs'); const plugins = {}; +const extensions = plugins._extensions = []; +const styles = plugins._styles = []; try { let pluginsDir = fs.readdirSync(__dirname + '/plugins'); @@ -12,6 +14,18 @@ try { try { let plugin = require(`./plugins/${name}/index`); plugins[plugin.name] = plugin.modules; + try { + let hasExt = fs.existsSync(`lib/plugins/${name}/client/extensions.js`); + if (hasExt) { + extensions.push(plugin.name.toLowerCase()); + } + } catch (e) {} + try { + let hasStyle = fs.existsSync(`lib/plugins/${name}/client/style.less`); + if (hasStyle) { + styles.push(plugin.name.toLowerCase()); + } + } catch (e) {} } catch (err) { console.error(err); } diff --git a/www/admin/inner.js b/www/admin/inner.js index bbb92d3db..2efa142c7 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -169,7 +169,23 @@ define([ } }; - const blocks = Sidebar.blocks('admin'); + const blocks = sidebar.blocks; + + // EXTENSION_POINT:ADMIN_CATEGORY + common.getExtensions('ADMIN_CATEGORY').forEach(ext => { + if (!ext || !ext.id || !ext.name || !ext.content) { + return console.error('Invalid extension point', 'ADMIN_CATEGORY', ext); + } + if (categories[ext.id]) { + return console.error('Extension point ID already used', ext); + } + console.error(ext); + categories[ext.id] = { + icon: ext.icon, + name: ext.name, + content: ext.content + }; + }); const flushCache = (cb) => { cb = cb || function () {}; @@ -3884,6 +3900,32 @@ define([ cb(opts); }); + // EXTENSION_POINT:ADMIN_ITEM + let utils = { + h, Util, Hash + }; + common.getExtensions('ADMIN_ITEM').forEach(ext => { + if (!ext || !ext.id || typeof(ext.getContent) !== "function") { + return console.error('Invalid extension point', 'ADMIN_CATEGORY', ext); + } + if (sidebar.hasItem(ext.id)) { + return console.error('Extension point ID already used', ext); + } + + sidebar.addItem(ext.id, cb => { + ext.getContent(common, blocks, utils, content => { + cb(content); + }); + }, { + noTitle: !ext.title, + noHint: !ext.description, + title: ext.title, + hint: ext.description + }); + }); + + + sidebar.makeLeftside(categories); }; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index ed69a18db..4525c8287 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -2822,6 +2822,15 @@ define([ initFeedback(data.feedback); } + if (data.edPublic) { + if (Array.isArray(Config.adminKeys) && + Config.adminKeys.includes(data.edPublic)) { + // Doesn't provides extra-rights but may show + // additional warnings in the UI + localStorage.CP_admin = "1"; + } + } + if (data.loggedIn) { window.CP_logged_in = true; } diff --git a/www/common/extensions.js b/www/common/extensions.js new file mode 100644 index 000000000..134c71113 --- /dev/null +++ b/www/common/extensions.js @@ -0,0 +1,58 @@ +define([ + 'optional!/extensions.js' +], (Extensions) => { + const ext = {}; + if (!Array.isArray(Extensions) || !Extensions.length) { return ext; } + + let all = Extensions.slice(); + while(all.length) { + let current = all.splice(0, 3); + + let f = current[0]; + if (typeof(f) !== "function") { + continue; + } + let defaultLang = current[1]; + let lang = current[2]; + if (!Object.keys(lang).length && Object.keys(defaultLang).length) { + // If our language doesn't exists, use default + lang = defaultLang; + } else if (Object.keys(defaultLang).length) { + // Otherwise fill our language with missing keys + Object.keys(defaultLang).forEach(key => { + if (typeof(lang[key]) !== "undefined") { return; } + lang[key] = defaultLang[key]; + }); + } + + lang._getKey = function (key, argArray) { + if (!lang[key]) { return '?'; } + var text = lang[key]; + if (typeof(text) === 'string') { + return text.replace(/\{(\d+)\}/g, function (str, p1) { + if (typeof(argArray[p1]) === 'string' || typeof(argArray[p1]) === "number") { + return argArray[p1]; + } + return ''; + }); + } else { + return text; + } + }; + + let currentExt = f(lang) || {}; + + Object.keys(currentExt).forEach(key => { + ext[key] = ext[key] || []; + Array.prototype.push.apply(ext[key], currentExt[key]); // concat in place + }); + } + + ext.getExtensions = id => { + let e = ext[id]; + if (!Array.isArray(e)) { e = []; } + return e; + }; + + return ext; +}); diff --git a/www/common/inner/sidebar-layout.js b/www/common/inner/sidebar-layout.js index 9df1ac220..82074fb7e 100644 --- a/www/common/inner/sidebar-layout.js +++ b/www/common/inner/sidebar-layout.js @@ -262,9 +262,9 @@ define([ options = options || {}; const title = options.noTitle ? undefined : h('label.cp-item-label', { id: `cp-${app}-${key}` - }, Messages[`${app}_${safeKey}Title`] || key); + }, options.title || Messages[`${app}_${safeKey}Title`] || key); const hint = options.noHint ? undefined : h('span.cp-sidebarlayout-description', - Messages[`${app}_${safeKey}Hint`] || 'Coming soon...'); + options.hint || Messages[`${app}_${safeKey}Hint`] || 'Coming soon...'); if (hint && options.htmlHint) { hint.innerHTML = Messages[`${app}_${safeKey}Hint`]; } @@ -281,6 +281,10 @@ define([ }); }; + sidebar.hasItem = key => { + return !key || !!items[key]; + }; + sidebar.addCheckboxItem = (data) => { const key = data.key; let blocks = sidebar.blocks; @@ -335,7 +339,7 @@ define([ 'data-category': key }, [ icon, - Messages[`${app}_cat_${key}`] || key, + category.name || Messages[`${app}_cat_${key}`] || key, ]); var $item = $(item).appendTo(container); Util.onClickEnter($item, function () { diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 5aab870ac..4facd8156 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -107,9 +107,11 @@ define([ var myOOId; var sessionId = Hash.createChannelId(); var cpNfInner; + let integrationChannel; var evOnPatch = Util.mkEvent(); var evOnSync = Util.mkEvent(); + var evIntegrationSave = Util.mkEvent(); // This structure is used for caching media data and blob urls for each media cryptpad url var mediasData = {}; @@ -313,7 +315,10 @@ define([ isCp: cp } }, function (err, h) { - if (!err) { evOnSync.fire(); } + if (!err) { + evOnSync.fire(); + evIntegrationSave.fire(); + } cb(err, h); }); }, @@ -1423,6 +1428,9 @@ define([ debug(obj, 'toOO'); chan.event('CMD', obj); + if (obj && obj.type === "saveChanges") { + evIntegrationSave.fire(); + } }; chan.on('CMD', function (obj) { @@ -1844,6 +1852,11 @@ define([ } delete APP.oldCursor; } + if (integrationChannel) { + APP.onDocumentUnlock = () => { + integrationChannel.event('EV_INTEGRATION_READY'); + }; + } } delete APP.startNew; @@ -2580,6 +2593,22 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null }); }; + sframeChan.on('EV_INTEGRATION_DOWNLOADAS', function (format) { + console.error('DOWNLOAD AS RECEIVED'); + var data = getContent(); + x2tConvertData(data, "document.bin", format, function (xlsData) { + UI.removeModals(); + if (xlsData) { + var blob = new Blob([xlsData], {type: "application/bin;charset=utf-8"}); + if (integrationChannel) { + integrationChannel.event('EV_INTEGRATION_ON_DOWNLOADAS', + blob, { raw: true }); + } + return; + } + UI.warn(Messages.error); + }); + }); sframeChan.on('EV_OOIFRAME_REFRESH', function (data) { // We want to get the "bin" content of a sheet from its json in order to download // something useful from a non-onlyoffice app (download from drive or settings). @@ -3132,6 +3161,73 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null UI.removeLoadingScreen(); }; + let convertImportBlob = (blob, title) => { + new Response(blob).arrayBuffer().then(function (buffer) { + var u8Xlsx = new Uint8Array(buffer); + x2tImportData(u8Xlsx, title, 'bin', function (bin) { + if (!bin) { + return void UI.errorLoadingScreen(Messages.error); + } + var blob = new Blob([bin], {type: 'text/plain'}); + var file = getFileType(); + resetData(blob, file); + //saveToServer(blob, title); + Title.updateTitle(title); + UI.removeLoadingScreen(); + }); + }); + }; + + if (privateData.integration) { + let cfg = privateData.integrationConfig || {}; + common.openIntegrationChannel(APP.onLocal); + integrationChannel = common.getSframeChannel(); + var integrationSave = function (cb) { + var ext = cfg.fileType; + + var upload = Util.once(function (_blob) { + integrationChannel.query('Q_INTEGRATION_SAVE', { + blob: _blob + }, cb, { + raw: true + }); + }); + + var data = getContent(); + x2tConvertData(data, "document.bin", ext, function (xlsData) { + UI.removeModals(); + if (xlsData) { + var blob = new Blob([xlsData], {type: "application/bin;charset=utf-8"}); + upload(blob); + return; + } + UI.warn(Messages.error); + }); + }; + const integrationHasUnsavedChanges = function(unsavedChanges, cb) { + integrationChannel.query('Q_INTEGRATION_HAS_UNSAVED_CHANGES', unsavedChanges, cb); + }; + var inte = common.createIntegration(integrationSave, + integrationHasUnsavedChanges); + if (inte) { + evIntegrationSave.reg(function () { + inte.changed(); + }); + } + integrationChannel.on('Q_INTEGRATION_NEEDSAVE', function (data, cb) { + integrationSave(function (obj) { + if (obj && obj.error) { console.error(obj.error); } + cb(); + }); + }); + if (privateData.initialState) { + var blob = privateData.initialState; + let title = `document.${cfg.fileType}`; + console.error(blob, title); + return convertImportBlob(blob, title); + } + } + if (privateData.isNewFile && privateData.fromFileData) { try { (function () { diff --git a/www/common/onlyoffice/main.js b/www/common/onlyoffice/main.js index 9da5f22e8..7cc95f70e 100644 --- a/www/common/onlyoffice/main.js +++ b/www/common/onlyoffice/main.js @@ -11,12 +11,15 @@ define([ '/common/sframe-common-outer.js' ], function (nThen, ApiConfig, DomReady, Hash, SFCommonO) { + var isIntegration = Boolean(window.CP_integration_outer); + var integration = window.CP_integration_outer || {}; + // Loaded in load #2 var hash, href, version; 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; var parsed = Hash.parsePadUrl(href); @@ -24,9 +27,14 @@ define([ var opts = parsed.getOptions(); version = opts.versionHash; } + if (isIntegration) { + href = integration.href; + hash = integration.hash; + } }).nThen(function (/*waitFor*/) { var addData = function (obj) { - obj.ooType = window.location.pathname.replace(/^\//, '').replace(/\/$/, ''); + let path = (integration && integration.pathname) || window.location.pathname; + obj.ooType = path.replace(/^\//, '').replace(/\/$/, ''); obj.ooVersionHash = version; obj.ooForceVersion = localStorage.CryptPad_ooVersion || ""; }; @@ -154,18 +162,21 @@ define([ Utils.initUnsafeIframe(obj, cb); }); - - }; SFCommonO.start({ hash: hash, href: href, type: 'oo', - useCreationScreen: true, addData: addData, addRpc: addRpc, getPropChannels: getPropChannels, - messaging: true + messaging: true, + useCreationScreen: !isIntegration, + noDrive: true, + integration: isIntegration, + integrationUtils: integration.utils, + integrationConfig: integration.config || {}, + initialState: integration.initialState || undefined }); }); }); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index e5b19ca80..029a15f74 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -3014,6 +3014,8 @@ define([ // Make sure we have a valid user object before emitting cacheready if (rt.proxy && !rt.proxy.drive) { return; } + returned.edPublic = rt.proxy.edPublic; + onCacheReady(clientId, function () { if (typeof(cb) === "function") { cb(returned); } onCacheReadyEvt.fire(); @@ -3047,6 +3049,8 @@ define([ drive[Constants.oldStorageKey] = []; } */ + + returned.edPublic = rt.proxy.edPublic; // Drive already exist: return the existing drive, don't load data from legacy store if (store.manager) { // If a cache is loading, make sure it is complete before calling onReady @@ -3156,6 +3160,7 @@ define([ loadUniversal(Cursor, 'cursor', function () {}); loadUniversal(Integration, 'integration', function () {}); loadUniversal(Messenger, 'messenger', function () {}); + loadOnlyOffice(); store.messenger = store.modules['messenger']; // And now we're ready diff --git a/www/common/outer/x2t.js b/www/common/outer/x2t.js index c65cbca3f..a814302c0 100644 --- a/www/common/outer/x2t.js +++ b/www/common/outer/x2t.js @@ -12,7 +12,7 @@ define([ var CURRENT_VERSION = X2T.CURRENT_VERSION = CurrentVersion.currentVersion; var debug = function (str) { - if (localStorage.CryptPad_dev !== "1") { return; } + //if (localStorage.CryptPad_dev !== "1") { return; } console.debug(str); }; diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 81cd6ccfc..3f886725c 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -648,8 +648,8 @@ define([ const integrationHasUnsavedChanges = function(unsavedChanges, cb) { integrationChannel.query('Q_INTEGRATION_HAS_UNSAVED_CHANGES', unsavedChanges, cb); }; - var inte = common.createIntegration(onLocal, cpNfInner.chainpad, - integrationSave, integrationHasUnsavedChanges); + var inte = common.createIntegration(integrationSave, + integrationHasUnsavedChanges); if (inte) { integration = true; evIntegrationSave.reg(function () { diff --git a/www/common/sframe-common-integration.js b/www/common/sframe-common-integration.js index 46af8fa2c..8b1ab5374 100644 --- a/www/common/sframe-common-integration.js +++ b/www/common/sframe-common-integration.js @@ -9,8 +9,6 @@ define([ module.create = function ( Common, - onLocal, - chainpad, saveHandler, unsavedChangesHandler) { diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 272c9349d..6c46ced04 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -2077,6 +2077,16 @@ define([ cfg.integrationUtils.save(obj, cb); } }); + sframeChan.on('EV_INTEGRATION_READY', function () { + if (cfg.integrationUtils && cfg.integrationUtils.onReady) { + cfg.integrationUtils.onReady(); + } + }); + sframeChan.on('EV_INTEGRATION_ON_DOWNLOADAS', function (obj) { + if (cfg.integrationUtils && cfg.integrationUtils.onDownloadAs) { + cfg.integrationUtils.onDownloadAs(obj); + } + }); sframeChan.on('Q_INTEGRATION_HAS_UNSAVED_CHANGES', function (obj, cb) { if (cfg.integrationUtils && cfg.integrationUtils.onHasUnsavedChanges) { cfg.integrationUtils.onHasUnsavedChanges(obj, cb); @@ -2090,6 +2100,15 @@ define([ integrationSave = function (cb) { sframeChan.query('Q_INTEGRATION_NEEDSAVE', null, cb); }; + + if (cfg.integrationUtils) { + if (cfg.integrationUtils.setDownloadAs) { + cfg.integrationUtils.setDownloadAs(format => { + sframeChan.event('EV_INTEGRATION_DOWNLOADAS', format); + }); + } + } + } if (cfg.messaging) { diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 625a0dfe6..c297b5273 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -34,6 +34,7 @@ define([ '/common/common-constants.js', '/components/localforage/dist/localforage.min.js', '/common/hyperscript.js', + '/common/extensions.js' ], function ( $, ApiConfig, @@ -64,7 +65,8 @@ define([ Language, Constants, localForage, - h + h, + Ext ) { // Chainpad Netflux Inner var funcs = {}; @@ -775,6 +777,8 @@ define([ return Util.checkRestrictedApp(app, AppConfig, ea, priv.plan, priv.loggedIn); }; + funcs.getExtensions = Ext.getExtensions; + funcs.mailbox = {}; Object.freeze(funcs); diff --git a/www/cryptpad-api.js b/www/cryptpad-api.js index 783353917..e73ef40bd 100644 --- a/www/cryptpad-api.js +++ b/www/cryptpad-api.js @@ -14,6 +14,13 @@ 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 = {}; @@ -75,6 +82,7 @@ var makeIframe = function () {}; // placeholder + let onDocumentReady = []; var start = function (config, chan) { return new Promise(function (resolve, reject) { setTimeout(function () { @@ -83,7 +91,8 @@ var getBlob = function (cb) { var xhr = new XMLHttpRequest(); - xhr.open('GET', config.document.url, true); + let url = config.document.url; + xhr.open('GET', url, true); xhr.responseType = 'blob'; xhr.onload = function () { if (this.status === 200) { @@ -131,9 +140,18 @@ var getSession = function (cb) { chan.send('GET_SESSION', { - key: key + 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 @@ -153,21 +171,52 @@ }; 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; - document.getElementById('cryptpad-editor').remove(); + if (!config.editorConfig) { // Not OnlyOffice shim + document.getElementById('cryptpad-editor').remove(); + } makeIframe(config); }); chan.on('HAS_UNSAVED_CHANGES', function(unsavedChanges, cb) { - config.events.onHasUnsavedChanges(unsavedChanges); + if (config.events.onHasUnsavedChanges) { + config.events.onHasUnsavedChanges(unsavedChanges); + } cb(); }); chan.on('ON_INSERT_IMAGE', function(data, cb) { - config.events.onInsertImage(data, cb); + if (config.events.onIntertImage) { + config.events.onInsertImage(data, cb); + } else { cb(); } }); }); @@ -192,7 +241,28 @@ * @return {promise} */ var init = function (cryptpadURL, containerId, config) { - return new Promise(function (resolve, reject) { + // 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") { @@ -209,8 +279,8 @@ 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) { + /*'events.onSave', 'events.onHasUnsavedChanges', + 'events.onNewKey', 'events.onInsertImage'*/].some(function (k) { var s = k.split('.'); var c = config; return s.some(function (key) { @@ -235,14 +305,23 @@ 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); - container.appendChild(iframe); + 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); - var chan = makeChan(iframe, parsed.origin); + chan = makeChan(iframe, parsed.origin); start(config, chan).then(resolve).catch(reject); }; window.addEventListener('message', onMsg); @@ -250,8 +329,24 @@ 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; }; diff --git a/www/install/main.js b/www/install/main.js index b7735a321..d86ecedb5 100644 --- a/www/install/main.js +++ b/www/install/main.js @@ -174,6 +174,7 @@ define([ edPublic: proxy.edPublic }, function (e) { if (e) { UI.alert(Messages.error); return console.error(e); } + localStorage.CP_admin = "1"; startOnboarding(data.network, proxy); }); }); diff --git a/www/integration/main.js b/www/integration/main.js index f0876d275..fe3ffd010 100644 --- a/www/integration/main.js +++ b/www/integration/main.js @@ -96,6 +96,13 @@ define([ http.send(); }; chan.on('GET_SESSION', function (data, cb) { + if (data.keepOld) { + var key = data.key + "000000000000000000000000000000000"; + console.warn('KEY', key); + return void cb({ + key: `/2/integration/edit/${key.slice(0,24)}/` + }); + } var getHash = function () { //isNew = true; return Hash.createRandomHash('integration'); @@ -126,6 +133,24 @@ define([ var onInsertImage = function (data, cb) { chan.send('ON_INSERT_IMAGE', data, cb); }; + var onReady = function () { + chan.send('DOCUMENT_READY', {}); + }; + + let downloadAs; + chan.on('DOWNLOAD_AS', function (format) { + if (typeof(downloadAs) !== "function") { + console.error('UNSUPPORTED COMMAND', 'downloadAs'); + return; + } + downloadAs(format); + }); + let setDownloadAs = f => { + downloadAs = f; + }; + let onDownloadAs = function (blob) { // DownloadAs callback + chan.send('ON_DOWNLOADAS', blob); + }; chan.on('START', function (data) { console.warn('INNER START', data); @@ -141,13 +166,20 @@ define([ autosave: data.autosave }, utils: { + onReady: onReady, + onDownloadAs, + setDownloadAs, save: save, reload: reload, onHasUnsavedChanges: onHasUnsavedChanges, onInsertImage: onInsertImage } }; - require(['/common/sframe-app-outer.js'], function () { + 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; }); diff --git a/www/lib/optional/optional.js b/www/lib/optional/optional.js index 00767379f..c42ec2835 100644 --- a/www/lib/optional/optional.js +++ b/www/lib/optional/optional.js @@ -13,7 +13,7 @@ define("optional", [], { var onLoadFailure = function(err){ // optional module failed to load. var failedId = err.requireModules && err.requireModules[0]; - console.warn("Could not load optional module: " + failedId); + //console.warn("Could not load optional module: " + failedId); // Undefine the module to cleanup internal stuff in requireJS requirejs.undef(failedId); diff --git a/www/web-apps/apps/api/documents/api.js b/www/web-apps/apps/api/documents/api.js new file mode 120000 index 000000000..c1d2442f2 --- /dev/null +++ b/www/web-apps/apps/api/documents/api.js @@ -0,0 +1 @@ +../../../../cryptpad-api.js \ No newline at end of file