Remains of the sandboxed CKEditor - will be removed, but the version here may be useful for another editor that is prone to XSS (like Summernote).
250 lines
8.8 KiB
JavaScript
250 lines
8.8 KiB
JavaScript
'use strict';
|
|
|
|
const config = require('config');
|
|
const path = require('path');
|
|
const express = require('express');
|
|
const routerFactory = require('../lib/router-async');
|
|
const passport = require('../lib/passport');
|
|
const clientHelpers = require('../lib/client-helpers');
|
|
const gm = require('gm').subClass({
|
|
imageMagick: true
|
|
});
|
|
const users = require('../models/users');
|
|
|
|
const bluebird = require('bluebird');
|
|
const fsReadFile = bluebird.promisify(require('fs').readFile);
|
|
|
|
const files = require('../models/files');
|
|
const fileHelpers = require('../lib/file-helpers');
|
|
|
|
const templates = require('../models/templates');
|
|
const mosaicoTemplates = require('../models/mosaico-templates');
|
|
|
|
const contextHelpers = require('../lib/context-helpers');
|
|
const interoperableErrors = require('../shared/interoperable-errors');
|
|
|
|
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
|
|
const { base } = require('../shared/templates');
|
|
const { AppType } = require('../shared/app');
|
|
|
|
const {castToInteger} = require('../lib/helpers');
|
|
|
|
|
|
users.registerRestrictedAccessTokenMethod('mosaico', async ({entityTypeId, entityId}) => {
|
|
if (entityTypeId === 'template') {
|
|
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
|
|
|
|
if (tmpl.type === 'mosaico') {
|
|
return {
|
|
permissions: {
|
|
'template': {
|
|
[entityId]: new Set(['manageFiles', 'view'])
|
|
},
|
|
'mosaicoTemplate': {
|
|
[tmpl.data.mosaicoTemplate]: new Set(['view'])
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
async function placeholderImage(width, height) {
|
|
const magick = gm(width, height, '#707070');
|
|
const streamAsync = bluebird.promisify(magick.stream.bind(magick));
|
|
|
|
const size = 40;
|
|
let x = 0;
|
|
let y = 0;
|
|
|
|
// stripes
|
|
while (y < height) {
|
|
magick
|
|
.fill('#808080')
|
|
.drawPolygon([x, y], [x + size, y], [x + size * 2, y + size], [x + size * 2, y + size * 2])
|
|
.drawPolygon([x, y + size], [x + size, y + size * 2], [x, y + size * 2]);
|
|
x = x + size * 2;
|
|
if (x > width) {
|
|
x = 0;
|
|
y = y + size * 2;
|
|
}
|
|
}
|
|
|
|
// text
|
|
magick
|
|
.fill('#B0B0B0')
|
|
.fontSize(20)
|
|
.drawText(0, 0, width + ' x ' + height, 'center');
|
|
|
|
const stream = await streamAsync('png');
|
|
|
|
return {
|
|
format: 'png',
|
|
stream
|
|
};
|
|
}
|
|
|
|
async function resizedImage(filePath, method, width, height) {
|
|
const magick = gm(filePath);
|
|
const streamAsync = bluebird.promisify(magick.stream.bind(magick));
|
|
const formatAsync = bluebird.promisify(magick.format.bind(magick));
|
|
|
|
const format = (await formatAsync()).toLowerCase();
|
|
|
|
if (method === 'resize') {
|
|
magick
|
|
.autoOrient()
|
|
.resize(width, height);
|
|
} else if (method === 'cover') {
|
|
magick
|
|
.autoOrient()
|
|
.resize(width, height + '^')
|
|
.gravity('Center')
|
|
.extent(width, height + '>');
|
|
} else {
|
|
throw new Error(`Method ${method} not supported`);
|
|
}
|
|
|
|
const stream = await streamAsync();
|
|
|
|
return {
|
|
format,
|
|
stream
|
|
};
|
|
}
|
|
|
|
function sanitizeSize(val, min, max, defaultVal, allowNull) {
|
|
if (val === 'null' && allowNull) {
|
|
return null;
|
|
}
|
|
val = Number(val) || defaultVal;
|
|
val = Math.max(min, val);
|
|
val = Math.min(max, val);
|
|
return val;
|
|
}
|
|
|
|
|
|
|
|
function getRouter(appType) {
|
|
const router = routerFactory.create();
|
|
|
|
if (appType === AppType.SANDBOXED) {
|
|
router.getAsync('/templates/:mosaicoTemplateId/index.html', passport.loggedIn, async (req, res) => {
|
|
const tmpl = await mosaicoTemplates.getById(req.context, castToInteger(req.params.mosaicoTemplateId));
|
|
|
|
res.set('Content-Type', 'text/html');
|
|
res.send(base(tmpl.data.html, getTrustedUrl(), getSandboxUrl('', req.context), getPublicUrl()));
|
|
});
|
|
|
|
// Mosaico looks for block thumbnails in edres folder relative to index.html of the template. We respond to such requests here.
|
|
router.getAsync('/templates/:mosaicoTemplateId/edres/:fileName', async (req, res, next) => {
|
|
try {
|
|
const file = await files.getFileByOriginalName(contextHelpers.getAdminContext(), 'mosaicoTemplate', 'block', castToInteger(req.params.mosaicoTemplateId), req.params.fileName);
|
|
res.type(file.mimetype);
|
|
return res.download(file.path, file.name);
|
|
} catch (err) {
|
|
if (err instanceof interoperableErrors.NotFoundError) {
|
|
next();
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
});
|
|
|
|
// This is a fallback to versafix-1 if the block thumbnail is not defined by the template
|
|
router.use('/templates/:mosaicoTemplateId/edres', express.static(path.join(__dirname, '..', 'client', 'static', 'mosaico', 'templates', 'versafix-1', 'edres')));
|
|
|
|
|
|
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file');
|
|
|
|
router.getAsync('/upload/:type/:entityId', passport.loggedIn, async (req, res) => {
|
|
const id = castToInteger(req.params.entityId);
|
|
|
|
const entries = await files.list(req.context, req.params.type, 'file', id);
|
|
|
|
const filesOut = [];
|
|
for (const entry of entries) {
|
|
filesOut.push({
|
|
name: entry.originalname,
|
|
url: files.getFileUrl(req.context, req.params.type, 'file', id, entry.filename),
|
|
size: entry.size,
|
|
thumbnailUrl: files.getFileUrl(req.context, req.params.type, 'file', id, entry.filename) // TODO - use smaller thumbnails
|
|
})
|
|
}
|
|
|
|
res.json({
|
|
files: filesOut
|
|
});
|
|
});
|
|
|
|
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
|
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
|
|
|
|
let languageStrings = null;
|
|
if (config.language && config.language !== 'en') {
|
|
const lang = config.language.split('_')[0];
|
|
try {
|
|
const file = path.join(__dirname, '..', 'client', 'static', 'mosaico', 'lang', 'mosaico-' + lang + '.json');
|
|
languageStrings = await fsReadFile(file, 'utf8');
|
|
} catch (err) {
|
|
}
|
|
}
|
|
|
|
res.render('mosaico/root', {
|
|
layout: 'mosaico/layout',
|
|
editorConfig: config.mosaico,
|
|
languageStrings: languageStrings,
|
|
reactCsrfToken: req.csrfToken(),
|
|
mailtrainConfig: JSON.stringify(mailtrainConfig),
|
|
scriptFiles: [
|
|
getSandboxUrl('mailtrain/common.js'),
|
|
getSandboxUrl('mailtrain/mosaico.js')
|
|
],
|
|
publicPath: getSandboxUrl()
|
|
});
|
|
});
|
|
|
|
} else if (appType === AppType.TRUSTED || appType === AppType.PUBLIC) { // Mosaico editor loads the images from TRUSTED endpoint. This is hard to change because the index.html has to come from TRUSTED.
|
|
// So we serve /mosaico/img under both endpoints. There is no harm in it.
|
|
router.getAsync('/img', async (req, res) => {
|
|
const method = req.query.method;
|
|
const params = req.query.params;
|
|
let [width, height] = params.split(',');
|
|
let image;
|
|
|
|
|
|
// FIXME - cache the generated files !!!
|
|
|
|
if (method === 'placeholder') {
|
|
width = sanitizeSize(width, 1, 2048, 600, false);
|
|
height = sanitizeSize(height, 1, 2048, 300, false);
|
|
image = await placeholderImage(width, height);
|
|
|
|
} else {
|
|
width = sanitizeSize(width, 1, 2048, 600, false);
|
|
height = sanitizeSize(height, 1, 2048, 300, true);
|
|
|
|
let filePath;
|
|
const url = req.query.src;
|
|
|
|
const mosaicoLegacyUrlPrefix = getTrustedUrl(`mosaico/uploads/`);
|
|
if (url.startsWith(mosaicoLegacyUrlPrefix)) {
|
|
filePath = path.join(__dirname, '..', 'client', 'static' , 'mosaico', 'uploads', url.substring(mosaicoLegacyUrlPrefix.length));
|
|
} else {
|
|
const file = await files.getFileByUrl(contextHelpers.getAdminContext(), url);
|
|
filePath = file.path;
|
|
}
|
|
|
|
image = await resizedImage(filePath, method, width, height);
|
|
}
|
|
|
|
res.set('Content-Type', 'image/' + image.format);
|
|
image.stream.pipe(res);
|
|
});
|
|
}
|
|
|
|
return router;
|
|
}
|
|
|
|
module.exports.getRouter = getRouter;
|