diff --git a/customize.dist/pages/index.js b/customize.dist/pages/index.js index c65ecc93c..a3f03f96f 100644 --- a/customize.dist/pages/index.js +++ b/customize.dist/pages/index.js @@ -176,7 +176,7 @@ define([ h('div.row.cp-home-hero', [ h('div.cp-title.col-lg-6', [ h('img', { - src: '/customize/CryptPad_logo_hero.svg?' + urlArgs, + src: '/api/logo?' + urlArgs, 'aria-hidden': 'true', alt: '' }), diff --git a/customize.dist/pre-loading.js b/customize.dist/pre-loading.js index a5015bae1..8b737922b 100644 --- a/customize.dist/pre-loading.js +++ b/customize.dist/pre-loading.js @@ -5,7 +5,7 @@ (function () { var logoPath = '/customize/CryptPad_logo.svg'; if (location.pathname === '/' || location.pathname === '/index.html') { - logoPath = '/customize/CryptPad_logo_hero.svg'; + logoPath = '/api/logo'; } var elem = document.createElement('div'); diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index a99286b62..398170ba7 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -121,6 +121,11 @@ height: 40px; box-sizing: border-box; } + [type="file"] { // XXX hack, to fix with sidebar layout refactoring + height: auto; + box-sizing: border-box; + padding: 6.5px; + } .cp-sidebarlayout-input-block { display: inline-flex; width: @sidebar_button-width; diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index c491eb0de..1d149bb66 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -17,8 +17,9 @@ const BlockStore = require("../storage/block"); const MFA = require("../storage/mfa"); const ArchiveAccount = require('../archive-account'); const { Worker } = require('node:worker_threads'); +const Fse = require("fs-extra"); -var Fs = require("fs"); +const Fs = require("fs"); var Admin = module.exports; @@ -915,6 +916,52 @@ var deleteInvitation = (Env, Server, cb, data) => { Invitation.delete(Env, id, cb); }; +const MAX_LOGO_SIZE = 200*1024; // 200KB +var uploadLogo = (Env, Server, cb, data, unsafeKey) => { + const args = Array.isArray(data) && data[1]; + if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } + let dataURL = args.dataURL; + + // (size*4/3) + 24 ==> base64 and dataURL overhead + if (!dataURL || dataURL.length > ((MAX_LOGO_SIZE*4/3)+24)) { + return void cb('E_TOO_LARGE'); + } + + let s = dataURL.split(','); + let base64 = s[1]; + let mime = s[0].slice(s[0].indexOf(":")+1, s[0].indexOf(";")); + if (!base64 || !mime) { return void cb('EINVAL'); } + let buf; + try { + buf = Buffer.from(base64, 'base64'); + } catch (e) { + return void cb(e); + } + + nThen(waitFor => { + Fse.mkdirp('customize', {}, waitFor((err) => { + if (!err) { return; } + waitFor.abort(); + return void cb(err); + })); + }).nThen(waitFor => { + Fse.writeFile('./customize/CryptPad_logo_hero.svg', buf, waitFor((err) => { + if (!err) { return; } + waitFor.abort(); + return void cb(err); + })); + }).nThen(() => { + adminDecree(Env, null, function (err) { + if (err) { return void cb(err); } + Env.flushCache(); + cb(void 0, true); + }, ['UPLOAD_LOGO', [ + 'SET_LOGO_MIME', + [mime] + ]], unsafeKey); + }); +}; + var commands = { ACTIVE_SESSIONS: getActiveSessions, ACTIVE_PADS: getActiveChannelCount, @@ -982,6 +1029,8 @@ var commands = { ADD_KNOWN_USER: addKnownUser, DELETE_KNOWN_USER: deleteKnownUser, UPDATE_KNOWN_USER: updateKnownUser, + + UPLOAD_LOGO: uploadLogo, }; // addFirstAdmin is an anon_rpc command diff --git a/lib/decrees.js b/lib/decrees.js index 411b959d2..e8be29696 100644 --- a/lib/decrees.js +++ b/lib/decrees.js @@ -158,10 +158,16 @@ var makeGenericSetter = function (attr, validator) { }; }; +var isString = (str) => { + return str && typeof(str) === "string"; +}; var isInteger = function (n) { return !(typeof(n) !== 'number' || isNaN(n) || (n % 1) !== 0); }; +var args_isString = function (args) { + return !(!Array.isArray(args) || !isString(args[0])); +}; var args_isInteger = function (args) { return !(!Array.isArray(args) || !isInteger(args[0])); }; @@ -174,6 +180,9 @@ var arg_isPositiveInteger = function (args) { return Array.isArray(args) && isInteger(args[0]) && args[0] > 0; }; +// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['LOGO_MIME', ['image/png']]], console.log) +commands.SET_LOGO_MIME = makeGenericSetter('logoMimeType', args_isString); + // CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['ENABLE_PROFILING', [true]]], console.log) commands.ENABLE_PROFILING = makeBooleanSetter('enableProfiling'); diff --git a/lib/http-worker.js b/lib/http-worker.js index 2ddf19849..bcd3a7e98 100644 --- a/lib/http-worker.js +++ b/lib/http-worker.js @@ -506,6 +506,7 @@ app.use("/block", (req, res, next) => { next(); }); + app.use("/customize", Express.static('customize')); app.use("/customize", Express.static('customize.dist')); app.use("/customize.dist", Express.static('customize.dist')); @@ -682,6 +683,17 @@ app.get('/api/profiling', function (req, res) { }); }); +app.get('/api/logo', function (req, res) { + let path = Path.resolve('./customize/CryptPad_logo.svg'); + let base = Path.resolve('./customize.dist/CryptPad_logo.svg'); + Fs.exists(path, function (exists) { + let mime = Env.logoMimeType || 'image/svg+xml'; + res.setHeader('Content-Type', mime + '; charset=utf-8'); + if (exists) { return Fs.createReadStream(path).pipe(res); } + Fs.createReadStream(base).pipe(res); + }); +}); + // This endpoint handles authenticated RPCs over HTTP // via an interactive challenge-response protocol app.use(Express.json()); diff --git a/www/admin/inner.js b/www/admin/inner.js index 7ad80c1c5..8b827e355 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -72,6 +72,9 @@ define([ 'cp-admin-jurisdiction', 'cp-admin-notice', ], + 'customize': [ + 'cp-admin-logo' + ], 'users': [ // Msg.admin_cat_quota 'cp-admin-registration', 'cp-admin-invitation', @@ -3895,6 +3898,54 @@ Example return $div; }; + Messages.admin_logoTitle = "Upload Logo"; + Messages.admin_logoHint = "Max 200KB, svg, png or jpg"; + Messages.admin_logoButton = "Upload"; + create['logo'] = function () { + var key = 'logo'; + var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle + + let $button = $div.find('button'); + + var input = h('input', { + type: 'file', + accept: 'image/*', + 'aria-labelledby': 'cp-admin-logo' + }); + $(h('div', input)).insertBefore($button); + + var spinner = UI.makeSpinner($div); + + Util.onClickEnter($button, function () { + let files = input.files; + if (files.length !== 1) { + UI.warn(Messages.error); + return; + } + spinner.spin(); + $button.attr('disabled', 'disabled'); + let reader = new FileReader(); + reader.onloadend = function () { + let dataURL = this.result; + sframeCommand('UPLOAD_LOGO', {dataURL}, (err, response) => { + $button.removeAttr('disabled'); + if (err) { + UI.warn(Messages.error); + $input.val(''); + console.error(err, response); + spinner.hide(); + return; + } + spinner.done(); + UI.log(Messages.saved); + }); + }; + reader.readAsDataURL(files[0]); + }); + + return $div; + }; + var hideCategories = function () { APP.$rightside.find('> div').hide(); };