Merge branch 'master' of github.com:Mailtrain-org/mailtrain into access

This commit is contained in:
Tomas Bures 2017-06-21 02:16:28 +02:00
commit e7856bfb73
8 changed files with 368 additions and 364 deletions

View file

@ -5,7 +5,7 @@ module.exports = function (grunt) {
// Project configuration.
grunt.initConfig({
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: {

View file

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

View file

@ -1395,78 +1395,78 @@
<td align="right" valign="middle" class="links-color socialLinks mobile-textcenter" data-ko-display="socialIconType eq 'colors'">
<span data-ko-display="fbVisible" data-ko-wrap="false">&nbsp;</span>
<a data-ko-display="fbVisible" href="" style="-ko-attr-href: @fbUrl">
<img src="/mosaico/templates/versafix-1/img/social_def/facebook_ok.png" alt="Facebook" border="0" class="socialIcon" />
<img src="./img/social_def/facebook_ok.png" alt="Facebook" border="0" class="socialIcon" />
</a>
<span data-ko-display="twVisible" data-ko-wrap="false">&nbsp;</span>
<a data-ko-display="twVisible" href="" style="-ko-attr-href: @twUrl">
<img src="/mosaico/templates/versafix-1/img/social_def/twitter_ok.png" alt="Twitter" border="0" class="socialIcon" />
<img src="./img/social_def/twitter_ok.png" alt="Twitter" border="0" class="socialIcon" />
</a>
<span data-ko-display="ggVisible" data-ko-wrap="false">&nbsp;</span>
<a data-ko-display="ggVisible" href="" style="-ko-attr-href: @ggUrl">
<img src="/mosaico/templates/versafix-1/img/social_def/google+_ok.png" alt="Google+" border="0" class="socialIcon" />
<img src="./img/social_def/google+_ok.png" alt="Google+" border="0" class="socialIcon" />
</a>
<span data-ko-display="webVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="webVisible" href="" style="-ko-attr-href: @webUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/web_ok.png" alt="Web" border="0" class="socialIcon" />
<img src="./img/social_def/web_ok.png" alt="Web" border="0" class="socialIcon" />
</a>
<span data-ko-display="inVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="inVisible" href="" style="-ko-attr-href: @inUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/linkedin_ok.png" alt="Linkedin" border="0" class="socialIcon" />
<img src="./img/social_def/linkedin_ok.png" alt="Linkedin" border="0" class="socialIcon" />
</a>
<span data-ko-display="flVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="flVisible" href="" style="-ko-attr-href: @flUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/flickr_ok.png" alt="Flickr" border="0" class="socialIcon" />
<img src="./img/social_def/flickr_ok.png" alt="Flickr" border="0" class="socialIcon" />
</a>
<span data-ko-display="viVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="viVisible" href="" style="-ko-attr-href: @viUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/vimeo_ok.png" alt="Vimeo" border="0" class="socialIcon" />
<img src="./img/social_def/vimeo_ok.png" alt="Vimeo" border="0" class="socialIcon" />
</a>
<span data-ko-display="instVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="instVisible" href="" style="-ko-attr-href: @instUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/instagram_ok.png" alt="Instagram" border="0" class="socialIcon" />
<img src="./img/social_def/instagram_ok.png" alt="Instagram" border="0" class="socialIcon" />
</a>
<span data-ko-display="youVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="youVisible" href="" style="-ko-attr-href: @youUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/youtube_ok.png" alt="Youtube" border="0" class="socialIcon" />
<img src="./img/social_def/youtube_ok.png" alt="Youtube" border="0" class="socialIcon" />
</a>
</td>
<td align="right" valign="middle" class="links-color socialLinks mobile-textcenter" data-ko-display="socialIconType eq 'bw'"
style="display: none">
<span data-ko-display="fbVisible" data-ko-wrap="false">&nbsp;</span>
<a data-ko-display="fbVisible" href="" style="-ko-attr-href: @fbUrl">
<img src="/mosaico/templates/versafix-1/img/social_def/facebook_bw_ok.png" alt="Facebook" border="0" class="socialIcon" />
<img src="./img/social_def/facebook_bw_ok.png" alt="Facebook" border="0" class="socialIcon" />
</a>
<span data-ko-display="twVisible" data-ko-wrap="false">&nbsp;</span>
<a data-ko-display="twVisible" href="" style="-ko-attr-href: @twUrl">
<img src="/mosaico/templates/versafix-1/img/social_def/twitter_bw_ok.png" alt="Twitter" border="0" class="socialIcon" />
<img src="./img/social_def/twitter_bw_ok.png" alt="Twitter" border="0" class="socialIcon" />
</a>
<span data-ko-display="ggVisible" data-ko-wrap="false">&nbsp;</span>
<a data-ko-display="ggVisible" href="" style="-ko-attr-href: @ggUrl">
<img src="/mosaico/templates/versafix-1/img/social_def/google+_bw_ok.png" alt="Google+" border="0" class="socialIcon" />
<img src="./img/social_def/google+_bw_ok.png" alt="Google+" border="0" class="socialIcon" />
</a>
<span data-ko-display="webVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="webVisible" href="" style="-ko-attr-href: @webUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/web_bw_ok.png" alt="Web" border="0" class="socialIcon" />
<img src="./img/social_def/web_bw_ok.png" alt="Web" border="0" class="socialIcon" />
</a>
<span data-ko-display="inVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="inVisible" href="" style="-ko-attr-href: @inUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/linkedin_bw_ok.png" alt="Linkedin" border="0" class="socialIcon" />
<img src="./img/social_def/linkedin_bw_ok.png" alt="Linkedin" border="0" class="socialIcon" />
</a>
<span data-ko-display="flVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="flVisible" href="" style="-ko-attr-href: @flUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/flickr_bw_ok.png" alt="Flickr" border="0" class="socialIcon" />
<img src="./img/social_def/flickr_bw_ok.png" alt="Flickr" border="0" class="socialIcon" />
</a>
<span data-ko-display="viVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="viVisible" href="" style="-ko-attr-href: @viUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/vimeo_bw_ok.png" alt="Vimeo" border="0" class="socialIcon" />
<img src="./img/social_def/vimeo_bw_ok.png" alt="Vimeo" border="0" class="socialIcon" />
</a>
<span data-ko-display="instVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="instVisible" href="" style="-ko-attr-href: @instUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/instagram_bw_ok.png" alt="Instagram" border="0" class="socialIcon" />
<img src="./img/social_def/instagram_bw_ok.png" alt="Instagram" border="0" class="socialIcon" />
</a>
<span data-ko-display="youVisible" data-ko-wrap="false" style="display: none">&nbsp;</span>
<a data-ko-display="youVisible" href="" style="-ko-attr-href: @youUrl; display: none">
<img src="/mosaico/templates/versafix-1/img/social_def/youtube_bw_ok.png" alt="Youtube" border="0" class="socialIcon" />
<img src="./img/social_def/youtube_bw_ok.png" alt="Youtube" border="0" class="socialIcon" />
</a>
</td>
</tr>
@ -1514,7 +1514,7 @@
<tr data-ko-display="_root_.sponsor.visible" style="display: none;text-align:center">
<td align="center">
<a href="http://www.void.it" target="_blank" rel="noreferrer"><img border="0" hspace="0" vspace="0" src="/mosaico/templates/versafix-1/img/sponsor.gif" alt="sponsor"
<a href="http://www.void.it" target="_blank" rel="noreferrer"><img border="0" hspace="0" vspace="0" src="./img/sponsor.gif" alt="sponsor"
style="Margin:auto;display:inline !important;" /></a>
</td>
</tr>

View file

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

View file

@ -7,6 +7,7 @@ const passport = require('../lib/passport');
const _ = require('../lib/translate')._;
const fs = require('fs');
const path = require('path');
const settings = require('../lib/models/settings');
const editorHelpers = require('../lib/editor-helpers')
router.all('/*', (req, res, next) => {
@ -18,6 +19,12 @@ router.all('/*', (req, res, next) => {
});
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) => {
if (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) {
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 {
resource.editorData.mjml = fs.readFileSync(path.join(base, 'index.mjml'), 'utf8');
} catch (err) {
@ -55,7 +62,9 @@ router.get('/editor', passport.csrfProtection, (req, res) => {
mode: resource.editorData.mjml ? 'mjml' : 'html',
config: config.grapejs
},
csrfToken: req.csrfToken()
csrfToken: req.csrfToken(),
serviceUrl
});
});
});
});

View file

@ -129,6 +129,7 @@
<script>
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } });
var serviceUrl = '{{{serviceUrl}}}';
var resource = {{{stringifiedResource}}};
var config = (function(mode) {
@ -155,7 +156,6 @@
// convert relative to absolute urls
['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);
for (var i = 0; i < elements.length; i++) {
@ -246,6 +246,14 @@
document.body.appendChild(frame);
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() {
var imgs = frameDoc.querySelectorAll('img');
@ -253,8 +261,10 @@
var img = imgs[i];
var m = img.src.match(/\/editorapi\/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;
}
}
html = '<!doctype html>' + frameDoc.documentElement.outerHTML;
document.body.removeChild(frame);

View file

@ -109,7 +109,7 @@
</div>
<div class="col-md-4">
<h2><span class="glyphicon glyphicon-send"></span> {{#translate}}Send via Any Provider{{/translate}}</h2>
<p>{{#translate}}Mailtrain recommends <a href="https://sendpulse.com/?utm_source=mailtrain&utm_medium=providerlist">SendPulse</a> even though you can use any provider that supports SMTP protocol to send out your newsletters. Bounce and complaints handling via webhooks is supported for SES, SparkPost, SendGrid and Mailgun, also for Postfix and ZoneMTA.{{/translate}}</p>
<p>{{#translate}}You can use any provider that supports SMTP protocol to send out your newsletters. Bounce and complaints handling via webhooks is supported for SES, SparkPost, SendGrid and Mailgun, also for Postfix and ZoneMTA.{{/translate}}</p>
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" data-target=".modal-send-via-any-provider">{{#translate}}Show more{{/translate}}</button>
{{> modal_carousel
title='Send via Any Provider'
@ -130,40 +130,4 @@
</div>
</div>
<p><br></p>
<div class="row" style="background: #eee;">
<div class="col-md-8">
<h3>{{#translate}}Donate to Author{{/translate}}</h3>
<p>{{#translate}}Mailtrain is available under GPLv3 license and completely open source.{{/translate}}</p>
<p>{{#translate}}If you really like Mailtrain or your business benefits from it financially then I would really appreciate a small donation to keep the Mailtrain development engines running. You can either use Bitcoin or PayPal for donations. My Bitcoin wallet is{{/translate}}: <code>15Z8ADxhssKUiwP3jbbqJwA21744KMCfTM</code></p>
<p style="margin-bottom: 20px;"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=DB26KWR2BQX5W" class="btn btn-info">{{#translate}}Or Donate Using Paypal{{/translate}}</a></p>
</div>
</div>
<p><br></p>
<div class="row">
<div class="col-md-12">
<h3>{{#translate}}Official Mailtrain Partners{{/translate}}</h3>
<hr />
</div>
<div class="col-md-4">
<p>
<a href="https://sendpulse.com/?utm_source=mailtrain&utm_medium=logo">
<img class="media-object" src="/images/sendpulse-logo.png" alt="SendPulse">
</a>
</p>
<p>{{#translate}}A reliable SMTP server, easy integration, and 12,000 messages a month free{{/translate}}</p>
</div>
<div class="col-md-4">
<p>
<a href="http://www.iredmail.org/">
<img class="media-object" src="/images/iredmail-logo.png" alt="iRedMail">
</a>
</p>
<p>{{#translate}}Free, open source mail server solution{{/translate}}</p>
</div>
</div>
<p><br><br><br></p>

View file

@ -252,12 +252,9 @@
<div class="form-group">
<div class="pull-right">
<div class="text-right">
<button type="submit" id="verify-button" form="smtp-verify" class="btn btn-info" data-loading-text="{{#translate}}Checking{{/translate}}…"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> {{#translate}}Check Mailer config{{/translate}}</button>
</div>
<div class="col-sm-offset-2 col-xs-6">
<p class="form-control-static">{{#translate}}Don't have an SMTP account yet? Create a free SendPulse account{{/translate}} <a href="https://sendpulse.com/?utm_source=mailtrain&utm_medium=settings">{{#translate}}here{{/translate}}</a></p>
</div>
</div>
</fieldset>