Refactored and linted editorapi image handling
This commit is contained in:
parent
ac63e934ec
commit
3c4558d70c
5 changed files with 347 additions and 304 deletions
|
@ -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: {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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(/&/g, '&');
|
const dynamicUrl = key.replace(/&/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) + "¶ms=" + encodeURIComponent(width + "," + height);
|
// /editorapi/img?src=" + encodeURIComponent(src) + "&method=" + encodeURIComponent(method) + "¶ms=" + 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,55 +358,78 @@ 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) {
|
|
||||||
return res.status(500).send(_('Invalid resource type or ID'));
|
|
||||||
}
|
|
||||||
|
|
||||||
let opts = {
|
|
||||||
tmpDir: config.www.tmpdir || os.tmpdir(),
|
|
||||||
imageVersions: req.query.editor === 'mosaico' ? {
|
|
||||||
thumbnail: {
|
|
||||||
width: 90,
|
|
||||||
height: 90
|
|
||||||
}
|
|
||||||
} : {},
|
|
||||||
uploadDir: path.join(__dirname, '..', 'public', req.query.editor, 'uploads', dirName),
|
|
||||||
uploadUrl: '/' + req.query.editor + '/uploads/' + dirName, // must be root relative
|
|
||||||
acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mockres = httpMocks.createResponse({
|
|
||||||
eventEmitter: events.EventEmitter
|
|
||||||
});
|
|
||||||
|
|
||||||
mockres.on('end', () => {
|
|
||||||
if (req.query.editor === 'grapejs') {
|
|
||||||
let data = [];
|
|
||||||
JSON.parse(mockres._getData()).files.forEach(file => {
|
|
||||||
data.push({
|
|
||||||
src: file.url
|
|
||||||
});
|
|
||||||
});
|
|
||||||
res.json({
|
|
||||||
data
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.send(mockres._getData());
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
jqueryFileUpload.fileHandler(opts)(req, mockres);
|
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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
tmpDir: config.www.tmpdir || os.tmpdir(),
|
||||||
|
imageVersions: req.query.editor === 'mosaico' ? {
|
||||||
|
thumbnail: {
|
||||||
|
width: 90,
|
||||||
|
height: 90
|
||||||
|
}
|
||||||
|
} : {},
|
||||||
|
uploadDir: path.join(__dirname, '..', 'public', req.query.editor, 'uploads', dirName),
|
||||||
|
uploadUrl: '/' + req.query.editor + '/uploads/' + dirName, // must be root relative
|
||||||
|
acceptFileTypes: /\.(gif|jpe?g|png)$/i,
|
||||||
|
hostname: url.parse(serviceUrl).host // include port
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockres = httpMocks.createResponse({
|
||||||
|
eventEmitter: events.EventEmitter
|
||||||
|
});
|
||||||
|
|
||||||
|
mockres.on('error', err => {
|
||||||
|
res.status(500).json({
|
||||||
|
error: err.message || err,
|
||||||
|
data: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mockres.on('end', () => {
|
||||||
|
const data = [];
|
||||||
|
try {
|
||||||
|
JSON.parse(mockres._getData()).files.forEach(file => {
|
||||||
|
data.push({
|
||||||
|
src: file.url
|
||||||
|
});
|
||||||
|
});
|
||||||
|
res.json({
|
||||||
|
data
|
||||||
|
});
|
||||||
|
} catch(err) {
|
||||||
|
res.status(500).json({
|
||||||
|
error: err.message || err,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,44 +19,52 @@ router.all('/*', (req, res, next) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/editor', passport.csrfProtection, (req, res) => {
|
router.get('/editor', passport.csrfProtection, (req, res) => {
|
||||||
editorHelpers.getResource(req.query.type, req.query.id, (err, resource) => {
|
settings.get('serviceUrl', (err, serviceUrl) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
req.flash('danger', err.message || err);
|
req.flash('danger', err.message || err);
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
editorHelpers.getResource(req.query.type, req.query.id, (err, resource) => {
|
||||||
resource.editorData = JSON.parse(resource.editorData);
|
if (err) {
|
||||||
} catch (err) {
|
req.flash('danger', err.message || err);
|
||||||
resource.editorData = {
|
return res.redirect('/');
|
||||||
template: req.query.template || 'demo'
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!resource.html && !resource.editorData.html && !resource.editorData.mjml) {
|
|
||||||
const base = path.join(__dirname, '..', 'public', 'grapejs', 'templates', resource.editorData.template);
|
|
||||||
try {
|
try {
|
||||||
resource.editorData.mjml = fs.readFileSync(path.join(base, 'index.mjml'), 'utf8');
|
resource.editorData = JSON.parse(resource.editorData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
try {
|
resource.editorData = {
|
||||||
resource.html = fs.readFileSync(path.join(base, 'index.html'), 'utf8');
|
template: req.query.template || 'demo'
|
||||||
} catch (err) {
|
|
||||||
resource.html = err.message || err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
res.render('grapejs/editor', {
|
if (!resource.html && !resource.editorData.html && !resource.editorData.mjml) {
|
||||||
layout: 'grapejs/layout-editor',
|
const base = path.join(__dirname, '..', 'public', 'grapejs', 'templates', path.join('/', resource.editorData.template));
|
||||||
type: req.query.type,
|
try {
|
||||||
stringifiedResource: JSON.stringify(resource),
|
resource.editorData.mjml = fs.readFileSync(path.join(base, 'index.mjml'), 'utf8');
|
||||||
resource,
|
} catch (err) {
|
||||||
editor: {
|
try {
|
||||||
name: resource.editorName || 'grapejs',
|
resource.html = fs.readFileSync(path.join(base, 'index.html'), 'utf8');
|
||||||
mode: resource.editorData.mjml ? 'mjml' : 'html',
|
} catch (err) {
|
||||||
config: config.grapejs
|
resource.html = err.message || err;
|
||||||
},
|
}
|
||||||
csrfToken: req.csrfToken()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render('grapejs/editor', {
|
||||||
|
layout: 'grapejs/layout-editor',
|
||||||
|
type: req.query.type,
|
||||||
|
stringifiedResource: JSON.stringify(resource),
|
||||||
|
resource,
|
||||||
|
editor: {
|
||||||
|
name: resource.editorName || 'grapejs',
|
||||||
|
mode: resource.editorData.mjml ? 'mjml' : 'html',
|
||||||
|
config: config.grapejs
|
||||||
|
},
|
||||||
|
csrfToken: req.csrfToken(),
|
||||||
|
serviceUrl
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,7 +261,9 @@
|
||||||
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);
|
||||||
img.src = '/editorapi/img?src=' + encodedSrc + '&method=resize¶ms=' + img.clientWidth + '%2C' + img.clientHeight;
|
if (isLocalImage(decodeURIComponent(encodedSrc))) {
|
||||||
|
img.src = '/editorapi/img?src=' + encodedSrc + '&method=resize¶ms=' + img.clientWidth + '%2C' + img.clientHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html = '<!doctype html>' + frameDoc.documentElement.outerHTML;
|
html = '<!doctype html>' + frameDoc.documentElement.outerHTML;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue