Refactored and linted editorapi image handling

This commit is contained in:
witzig 2017-06-20 18:57:35 +02:00
parent ac63e934ec
commit 3c4558d70c
5 changed files with 347 additions and 304 deletions

View file

@ -5,7 +5,7 @@ module.exports = function (grunt) {
// Project configuration. // Project configuration.
grunt.initConfig({ grunt.initConfig({
eslint: { eslint: {
all: ['lib/**/*.js', 'test/**/*.js', 'config/**/*.js', 'Gruntfile.js', 'app.js', 'index.js'] all: ['lib/**/*.js', 'test/**/*.js', 'config/**/*.js', 'Gruntfile.js', 'app.js', 'index.js', 'routes/editorapi.js']
}, },
nodeunit: { nodeunit: {

View file

@ -69,7 +69,6 @@
"express-session": "^1.15.2", "express-session": "^1.15.2",
"faker": "^4.1.0", "faker": "^4.1.0",
"feedparser": "^2.1.0", "feedparser": "^2.1.0",
"file-type": "^5.2.0",
"fs-extra": "^3.0.1", "fs-extra": "^3.0.1",
"geoip-ultralight": "^0.1.5", "geoip-ultralight": "^0.1.5",
"gettext-parser": "^1.2.2", "gettext-parser": "^1.2.2",

View file

@ -1,38 +1,34 @@
'use strict'; 'use strict';
let log = require('npmlog'); const log = require('npmlog');
let config = require('config'); const config = require('config');
let express = require('express'); const express = require('express');
let router = new express.Router(); const router = new express.Router();
let passport = require('../lib/passport'); const passport = require('../lib/passport');
let os = require('os'); const os = require('os');
let fs = require('fs'); const fs = require('fs');
let path = require('path'); const path = require('path');
let mkdirp = require('mkdirp'); const mkdirp = require('mkdirp');
let cache = require('memory-cache'); const crypto = require('crypto');
let crypto = require('crypto'); const events = require('events');
let fetch = require('node-fetch'); const httpMocks = require('node-mocks-http');
let events = require('events'); const multiparty = require('multiparty');
let httpMocks = require('node-mocks-http'); const escapeStringRegexp = require('escape-string-regexp');
let multiparty = require('multiparty'); const jqueryFileUpload = require('jquery-file-upload-middleware');
let fileType = require('file-type'); const gm = require('gm').subClass({
let escapeStringRegexp = require('escape-string-regexp');
let jqueryFileUpload = require('jquery-file-upload-middleware');
let gm = require('gm').subClass({
imageMagick: true imageMagick: true
}); });
let url = require('url'); const url = require('url');
let htmlToText = require('html-to-text'); const htmlToText = require('html-to-text');
let premailerApi = require('premailer-api'); const premailerApi = require('premailer-api');
let editorHelpers = require('../lib/editor-helpers'); const _ = require('../lib/translate')._;
let _ = require('../lib/translate')._; const mailer = require('../lib/mailer');
let mailer = require('../lib/mailer'); const settings = require('../lib/models/settings');
let settings = require('../lib/models/settings'); const templates = require('../lib/models/templates');
let templates = require('../lib/models/templates'); const campaigns = require('../lib/models/campaigns');
let campaigns = require('../lib/models/campaigns');
router.all('/*', (req, res, next) => { router.all('/*', (req, res, next) => {
if (!req.user && !cache.get(req.get('If-Match'))) { if (!req.user) {
return res.status(403).send(_('Need to be logged in to access restricted content')); return res.status(403).send(_('Need to be logged in to access restricted content'));
} }
if (req.originalUrl.startsWith('/editorapi/img?')) { if (req.originalUrl.startsWith('/editorapi/img?')) {
@ -48,30 +44,150 @@ jqueryFileUpload.on('begin', fileInfo => {
fileInfo.name = fileInfo.name fileInfo.name = fileInfo.name
.toLowerCase() .toLowerCase()
.replace(/ /g, '-') .replace(/ /g, '-')
.replace(/[^a-z0-9+-\.]+/g, ''); .replace(/[^a-z0-9+-.]+/g, '');
}); });
let listImages = (dir, dirURL, callback) => { const listImages = (dir, dirURL, callback) => {
fs.readdir(dir, (err, files = []) => { fs.readdir(dir, (err, files = []) => {
if (err && err.code !== 'ENOENT') { if (err && err.code !== 'ENOENT') {
return callback(err.message || err); return callback(err.message || err);
} }
files = files.filter(name => /\.(jpe?g|png|gif)$/i.test(name)); files = files.filter(name => /\.(jpe?g|png|gif)$/i.test(name));
files = files.map(name => { files = files.map(name => ({
return {
// mosaico // mosaico
name, name,
url: dirURL + '/' + name, url: dirURL + '/' + name,
thumbnailUrl: dirURL + '/thumbnail/' + name, thumbnailUrl: dirURL + '/thumbnail/' + name,
// grapejs // grapejs
src: dirURL + '/' + name, src: dirURL + '/' + name
}; }));
});
callback(null, files); callback(null, files);
}); });
}; };
const placeholderImage = (width, height, callback) => {
const magick = gm(width, height, '#707070');
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');
magick.stream('png', (err, stream) => {
if (err) {
return callback(err);
}
const image = {
format: 'PNG',
stream
};
callback(null, image);
});
};
const resizedImage = (src, method, width, height, callback) => {
const pathname = path.join('/', url.parse(src).pathname);
const filePath = path.join(__dirname, '..', 'public', pathname);
const magick = gm(filePath);
magick.format((err, format) => {
if (err) {
return callback(err);
}
const streamHandler = (err, stream) => {
if (err) {
return callback(err);
}
const image = {
format,
stream
};
callback(null, image);
};
switch (method) {
case 'resize':
return magick
.autoOrient()
.resize(width, height)
.stream(streamHandler);
case 'cover':
return magick
.autoOrient()
.resize(width, height + '^')
.gravity('Center')
.extent(width, height + '>')
.stream(streamHandler);
default:
return callback(new Error(_('Method not supported')));
}
});
};
const getProcessedImage = (dynamicUrl, callback) => {
if (!dynamicUrl.includes('/editorapi/img?')) {
return callback(new Error('Invalid dynamicUrl'));
}
const {
src,
method,
params = '600,null'
} = url.parse(dynamicUrl, true).query;
let width = params.split(',')[0];
let height = params.split(',')[1];
const 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;
};
if (method === 'placeholder') {
width = sanitizeSize(width, 1, 2048, 600, false);
height = sanitizeSize(height, 1, 2048, 300, false);
placeholderImage(width, height, callback);
} else {
width = sanitizeSize(width, 1, 2048, 600, false);
height = sanitizeSize(height, 1, 2048, 300, true);
resizedImage(src, method, width, height, callback);
}
};
const getStaticImageUrl = (dynamicUrl, staticDir, staticDirUrl, callback) => { const getStaticImageUrl = (dynamicUrl, staticDir, staticDirUrl, callback) => {
if (!dynamicUrl.includes('/editorapi/img?')) {
return callback(null, dynamicUrl);
}
mkdirp(staticDir, err => { mkdirp(staticDir, err => {
if (err) { if (err) {
return callback(err); return callback(err);
@ -82,242 +198,126 @@ const getStaticImageUrl = (dynamicUrl, staticDir, staticDirUrl, callback) => {
return callback(err); return callback(err);
} }
let hash = crypto.createHash('md5').update(dynamicUrl).digest('hex'); const hash = crypto.createHash('md5').update(dynamicUrl).digest('hex');
let match = files.find(el => el.startsWith(hash)); const match = files.find(el => el.startsWith(hash));
let headers = {};
if (match) { if (match) {
return callback(null, staticDirUrl + '/' + match); return callback(null, staticDirUrl + '/' + match);
} }
if (dynamicUrl.includes('/editorapi/img?')) { getProcessedImage(dynamicUrl, (err, image) => {
let token = crypto.randomBytes(16).toString('hex'); if (err) {
cache.put(token, true, 1000); return callback(err);
headers['If-Match'] = token;
} }
fetch(dynamicUrl, { const fileName = hash + '.' + image.format.toLowerCase();
headers const filePath = path.join(staticDir, fileName);
}) const fileUrl = staticDirUrl + '/' + fileName;
.then(res => {
if (res.status < 200 || res.status >= 300) { const writeStream = fs.createWriteStream(filePath);
throw new Error(`Received HTTP status code ${res.status} while fetching image ${dynamicUrl}`); writeStream.on('error', err => callback(err));
} writeStream.on('finish', () => callback(null, fileUrl));
return res.buffer(); image.stream.pipe(writeStream);
})
.then(buffer => {
let ft = fileType(buffer);
if (ft && ['image/jpeg', 'image/png', 'image/gif'].includes(ft.mime)) {
fs.writeFile(path.join(staticDir, hash + '.' + ft.ext), buffer, err => {
if (err) {
throw err;
}
let staticUrl = staticDirUrl + '/' + hash + '.' + ft.ext;
callback(null, staticUrl);
});
} else {
throw new Error(`Unsupported image MIME type for ${dynamicUrl}`);
}
})
.catch(err => {
callback(err);
}); });
}); });
}); });
}; };
let prepareHtml = ({ const prepareHtml = (html, editorName, callback) => {
editorName,
html
}, callback) => {
settings.get('serviceUrl', (err, serviceUrl) => { settings.get('serviceUrl', (err, serviceUrl) => {
if (err) { if (err) {
return callback(err.message || err); return callback(err.message || err);
} }
const srcs = new Map();
const re = /<img[^>]+src="([^"]*\/editorapi\/img\?[^"]+)"/ig;
let jobs = 0; let jobs = 0;
let srcs = {};
let re = /<img[^>]+src="([^"]+)"/g;
let result; let result;
while ((result = re.exec(html)) !== null) { while ((result = re.exec(html)) !== null) {
srcs[result[1]] = result[1]; srcs.set(result[1], result[1]);
} }
let done = () => { const done = () => {
if (jobs === 0) { if (jobs === 0) {
Object.keys(srcs).forEach(src => { for (const [key, value] of srcs) {
// console.log(`replace dynamic - ${src} - with static - ${srcs[src]}`); // console.log(`replace dynamicUrl: ${key} - with staticUrl: ${value}`);
html = html.replace(new RegExp(escapeStringRegexp(src), 'g'), srcs[src]); html = html.replace(new RegExp(escapeStringRegexp(key), 'g'), value);
}); }
callback(null, html); return callback(null, html);
} }
}; };
const staticDir = path.join(__dirname, '..', 'public', editorName, 'uploads', 'static'); const staticDir = path.join(__dirname, '..', 'public', editorName, 'uploads', 'static');
const staticDirUrl = url.resolve(serviceUrl, editorName + '/uploads/static'); const staticDirUrl = url.resolve(serviceUrl, editorName + '/uploads/static');
Object.keys(srcs).forEach(src => { for (const key of srcs.keys()) {
jobs++; jobs++;
let dynamicUrl = src.replace(/&amp;/g, '&'); const dynamicUrl = key.replace(/&amp;/g, '&');
dynamicUrl = /^https?:\/\/|^\/\//i.test(dynamicUrl) ? dynamicUrl : url.resolve(serviceUrl, dynamicUrl);
getStaticImageUrl(dynamicUrl, staticDir, staticDirUrl, (err, staticUrl) => { getStaticImageUrl(dynamicUrl, staticDir, staticDirUrl, (err, staticUrl) => {
if (err) { if (err) {
// TODO: Send a warning back to the editor. For now we just skip image resizing. // TODO: Send a warning back to the editor. For now we just skip image resizing.
log.error('editorapi', err.message || err); log.error('editorapi', err);
if (dynamicUrl.includes('/editorapi/img?')) { if (dynamicUrl.includes('/editorapi/img?')) {
staticUrl = url.parse(dynamicUrl, true).query.src || dynamicUrl; staticUrl = url.parse(dynamicUrl, true).query.src || dynamicUrl;
} else { } else {
staticUrl = dynamicUrl; staticUrl = dynamicUrl;
} }
if (!/^https?:\/\/|^\/\//i.test(staticUrl)) {
staticUrl = url.resolve(serviceUrl, staticUrl);
}
} }
srcs[src] = staticUrl; srcs.set(key, staticUrl);
jobs--; jobs--;
done(); done();
}); });
}); }
done(); done();
}); });
}; };
let placeholderImage = (req, res, { // URL structure defined by Mosaico
width,
height
}) => {
let magick = gm(width, height, '#707070');
let x = 0;
let y = 0;
let size = 40;
// stripes
while (y < height) {
magick = 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 = magick
.fill('#B0B0B0')
.fontSize(20)
.drawText(0, 0, width + ' x ' + height, 'center');
res.set('Content-Type', 'image/png');
magick.stream('png').pipe(res);
};
let resizedImage = (req, res, {
src,
method,
width,
height
}) => {
let magick = gm(src);
magick.format((err, format) => {
if (err) {
return res.status(500).send(err.message || err);
}
switch (method) {
case 'resize':
res.set('Content-Type', 'image/' + format.toLowerCase());
magick.autoOrient()
.resize(width, height)
.stream()
.pipe(res);
return;
case 'cover':
res.set('Content-Type', 'image/' + format.toLowerCase());
magick.autoOrient()
.resize(width, height + '^')
.gravity('Center')
.extent(width, height + '>')
.stream()
.pipe(res);
return;
default:
res.status(501).send(_('Method not supported'));
}
});
};
// /editorapi/img?src=" + encodeURIComponent(src) + "&method=" + encodeURIComponent(method) + "&params=" + encodeURIComponent(width + "," + height); // /editorapi/img?src=" + encodeURIComponent(src) + "&method=" + encodeURIComponent(method) + "&params=" + encodeURIComponent(width + "," + height);
router.get('/img', passport.csrfProtection, (req, res) => { router.get('/img', (req, res) => {
settings.get('serviceUrl', (err, serviceUrl) => { getProcessedImage(req.originalUrl, (err, image) => {
if (err) { if (err) {
return res.status(500).send(err.message || err); res.status(err.status || 500);
res.send(err.message || err);
return;
} }
let { res.set('Content-Type', 'image/' + image.format.toLowerCase());
src, image.stream.pipe(res);
method,
params = '600,null'
} = req.query;
let width = params.split(',')[0];
let height = params.split(',')[1];
width = (width === 'null') ? null : Number(width);
height = (height === 'null') ? null : Number(height);
switch (method) {
case 'placeholder':
return placeholderImage(req, res, {
width,
height
});
case 'resize':
case 'cover':
src = /^https?:\/\/|^\/\//i.test(src) ? src : url.resolve(serviceUrl, src);
return resizedImage(req, res, {
src,
method,
width,
height
});
default:
return res.status(501).send(_('Method not supported'));
}
}); });
}); });
router.post('/update', passport.parseForm, passport.csrfProtection, (req, res) => { router.post('/update', passport.parseForm, passport.csrfProtection, (req, res) => {
prepareHtml({ const sendResponse = err => {
editorName: req.query.editor,
html: req.body.html
}, (err, html) => {
if (err) { if (err) {
return res.status(500).send(err.message || err); return res.status(500).send(err.message || err);
} }
res.send('ok');
};
prepareHtml(req.body.html, req.query.editor, (err, html) => {
if (err) {
return sendResponse(err);
}
req.body.html = html; req.body.html = html;
if (req.query.type === 'template') { switch (req.query.type) {
templates.update(req.body.id, req.body, (err, updated) => { case 'template':
if (err) { return templates.update(req.body.id, req.body, sendResponse);
return res.status(500).send(err.message || err); case 'campaign':
} return campaigns.update(req.body.id, req.body, sendResponse);
res.send('ok'); default:
}); return sendResponse(new Error(_('Invalid resource type')));
} else if (req.query.type === 'campaign') {
campaigns.update(req.body.id, req.body, (err, updated) => {
if (err) {
return res.status(500).send(err.message || err);
}
res.send('ok');
});
} else {
res.status(500).send(_('Invalid resource type'));
} }
}); });
}); });
@ -331,8 +331,8 @@ router.get('/upload', passport.csrfProtection, (req, res) => {
return res.status(500).send(err.message || err); return res.status(500).send(err.message || err);
} }
let baseDir = path.join(__dirname, '..', 'public', req.query.editor, 'uploads'); const baseDir = path.join(__dirname, '..', 'public', req.query.editor, 'uploads');
let baseDirUrl = serviceUrl + req.query.editor + '/uploads'; const baseDirUrl = serviceUrl + req.query.editor + '/uploads';
listImages(path.join(baseDir, '0'), baseDirUrl + '/0', (err, sharedImages) => { listImages(path.join(baseDir, '0'), baseDirUrl + '/0', (err, sharedImages) => {
if (err) { if (err) {
@ -358,15 +358,29 @@ router.get('/upload', passport.csrfProtection, (req, res) => {
}); });
router.post('/upload', passport.csrfProtection, (req, res) => { router.post('/upload', passport.csrfProtection, (req, res) => {
let dirName = req.query.type === 'template' ? '0' : settings.get('serviceUrl', (err, serviceUrl) => {
req.query.type === 'campaign' && Number(req.query.id) > 0 ? req.query.id : if (err) {
null; return res.status(500).send(err.message || err);
}
if (dirName === null) { const getDirName = () => {
switch (req.query.type) {
case 'template':
return '0';
case 'campaign':
return Number(req.query.id) > 0 ? req.query.id : false;
default:
return false;
}
};
const dirName = getDirName();
if (dirName === false) {
return res.status(500).send(_('Invalid resource type or ID')); return res.status(500).send(_('Invalid resource type or ID'));
} }
let opts = { const opts = {
tmpDir: config.www.tmpdir || os.tmpdir(), tmpDir: config.www.tmpdir || os.tmpdir(),
imageVersions: req.query.editor === 'mosaico' ? { imageVersions: req.query.editor === 'mosaico' ? {
thumbnail: { thumbnail: {
@ -376,16 +390,24 @@ router.post('/upload', passport.csrfProtection, (req, res) => {
} : {}, } : {},
uploadDir: path.join(__dirname, '..', 'public', req.query.editor, 'uploads', dirName), uploadDir: path.join(__dirname, '..', 'public', req.query.editor, 'uploads', dirName),
uploadUrl: '/' + req.query.editor + '/uploads/' + dirName, // must be root relative uploadUrl: '/' + req.query.editor + '/uploads/' + dirName, // must be root relative
acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, acceptFileTypes: /\.(gif|jpe?g|png)$/i,
hostname: url.parse(serviceUrl).host // include port
}; };
let mockres = httpMocks.createResponse({ const mockres = httpMocks.createResponse({
eventEmitter: events.EventEmitter eventEmitter: events.EventEmitter
}); });
mockres.on('error', err => {
res.status(500).json({
error: err.message || err,
data: []
});
});
mockres.on('end', () => { mockres.on('end', () => {
if (req.query.editor === 'grapejs') { const data = [];
let data = []; try {
JSON.parse(mockres._getData()).files.forEach(file => { JSON.parse(mockres._getData()).files.forEach(file => {
data.push({ data.push({
src: file.url src: file.url
@ -394,19 +416,20 @@ router.post('/upload', passport.csrfProtection, (req, res) => {
res.json({ res.json({
data data
}); });
} else { } catch(err) {
res.send(mockres._getData()); res.status(500).json({
error: err.message || err,
data
});
} }
}); });
jqueryFileUpload.fileHandler(opts)(req, mockres); jqueryFileUpload.fileHandler(opts)(req, req.query.editor === 'grapejs' ? mockres : res);
});
}); });
router.post('/download', passport.csrfProtection, (req, res) => { router.post('/download', passport.csrfProtection, (req, res) => {
prepareHtml({ prepareHtml(req.body.html, req.query.editor, (err, html) => {
editorName: req.query.editor,
html: req.body.html
}, (err, html) => {
if (err) { if (err) {
return res.status(500).send(err.message || err); return res.status(500).send(err.message || err);
} }
@ -416,9 +439,12 @@ router.post('/download', passport.csrfProtection, (req, res) => {
}); });
}); });
let parseGrapejsMultipartTestForm = (req, res, next) => { const parseGrapejsMultipartTestForm = (req, res, next) => {
if (req.query.editor === 'grapejs') { if (req.query.editor === 'grapejs') {
new multiparty.Form().parse(req, (err, fields, files) => { new multiparty.Form().parse(req, (err, fields) => {
if (err) {
return next(err);
}
req.body.email = fields.email[0]; req.body.email = fields.email[0];
req.body.subject = fields.subject[0]; req.body.subject = fields.subject[0];
req.body.html = fields.html[0]; req.body.html = fields.html[0];
@ -431,7 +457,7 @@ let parseGrapejsMultipartTestForm = (req, res, next) => {
}; };
router.post('/test', parseGrapejsMultipartTestForm, passport.csrfProtection, (req, res) => { router.post('/test', parseGrapejsMultipartTestForm, passport.csrfProtection, (req, res) => {
let sendError = err => { const sendError = err => {
if (req.query.editor === 'grapejs') { if (req.query.editor === 'grapejs') {
res.status(500).json({ res.status(500).json({
errors: err.message || err errors: err.message || err
@ -441,10 +467,7 @@ router.post('/test', parseGrapejsMultipartTestForm, passport.csrfProtection, (re
} }
}; };
prepareHtml({ prepareHtml(req.body.html, req.query.editor, (err, html) => {
editorName: req.query.editor,
html: req.body.html
}, (err, html) => {
if (err) { if (err) {
return sendError(err); return sendError(err);
} }
@ -459,29 +482,31 @@ router.post('/test', parseGrapejsMultipartTestForm, passport.csrfProtection, (re
return sendError(err); return sendError(err);
} }
let opts = { const opts = {
from: { from: {
name: configItems.defaultFrom, name: configItems.defaultFrom,
address: configItems.defaultAddress, address: configItems.defaultAddress
}, },
to: req.body.email, to: req.body.email,
subject: req.body.subject, subject: req.body.subject,
text: htmlToText.fromString(html, { text: htmlToText.fromString(html, {
wordwrap: 100 wordwrap: 100
}), }),
html, html
}; };
transport.sendMail(opts, (err, info) => { transport.sendMail(opts, err => {
if (err) { if (err) {
return sendError(err); return sendError(err);
} }
req.query.editor === 'grapejs' ? if (req.query.editor === 'grapejs') {
res.json({ res.json({
data: 'ok' data: 'ok'
}) : });
} else {
res.send('ok'); res.send('ok');
}
}); });
}); });
}); });

View file

@ -7,6 +7,7 @@ const passport = require('../lib/passport');
const _ = require('../lib/translate')._; const _ = require('../lib/translate')._;
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const settings = require('../lib/models/settings');
const editorHelpers = require('../lib/editor-helpers') const editorHelpers = require('../lib/editor-helpers')
router.all('/*', (req, res, next) => { router.all('/*', (req, res, next) => {
@ -18,6 +19,12 @@ router.all('/*', (req, res, next) => {
}); });
router.get('/editor', passport.csrfProtection, (req, res) => { router.get('/editor', passport.csrfProtection, (req, res) => {
settings.get('serviceUrl', (err, serviceUrl) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
editorHelpers.getResource(req.query.type, req.query.id, (err, resource) => { editorHelpers.getResource(req.query.type, req.query.id, (err, resource) => {
if (err) { if (err) {
req.flash('danger', err.message || err); req.flash('danger', err.message || err);
@ -33,7 +40,7 @@ router.get('/editor', passport.csrfProtection, (req, res) => {
} }
if (!resource.html && !resource.editorData.html && !resource.editorData.mjml) { if (!resource.html && !resource.editorData.html && !resource.editorData.mjml) {
const base = path.join(__dirname, '..', 'public', 'grapejs', 'templates', resource.editorData.template); const base = path.join(__dirname, '..', 'public', 'grapejs', 'templates', path.join('/', resource.editorData.template));
try { try {
resource.editorData.mjml = fs.readFileSync(path.join(base, 'index.mjml'), 'utf8'); resource.editorData.mjml = fs.readFileSync(path.join(base, 'index.mjml'), 'utf8');
} catch (err) { } catch (err) {
@ -55,7 +62,9 @@ router.get('/editor', passport.csrfProtection, (req, res) => {
mode: resource.editorData.mjml ? 'mjml' : 'html', mode: resource.editorData.mjml ? 'mjml' : 'html',
config: config.grapejs config: config.grapejs
}, },
csrfToken: req.csrfToken() csrfToken: req.csrfToken(),
serviceUrl
});
}); });
}); });
}); });

View file

@ -129,6 +129,7 @@
<script> <script>
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } }); $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } });
var serviceUrl = '{{{serviceUrl}}}';
var resource = {{{stringifiedResource}}}; var resource = {{{stringifiedResource}}};
var config = (function(mode) { var config = (function(mode) {
@ -155,7 +156,6 @@
// convert relative to absolute urls // convert relative to absolute urls
['mj-wrapper', 'mj-section', 'mj-navbar', 'mj-hero', 'mj-image'].forEach(function(tagName) { ['mj-wrapper', 'mj-section', 'mj-navbar', 'mj-hero', 'mj-image'].forEach(function(tagName) {
var serviceUrl = window.location.protocol + '//' + window.location.host + '/';
var elements = doc.getElementsByTagName(tagName); var elements = doc.getElementsByTagName(tagName);
for (var i = 0; i < elements.length; i++) { for (var i = 0; i < elements.length; i++) {
@ -246,6 +246,14 @@
document.body.appendChild(frame); document.body.appendChild(frame);
var frameDoc = frame.contentDocument || frame.contentWindow.document; var frameDoc = frame.contentDocument || frame.contentWindow.document;
var isLocalImage = function(src) {
var a1 = document.createElement('a');
var a2 = document.createElement('a');
a1.href = serviceUrl;
a2.href = src;
return a1.host === a2.host;
};
frame.onload = function() { frame.onload = function() {
var imgs = frameDoc.querySelectorAll('img'); var imgs = frameDoc.querySelectorAll('img');
@ -253,8 +261,10 @@
var img = imgs[i]; var img = imgs[i];
var m = img.src.match(/\/editorapi\/img\?src=([^&]*)/); var m = img.src.match(/\/editorapi\/img\?src=([^&]*)/);
var encodedSrc = m && m[1] || encodeURIComponent(img.src); var encodedSrc = m && m[1] || encodeURIComponent(img.src);
if (isLocalImage(decodeURIComponent(encodedSrc))) {
img.src = '/editorapi/img?src=' + encodedSrc + '&method=resize&params=' + img.clientWidth + '%2C' + img.clientHeight; img.src = '/editorapi/img?src=' + encodedSrc + '&method=resize&params=' + img.clientWidth + '%2C' + img.clientHeight;
} }
}
html = '<!doctype html>' + frameDoc.documentElement.outerHTML; html = '<!doctype html>' + frameDoc.documentElement.outerHTML;
document.body.removeChild(frame); document.body.removeChild(frame);