Obsoleting some old files
Transition to SPA-style client Basis for Mosaico template editor
This commit is contained in:
parent
7750232716
commit
c85f2d4440
942 changed files with 86311 additions and 967 deletions
|
@ -1,15 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
router.get('/logout', (req, res) => {
|
||||
req.logout();
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
clientHelpers.registerRootRoute(router, 'account', _('Account'));
|
||||
|
||||
module.exports = router;
|
|
@ -1,166 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nodeifyFunction } = require('../lib/nodeify');
|
||||
const getSettings = nodeifyFunction(require('../models/settings').get);
|
||||
|
||||
let campaigns = require('../lib/models/campaigns');
|
||||
let links = require('../lib/models/links');
|
||||
let lists = require('../lib/models/lists');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
let tools = require('../lib/tools');
|
||||
let express = require('express');
|
||||
let request = require('request');
|
||||
let router = new express.Router();
|
||||
let passport = require('../lib/passport');
|
||||
let marked = require('marked');
|
||||
let _ = require('../lib/translate')._;
|
||||
let util = require('util');
|
||||
|
||||
router.get('/:campaign/:list/:subscription', passport.csrfProtection, (req, res, next) => {
|
||||
getSettings('serviceUrl', (err, serviceUrl) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
campaigns.getByCid(req.params.campaign, (err, campaign) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!campaign) {
|
||||
err = new Error(_('Not Found'));
|
||||
err.status = 404;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
lists.getByCid(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
err = new Error(_('Not Found'));
|
||||
err.status = 404;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
subscriptions.getWithMergeTags(list.id, req.params.subscription, (err, subscription) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
err = new Error(_('Not Found'));
|
||||
err.status = 404;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
campaigns.getAttachments(campaign.id, (err, attachments) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
let renderHtml = (html, renderTags) => {
|
||||
let render = (view, layout) => {
|
||||
res.render(view, {
|
||||
layout,
|
||||
message: renderTags ? tools.formatMessage(serviceUrl, campaign, list, subscription, html, false, true) : html,
|
||||
campaign,
|
||||
list,
|
||||
subscription,
|
||||
attachments,
|
||||
csrfToken: req.csrfToken()
|
||||
});
|
||||
};
|
||||
|
||||
if (campaign.editorName && campaign.editorName !== 'summernote' && campaign.editorName !== 'codeeditor') {
|
||||
res.render('partials/tracking-scripts', {
|
||||
layout: 'archive/layout-raw'
|
||||
}, (err, scripts) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
html = scripts ? html.replace(/<\/body\b/i, match => scripts + match) : html;
|
||||
render('archive/view-raw', 'archive/layout-raw');
|
||||
});
|
||||
} else {
|
||||
render('archive/view', 'archive/layout');
|
||||
}
|
||||
};
|
||||
|
||||
let renderAndShow = (html, renderTags) => {
|
||||
if (req.query.track === 'no') {
|
||||
return renderHtml(html, renderTags);
|
||||
}
|
||||
// rewrite links to count clicks
|
||||
links.updateLinks(campaign, list, subscription, serviceUrl, html, (err, html) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
renderHtml(html, renderTags);
|
||||
});
|
||||
};
|
||||
|
||||
if (campaign.sourceUrl) {
|
||||
let form = tools.getMessageLinks(serviceUrl, campaign, list, subscription);
|
||||
Object.keys(subscription.mergeTags).forEach(key => {
|
||||
form[key] = subscription.mergeTags[key];
|
||||
});
|
||||
request.post({
|
||||
url: campaign.sourceUrl,
|
||||
form
|
||||
}, (err, httpResponse, body) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (httpResponse.statusCode !== 200) {
|
||||
return next(new Error(util.format(_('Received status code %s from %s'), httpResponse.statusCode, campaign.sourceUrl)));
|
||||
}
|
||||
renderAndShow(body && body.toString(), false);
|
||||
});
|
||||
} else {
|
||||
renderAndShow(campaign.html || marked(campaign.text, {
|
||||
breaks: true,
|
||||
sanitize: true,
|
||||
gfm: true,
|
||||
tables: true,
|
||||
smartypants: true
|
||||
}), true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/attachment/download', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
let url = '/archive/' + encodeURIComponent(req.body.campaign || '') + '/' + encodeURIComponent(req.body.list || '') + '/' + encodeURIComponent(req.body.subscription || '');
|
||||
campaigns.getByCid(req.body.campaign, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
return res.redirect(url);
|
||||
}
|
||||
campaigns.getAttachment(campaign.id, Number(req.body.attachment), (err, attachment) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
return res.redirect(url);
|
||||
} else if (!attachment) {
|
||||
req.flash('warning', _('Attachment not found'));
|
||||
return res.redirect(url);
|
||||
}
|
||||
|
||||
res.set('Content-Disposition', 'attachment; filename="' + encodeURIComponent(attachment.filename).replace(/['()]/g, escape) + '"');
|
||||
res.set('Content-Type', attachment.contentType);
|
||||
res.send(attachment.content);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -1,10 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
clientHelpers.registerRootRoute(router, 'blacklist', _('Blacklist'));
|
||||
|
||||
module.exports = router;
|
|
@ -1,919 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nodeifyFunction } = require('../lib/nodeify');
|
||||
const getSettings = nodeifyFunction(require('../models/settings').get);
|
||||
|
||||
let config = require('config');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let lists = require('../lib/models/lists');
|
||||
let templates = require('../lib/models/templates');
|
||||
let campaigns = require('../lib/models/campaigns');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
let tools = require('../lib/tools');
|
||||
let editorHelpers = require('../lib/editor-helpers.js');
|
||||
let striptags = require('striptags');
|
||||
let passport = require('../lib/passport');
|
||||
let htmlescape = require('escape-html');
|
||||
let multer = require('multer');
|
||||
let _ = require('../lib/translate')._;
|
||||
let util = require('util');
|
||||
let uploadStorage = multer.memoryStorage();
|
||||
let uploads = multer({
|
||||
storage: uploadStorage
|
||||
});
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
res.setSelectedMenu('campaigns');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.render('campaigns/campaigns', {
|
||||
title: _('Campaigns')
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/create', passport.csrfProtection, (req, res) => {
|
||||
let data = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
if (/^\d+:\d+$/.test(data.list)) {
|
||||
data.segment = Number(data.list.split(':').pop());
|
||||
data.list = Number(data.list.split(':').shift());
|
||||
}
|
||||
|
||||
getSettings(['defaultFrom', 'defaultAddress', 'defaultSubject'], (err, configItems) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
lists.quicklist((err, listItems) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (Number(data.list)) {
|
||||
listItems.forEach(list => {
|
||||
list.segments.forEach(segment => {
|
||||
if (segment.id === data.segment) {
|
||||
segment.selected = true;
|
||||
}
|
||||
});
|
||||
if (list.id === data.list && !data.segment) {
|
||||
list.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
templates.quicklist((err, templateItems) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (Number(data.template)) {
|
||||
templateItems.forEach(item => {
|
||||
if (item.id === Number(data.template)) {
|
||||
item.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
data.csrfToken = req.csrfToken();
|
||||
data.listItems = listItems;
|
||||
data.templateItems = templateItems;
|
||||
|
||||
data.from = data.from || configItems.defaultFrom;
|
||||
data.address = data.address || configItems.defaultAddress;
|
||||
data.replyTo = data.replyTo || '';
|
||||
data.subject = data.subject || configItems.defaultSubject;
|
||||
|
||||
let view;
|
||||
switch (req.query.type) {
|
||||
case 'rss':
|
||||
view = 'campaigns/create-rss';
|
||||
break;
|
||||
case 'triggered':
|
||||
view = 'campaigns/create-triggered';
|
||||
break;
|
||||
default:
|
||||
view = 'campaigns/create';
|
||||
}
|
||||
res.render(view, data);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.create(req.body, false, (err, id) => {
|
||||
if (err || !id) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create campaign'));
|
||||
return res.redirect('/campaigns/create?' + tools.queryParams(req.body));
|
||||
}
|
||||
req.flash('success', util.format(_('Campaign “%s” created'), req.body.name));
|
||||
res.redirect((req.body.type === 'rss') ?
|
||||
'/campaigns/edit/' + id :
|
||||
'/campaigns/edit/' + id + '?tab=template'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
|
||||
campaigns.get(req.params.id, false, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
campaigns.getAttachments(campaign.id, (err, attachments) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
campaign.attachments = attachments;
|
||||
|
||||
getSettings(['disableWysiwyg'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
lists.quicklist((err, listItems) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (Number(campaign.list)) {
|
||||
listItems.forEach(list => {
|
||||
list.segments.forEach(segment => {
|
||||
if (segment.id === campaign.segment) {
|
||||
segment.selected = true;
|
||||
}
|
||||
});
|
||||
if (list.id === campaign.list && !campaign.segment) {
|
||||
list.selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
campaign.csrfToken = req.csrfToken();
|
||||
campaign.listItems = listItems;
|
||||
campaign.useEditor = true;
|
||||
campaign.editorName = campaign.editorName || 'summernote';
|
||||
campaign.editorConfig = config[campaign.editorName];
|
||||
|
||||
campaign.disableWysiwyg = configItems.disableWysiwyg;
|
||||
campaign.showGeneral = req.query.tab === 'general' || !req.query.tab;
|
||||
campaign.showTemplate = req.query.tab === 'template';
|
||||
campaign.showAttachments = req.query.tab === 'attachments';
|
||||
|
||||
let view;
|
||||
switch (campaign.type) {
|
||||
case 4: //triggered
|
||||
view = 'campaigns/edit-triggered';
|
||||
break;
|
||||
case 2: //rss
|
||||
view = 'campaigns/edit-rss';
|
||||
break;
|
||||
case 1:
|
||||
default:
|
||||
view = 'campaigns/edit';
|
||||
}
|
||||
|
||||
editorHelpers.getMergeTagsForResource(campaign, (err, mergeTags) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
campaign.mergeTags = mergeTags;
|
||||
res.render(view, campaign);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.update(req.body.id, req.body, (err, updated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
} else if (updated) {
|
||||
req.flash('success', _('Campaign settings updated'));
|
||||
} else {
|
||||
req.flash('info', _('Campaign settings not updated'));
|
||||
}
|
||||
|
||||
if (req.body.id) {
|
||||
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
|
||||
} else {
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Campaign deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified campaign'));
|
||||
}
|
||||
|
||||
return res.redirect('/campaigns');
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/ajax', (req, res) => {
|
||||
campaigns.filter(req.body, Number(req.query.parent) || false, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
let getStatusText = data => {
|
||||
switch (data.status) {
|
||||
case 1:
|
||||
return _('Idling');
|
||||
case 2:
|
||||
if (data.scheduled && data.scheduled > new Date()) {
|
||||
return _('Scheduled');
|
||||
}
|
||||
return '<span class="glyphicon glyphicon-refresh spinning"></span> ' + _('Sending') + '…';
|
||||
case 3:
|
||||
return _('Finished');
|
||||
case 4:
|
||||
return _('Paused');
|
||||
case 5:
|
||||
return _('Inactive');
|
||||
case 6:
|
||||
return _('Active');
|
||||
}
|
||||
return _('Other');
|
||||
};
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
(Number(req.body.start) || 0) + 1 + i,
|
||||
'<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> <a href="/campaigns/view/' + row.id + '">' + htmlescape(row.name || '') + '</a>',
|
||||
htmlescape(striptags(row.description) || ''),
|
||||
getStatusText(row),
|
||||
'<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>'
|
||||
].concat('<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/campaigns/edit/' + row.id + '">' + _('Edit') + '</a>'))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/view/:id', passport.csrfProtection, (req, res) => {
|
||||
campaigns.get(req.params.id, true, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
let getList = (listId, callback) => {
|
||||
lists.get(listId, (err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!list) {
|
||||
list = {
|
||||
id: listId
|
||||
};
|
||||
}
|
||||
subscriptions.listTestUsers(listId, (err, testUsers) => {
|
||||
if (err || !testUsers) {
|
||||
testUsers = [];
|
||||
}
|
||||
return callback(null, list, testUsers);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getList(campaign.list, (err, list, testUsers) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
campaign.csrfToken = req.csrfToken();
|
||||
|
||||
campaign.list = list;
|
||||
campaign.testUsers = testUsers;
|
||||
|
||||
campaign.isIdling = campaign.status === 1;
|
||||
campaign.isSending = campaign.status === 2;
|
||||
campaign.isFinished = campaign.status === 3;
|
||||
campaign.isPaused = campaign.status === 4;
|
||||
campaign.isInactive = campaign.status === 5;
|
||||
campaign.isActive = campaign.status === 6;
|
||||
|
||||
campaign.isNormal = campaign.type === 1 || campaign.type === 3;
|
||||
campaign.isRss = campaign.type === 2;
|
||||
campaign.isTriggered = campaign.type === 4;
|
||||
|
||||
campaign.isScheduled = campaign.scheduled && campaign.scheduled > new Date();
|
||||
|
||||
// show only messages that weren't bounced as delivered
|
||||
campaign.delivered = campaign.delivered - campaign.bounced;
|
||||
|
||||
campaign.openRate = campaign.delivered ? Math.round((campaign.opened / campaign.delivered) * 10000) / 100 : 0;
|
||||
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 10000) / 100 : 0;
|
||||
campaign.bounceRate = campaign.delivered ? Math.round((campaign.bounced / campaign.delivered) * 10000) / 100 : 0;
|
||||
campaign.complaintRate = campaign.delivered ? Math.round((campaign.complained / campaign.delivered) * 10000) / 100 : 0;
|
||||
campaign.unsubscribeRate = campaign.delivered ? Math.round((campaign.unsubscribed / campaign.delivered) * 10000) / 100 : 0;
|
||||
|
||||
campaigns.getLinks(campaign.id, (err, links) => {
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
let index = 0;
|
||||
campaign.links = (links || []).map(link => {
|
||||
link.index = ++index;
|
||||
link.totalPercentage = campaign.delivered ? Math.round(((link.clicks / campaign.delivered) * 100) * 1000) / 1000 : 0;
|
||||
link.relPercentage = campaign.clicks ? Math.round(((link.clicks / campaign.clicks) * 100) * 1000) / 1000 : 0;
|
||||
link.short = link.url.replace(/^https?:\/\/(www.)?/i, '');
|
||||
if (link.short > 63) {
|
||||
link.short = link.short.substr(0, 60) + '…';
|
||||
}
|
||||
return link;
|
||||
});
|
||||
campaign.showOverview = !req.query.tab || req.query.tab === 'overview';
|
||||
campaign.showLinks = req.query.tab === 'links';
|
||||
res.render('campaigns/view', campaign);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/preview/:id', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
let campaign = req.body.campaign;
|
||||
let list = req.body.list;
|
||||
let listId = req.body.listId;
|
||||
let subscription = req.body.subscriber;
|
||||
|
||||
if (subscription === '_create') {
|
||||
return res.redirect('/lists/subscription/' + encodeURIComponent(listId) + '/add/?is-test=true');
|
||||
}
|
||||
|
||||
res.redirect('/archive/' + encodeURIComponent(campaign) + '/' + encodeURIComponent(list) + '/' + encodeURIComponent(subscription) + '?track=no');
|
||||
});
|
||||
|
||||
router.get('/opened/:id', passport.csrfProtection, (req, res) => {
|
||||
campaigns.get(req.params.id, true, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
lists.get(campaign.list, (err, list) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
campaign.csrfToken = req.csrfToken();
|
||||
campaign.list = list;
|
||||
|
||||
// show only messages that weren't bounced as delivered
|
||||
campaign.delivered = campaign.delivered - campaign.bounced;
|
||||
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0;
|
||||
|
||||
res.render('campaigns/opened', campaign);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/status/:id/:status', passport.csrfProtection, (req, res) => {
|
||||
let id = Number(req.params.id) || 0;
|
||||
let status;
|
||||
switch (req.params.status) {
|
||||
case 'delivered':
|
||||
status = 1;
|
||||
break;
|
||||
case 'unsubscribed':
|
||||
status = 2;
|
||||
break;
|
||||
case 'bounced':
|
||||
status = 3;
|
||||
break;
|
||||
case 'complained':
|
||||
status = 4;
|
||||
break;
|
||||
case 'blacklisted':
|
||||
status = 5;
|
||||
break;
|
||||
default:
|
||||
req.flash('danger', _('Unknown status selector'));
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
campaigns.get(id, true, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
let getList = (listId, callback) => {
|
||||
lists.get(listId, (err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!list) {
|
||||
list = {
|
||||
id: listId
|
||||
};
|
||||
}
|
||||
return callback(null, list);
|
||||
});
|
||||
};
|
||||
|
||||
getList(campaign.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
campaign.csrfToken = req.csrfToken();
|
||||
campaign.list = list;
|
||||
|
||||
// show only messages that weren't bounced as delivered
|
||||
campaign.delivered = campaign.delivered - campaign.bounced;
|
||||
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0;
|
||||
campaign.status = status;
|
||||
|
||||
res.render('campaigns/' + req.params.status, campaign);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/clicked/:id/:linkId', passport.csrfProtection, (req, res) => {
|
||||
campaigns.get(req.params.id, true, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
let getList = (listId, callback) => {
|
||||
lists.get(listId, (err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!list) {
|
||||
list = {
|
||||
id: listId
|
||||
};
|
||||
}
|
||||
return callback(null, list);
|
||||
});
|
||||
};
|
||||
|
||||
getList(campaign.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
|
||||
campaign.csrfToken = req.csrfToken();
|
||||
campaign.list = list;
|
||||
|
||||
// show only messages that weren't bounced as delivered
|
||||
campaign.delivered = campaign.delivered - campaign.bounced;
|
||||
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0;
|
||||
|
||||
if (req.params.linkId === 'all') {
|
||||
campaign.aggregated = true;
|
||||
campaign.link = {
|
||||
id: 0
|
||||
};
|
||||
res.render('campaigns/clicked', campaign);
|
||||
} else {
|
||||
campaigns.getLinks(campaign.id, req.params.linkId, (err, links) => {
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
let index = 0;
|
||||
campaign.link = (links || []).map(link => {
|
||||
link.index = ++index;
|
||||
link.totalPercentage = campaign.delivered ? Math.round(((link.clicks / campaign.delivered) * 100) * 1000) / 1000 : 0;
|
||||
link.relPercentage = campaign.clicks ? Math.round(((link.clicks / campaign.clicks) * 100) * 1000) / 1000 : 0;
|
||||
link.short = link.url.replace(/^https?:\/\/(www.)?/i, '');
|
||||
if (link.short > 63) {
|
||||
link.short = link.short.substr(0, 60) + '…';
|
||||
}
|
||||
return link;
|
||||
}).shift();
|
||||
res.render('campaigns/clicked', campaign);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/clicked/ajax/:id/:linkId', (req, res) => {
|
||||
let linkId = Number(req.params.linkId) || 0;
|
||||
|
||||
campaigns.get(req.params.id, true, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
return res.json({
|
||||
error: err && err.message || err || _('Campaign not found'),
|
||||
data: []
|
||||
});
|
||||
}
|
||||
lists.get(campaign.list, (err, list) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err && err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
let campaignCid = campaign.cid;
|
||||
let listCid = list.cid;
|
||||
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '.created', 'count'];
|
||||
campaigns.filterClickedSubscribers(campaign, linkId, req.body, columns, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
'<a href="/archive/' + encodeURIComponent(campaignCid) + '/' + encodeURIComponent(listCid) + '/' + encodeURIComponent(row.cid) + '?track=no">' + ((Number(req.body.start) || 0) + 1 + i) + '</a>',
|
||||
htmlescape(row.email || ''),
|
||||
htmlescape(row.firstName || ''),
|
||||
htmlescape(row.lastName || ''),
|
||||
row.created && row.created.toISOString ? '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>' : 'N/A',
|
||||
row.count,
|
||||
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/subscription/' + campaign.list + '/edit/' + row.cid + '">' + _('Edit') + '</a>'
|
||||
])
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/clicked/ajax/:id/:linkId/stats', (req, res) => {
|
||||
let linkId = Number(req.params.linkId) || 0;
|
||||
campaigns.get(req.params.id, true, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
return res.json({
|
||||
error: err && err.message || err || _('Campaign not found'),
|
||||
data: []
|
||||
});
|
||||
}
|
||||
lists.get(campaign.list, (err, list) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err && err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
let column = req.body.column;
|
||||
let limit = req.body.limit;
|
||||
|
||||
campaigns.statsClickedSubscribersByColumn(campaign, linkId, req.body, column, limit, (err, data, total) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
total: total,
|
||||
data: data
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.post('/status/ajax/:id/:status', (req, res) => {
|
||||
let status = Number(req.params.status) || 0;
|
||||
|
||||
campaigns.get(req.params.id, true, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
return res.json({
|
||||
error: err && err.message || err || _('Campaign not found'),
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
lists.get(campaign.list, (err, list) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err && err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
let campaignCid = campaign.cid;
|
||||
let listCid = list.cid;
|
||||
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign__' + campaign.id + '.updated'];
|
||||
campaigns.filterStatusSubscribers(campaign, status, req.body, columns, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
'<a href="/archive/' + encodeURIComponent(campaignCid) + '/' + encodeURIComponent(listCid) + '/' + encodeURIComponent(row.cid) + '?track=no">' + ((Number(req.body.start) || 0) + 1 + i) + '</a>',
|
||||
htmlescape(row.email || ''),
|
||||
htmlescape(row.firstName || ''),
|
||||
htmlescape(row.lastName || ''),
|
||||
htmlescape(row.response || ''),
|
||||
row.updated && row.created.toISOString ? '<span class="datestring" data-date="' + row.updated.toISOString() + '" title="' + row.updated.toISOString() + '">' + row.updated.toISOString() + '</span>' : 'N/A',
|
||||
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/subscription/' + campaign.list + '/edit/' + row.cid + '">' + _('Edit') + '</a>'
|
||||
])
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/clicked/ajax/:id/:linkId', (req, res) => {
|
||||
let linkId = Number(req.params.linkId) || 0;
|
||||
|
||||
campaigns.get(req.params.id, true, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
return res.json({
|
||||
error: err && err.message || err || _('Campaign not found'),
|
||||
data: []
|
||||
});
|
||||
}
|
||||
lists.get(campaign.list, (err, list) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err && err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
let campaignCid = campaign.cid;
|
||||
let listCid = list.cid;
|
||||
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '.created', 'count'];
|
||||
campaigns.filterClickedSubscribers(campaign, linkId, req.body, columns, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
'<a href="/archive/' + encodeURIComponent(campaignCid) + '/' + encodeURIComponent(listCid) + '/' + encodeURIComponent(row.cid) + '?track=no">' + ((Number(req.body.start) || 0) + 1 + i) + '</a>',
|
||||
htmlescape(row.email || ''),
|
||||
htmlescape(row.firstName || ''),
|
||||
htmlescape(row.lastName || ''),
|
||||
row.created && row.created.toISOString ? '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>' : 'N/A',
|
||||
row.count,
|
||||
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/subscription/' + campaign.list + '/edit/' + row.cid + '">' + _('Edit') + '</a>'
|
||||
])
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/quicklist/ajax', (req, res) => {
|
||||
campaigns.filterQuicklist(req.body, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => ({
|
||||
"0": (Number(req.body.start) || 0) + 1 + i,
|
||||
"1": '<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> <a href="/campaigns/view/' + row.id + '">' + htmlescape(row.name || '') + '</a>',
|
||||
"2": htmlescape(striptags(row.description) || ''),
|
||||
"3": '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>',
|
||||
"DT_RowId": row.id
|
||||
}))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Campaign deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified campaign'));
|
||||
}
|
||||
|
||||
return res.redirect('/campaigns');
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/send', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
let delayHours = Math.max(Number(req.body['delay-hours']) || 0, 0);
|
||||
let delayMinutes = Math.max(Number(req.body['delay-minutes']) || 0, 0);
|
||||
let scheduled = new Date(Date.now() + delayHours * 3600 * 1000 + delayMinutes * 60 * 1000);
|
||||
|
||||
campaigns.send(req.body.id, scheduled, (err, scheduled) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (scheduled) {
|
||||
req.flash('success', _('Scheduled sending'));
|
||||
} else {
|
||||
req.flash('info', _('Could not schedule sending'));
|
||||
}
|
||||
|
||||
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/resume', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.send(req.body.id, false, (err, scheduled) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (scheduled) {
|
||||
req.flash('success', _('Sending resumed'));
|
||||
} else {
|
||||
req.flash('info', _('Could not resume sending'));
|
||||
}
|
||||
|
||||
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/reset', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.reset(req.body.id, (err, reset) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (reset) {
|
||||
req.flash('success', _('Sending reset'));
|
||||
} else {
|
||||
req.flash('info', _('Could not reset sending'));
|
||||
}
|
||||
|
||||
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/pause', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.pause(req.body.id, (err, reset) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (reset) {
|
||||
req.flash('success', _('Sending paused'));
|
||||
} else {
|
||||
req.flash('info', _('Could not pause sending'));
|
||||
}
|
||||
|
||||
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/activate', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.activate(req.body.id, (err, reset) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (reset) {
|
||||
req.flash('success', _('Sending activated'));
|
||||
} else {
|
||||
req.flash('info', _('Could not activate sending'));
|
||||
}
|
||||
|
||||
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/inactivate', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.inactivate(req.body.id, (err, reset) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (reset) {
|
||||
req.flash('success', _('Sending paused'));
|
||||
} else {
|
||||
req.flash('info', _('Could not pause sending'));
|
||||
}
|
||||
|
||||
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/attachment', uploads.single('attachment'), passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.get(req.body.id, false, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
campaigns.addAttachment(campaign.id, {
|
||||
filename: req.file.originalname,
|
||||
contentType: req.file.mimetype,
|
||||
content: req.file.buffer
|
||||
}, (err, attachmentId) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (attachmentId) {
|
||||
req.flash('success', _('Attachment uploaded'));
|
||||
} else {
|
||||
req.flash('info', _('Could not store attachment'));
|
||||
}
|
||||
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/attachment/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.get(req.body.id, false, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
campaigns.deleteAttachment(campaign.id, Number(req.body.attachment), (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Attachment deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete attachment'));
|
||||
}
|
||||
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/attachment/download', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.get(req.body.id, false, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
campaigns.getAttachment(campaign.id, Number(req.body.attachment), (err, attachment) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
|
||||
} else if (!attachment) {
|
||||
req.flash('warning', _('Attachment not found'));
|
||||
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
|
||||
}
|
||||
|
||||
res.set('Content-Disposition', 'attachment; filename="' + encodeURIComponent(attachment.filename).replace(/['()]/g, escape) + '"');
|
||||
res.set('Content-Type', attachment.contentType);
|
||||
res.send(attachment.content);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/attachment/:campaign', passport.csrfProtection, (req, res) => {
|
||||
campaigns.get(req.params.campaign, false, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
campaign.csrfToken = req.csrfToken();
|
||||
res.render('campaigns/upload-attachment', campaign);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -1,532 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nodeifyFunction } = require('../lib/nodeify');
|
||||
const getSettings = nodeifyFunction(require('../models/settings').get);
|
||||
|
||||
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
|
||||
});
|
||||
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 templates = require('../lib/models/templates');
|
||||
const campaigns = require('../lib/models/campaigns');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(403).send(_('Need to be logged in to access restricted content'));
|
||||
}
|
||||
if (req.originalUrl.startsWith('/editorapi/img?')) {
|
||||
return next();
|
||||
}
|
||||
if (!config.editors.map(e => e[0]).includes(req.query.editor)) {
|
||||
return res.status(500).send(_('Invalid editor name'));
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
jqueryFileUpload.on('begin', fileInfo => {
|
||||
fileInfo.name = fileInfo.name
|
||||
.toLowerCase()
|
||||
.replace(/ /g, '-')
|
||||
.replace(/[^a-z0-9+-.]+/g, '');
|
||||
});
|
||||
|
||||
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 => ({
|
||||
// mosaico
|
||||
name,
|
||||
url: dirURL + '/' + name,
|
||||
thumbnailUrl: dirURL + '/thumbnail/' + name,
|
||||
// grapejs
|
||||
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);
|
||||
}
|
||||
|
||||
fs.readdir(staticDir, (err, files) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const hash = crypto.createHash('md5').update(dynamicUrl).digest('hex');
|
||||
const match = files.find(el => el.startsWith(hash));
|
||||
|
||||
if (match) {
|
||||
return callback(null, staticDirUrl + '/' + match);
|
||||
}
|
||||
|
||||
getProcessedImage(dynamicUrl, (err, image) => {
|
||||
if (err) {
|
||||
return 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const prepareHtml = (html, editorName, callback) => {
|
||||
getSettings('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 result;
|
||||
|
||||
while ((result = re.exec(html)) !== null) {
|
||||
srcs.set(result[1], result[1]);
|
||||
}
|
||||
|
||||
const done = () => {
|
||||
if (jobs === 0) {
|
||||
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');
|
||||
|
||||
for (const key of srcs.keys()) {
|
||||
jobs++;
|
||||
const dynamicUrl = key.replace(/&/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);
|
||||
|
||||
if (dynamicUrl.includes('/editorapi/img?')) {
|
||||
staticUrl = url.parse(dynamicUrl, true).query.src || dynamicUrl;
|
||||
} else {
|
||||
staticUrl = dynamicUrl;
|
||||
}
|
||||
|
||||
if (!/^https?:\/\/|^\/\//i.test(staticUrl)) {
|
||||
staticUrl = url.resolve(serviceUrl, staticUrl);
|
||||
}
|
||||
}
|
||||
|
||||
srcs.set(key, staticUrl);
|
||||
jobs--;
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
// URL structure defined by Mosaico
|
||||
// /editorapi/img?src=" + encodeURIComponent(src) + "&method=" + encodeURIComponent(method) + "¶ms=" + encodeURIComponent(width + "," + height);
|
||||
router.get('/img', (req, res) => {
|
||||
getProcessedImage(req.originalUrl, (err, image) => {
|
||||
if (err) {
|
||||
res.status(err.status || 500);
|
||||
res.send(err.message || err);
|
||||
return;
|
||||
}
|
||||
|
||||
res.set('Content-Type', 'image/' + image.format.toLowerCase());
|
||||
image.stream.pipe(res);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/update', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
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;
|
||||
|
||||
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')));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/artf/grapesjs/wiki/API-Asset-Manager
|
||||
// https://github.com/aguidrevitch/jquery-file-upload-middleware
|
||||
|
||||
router.get('/upload', passport.csrfProtection, (req, res) => {
|
||||
getSettings('serviceUrl', (err, serviceUrl) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
|
||||
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) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
|
||||
if (req.query.type === 'campaign' && Number(req.query.id) > 0) {
|
||||
listImages(path.join(baseDir, req.query.id), baseDirUrl + '/' + req.query.id, (err, campaignImages) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
res.json({
|
||||
files: sharedImages.concat(campaignImages)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
files: sharedImages
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/upload', passport.csrfProtection, (req, res) => {
|
||||
getSettings('serviceUrl', (err, serviceUrl) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
|
||||
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();
|
||||
const serviceUrlParts = url.parse(serviceUrl);
|
||||
|
||||
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: serviceUrlParts.host, // include port
|
||||
ssl: serviceUrlParts.protocol === 'https:'
|
||||
};
|
||||
|
||||
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) => {
|
||||
prepareHtml(req.body.html, req.query.editor, (err, html) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
res.setHeader('Content-disposition', 'attachment; filename=' + req.body.filename);
|
||||
res.setHeader('Content-type', 'text/html');
|
||||
res.send(html);
|
||||
});
|
||||
});
|
||||
|
||||
const parseGrapejsMultipartTestForm = (req, res, next) => {
|
||||
if (req.query.editor === 'grapejs') {
|
||||
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];
|
||||
req.body._csrf = fields._csrf[0];
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
router.post('/test', parseGrapejsMultipartTestForm, passport.csrfProtection, (req, res) => {
|
||||
const sendError = err => {
|
||||
if (req.query.editor === 'grapejs') {
|
||||
res.status(500).json({
|
||||
errors: err.message || err
|
||||
});
|
||||
} else {
|
||||
res.status(500).send(err.message || err);
|
||||
}
|
||||
};
|
||||
|
||||
prepareHtml(req.body.html, req.query.editor, (err, html) => {
|
||||
if (err) {
|
||||
return sendError(err);
|
||||
}
|
||||
|
||||
getSettings(['defaultAddress', 'defaultFrom'], (err, configItems) => {
|
||||
if (err) {
|
||||
return sendError(err);
|
||||
}
|
||||
|
||||
mailer.getMailer((err, transport) => {
|
||||
if (err) {
|
||||
return sendError(err);
|
||||
}
|
||||
|
||||
const opts = {
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: req.body.email,
|
||||
subject: req.body.subject,
|
||||
text: htmlToText.fromString(html, {
|
||||
wordwrap: 100
|
||||
}),
|
||||
html
|
||||
};
|
||||
|
||||
transport.sendMail(opts, err => {
|
||||
if (err) {
|
||||
return sendError(err);
|
||||
}
|
||||
|
||||
if (req.query.editor === 'grapejs') {
|
||||
res.json({
|
||||
data: 'ok'
|
||||
});
|
||||
} else {
|
||||
res.send('ok');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/html-to-text', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
premailerApi.prepare({
|
||||
html: req.body.html,
|
||||
fetchHTML: false
|
||||
}, (err, email) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
res.send(email.text.replace(/%5B/g, '[').replace(/%5D/g, ']'));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
192
routes/fields.js
192
routes/fields.js
|
@ -1,192 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let lists = require('../lib/models/lists');
|
||||
let fields = require('../lib/models/fields');
|
||||
let tools = require('../lib/tools');
|
||||
let passport = require('../lib/passport');
|
||||
let _ = require('../lib/translate')._;
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
res.setSelectedMenu('lists');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/:list', (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, rows) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
res.render('lists/fields/fields', {
|
||||
rows: rows.map(row => {
|
||||
row.index = ++index;
|
||||
row.type = fields.types[row.type];
|
||||
if (Array.isArray(row.options)) {
|
||||
row.options.forEach(option => {
|
||||
option.index = ++index;
|
||||
});
|
||||
}
|
||||
return row;
|
||||
}),
|
||||
list
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/create', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, rows) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
let data = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
data.csrfToken = req.csrfToken();
|
||||
data.list = list;
|
||||
|
||||
if (data.type) {
|
||||
data['selected' + (data.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())] = true;
|
||||
}
|
||||
|
||||
if (!('visible' in data) && !data.name) {
|
||||
data.visible = true;
|
||||
}
|
||||
|
||||
data.groups = rows.filter(row => fields.grouped.indexOf(row.type) >= 0).map(row => {
|
||||
row.selected = Number(req.query.group) === row.id;
|
||||
return row;
|
||||
});
|
||||
|
||||
res.render('lists/fields/create', data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/create', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
fields.create(req.params.list, req.body, (err, id) => {
|
||||
if (err || !id) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create custom field'));
|
||||
return res.redirect('/fields/' + encodeURIComponent(req.params.list) + '/create?' + tools.queryParams(req.body));
|
||||
}
|
||||
req.flash('success', 'Custom field created');
|
||||
res.redirect('/fields/' + encodeURIComponent(req.params.list));
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/edit/:field', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
fields.get(req.params.field, (err, field) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
if (!field) {
|
||||
req.flash('danger', _('Selected field not found'));
|
||||
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
let data = {
|
||||
csrfToken: req.csrfToken(),
|
||||
field,
|
||||
list
|
||||
};
|
||||
|
||||
if (field.type) {
|
||||
data['selected' + (field.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())] = true;
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, rows) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
data.groups = field.type === 'option' ? rows.filter(row => fields.grouped.indexOf(row.type) >= 0).map(row => {
|
||||
row.selected = Number(field.group) === row.id;
|
||||
return row;
|
||||
}) : false;
|
||||
|
||||
res.render('lists/fields/edit', data);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
fields.update(req.body.id, req.body, (err, updated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
} else if (updated) {
|
||||
req.flash('success', _('Field settings updated'));
|
||||
} else {
|
||||
req.flash('info', _('Field settings not updated'));
|
||||
}
|
||||
|
||||
if (req.body.id) {
|
||||
return res.redirect('/fields/' + encodeURIComponent(req.params.list) + '/edit/' + encodeURIComponent(req.body.id));
|
||||
} else {
|
||||
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
fields.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Custom field deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified field'));
|
||||
}
|
||||
|
||||
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -1,336 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let passport = require('../lib/passport');
|
||||
let tools = require('../lib/tools');
|
||||
let helpers = require('../lib/helpers');
|
||||
let _ = require('../lib/translate')._;
|
||||
let lists = require('../lib/models/lists');
|
||||
let fields = require('../lib/models/fields');
|
||||
let forms = require('../lib/models/forms');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
res.setSelectedMenu('lists');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/:list', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
forms.list(list.id, (err, rows) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
res.render('lists/forms/forms', {
|
||||
customForms: rows.map(row => {
|
||||
row.index = ++index;
|
||||
row.isDefaultForm = list.defaultForm === row.id;
|
||||
return row;
|
||||
}),
|
||||
list,
|
||||
csrfToken: req.csrfToken()
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/create', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
let data = {};
|
||||
data.csrfToken = req.csrfToken();
|
||||
data.list = list;
|
||||
|
||||
res.render('lists/forms/create', data);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/create', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
forms.create(req.params.list, req.body, (err, id) => {
|
||||
if (err || !id) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create custom form'));
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list) + '/create?' + tools.queryParams(req.body));
|
||||
}
|
||||
req.flash('success', 'Custom form created');
|
||||
res.redirect('/forms/' + encodeURIComponent(req.params.list) + '/edit/' + id);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/edit/:form', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
forms.get(req.params.form, (err, form) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
if (!form) {
|
||||
req.flash('danger', _('Selected form not found'));
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, rows) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
let customFields = rows.map(row => {
|
||||
row.type = fields.types[row.type];
|
||||
return row;
|
||||
});
|
||||
|
||||
let allFields = helpers.filterCustomFields(customFields, [], 'exclude');
|
||||
let fieldsShownOnSubscribe = allFields;
|
||||
let fieldsHiddenOnSubscribe = [];
|
||||
let fieldsShownOnManage = allFields;
|
||||
let fieldsHiddenOnManage = [];
|
||||
|
||||
if (form.fieldsShownOnSubscribe) {
|
||||
fieldsShownOnSubscribe = helpers.filterCustomFields(customFields, form.fieldsShownOnSubscribe, 'include');
|
||||
fieldsHiddenOnSubscribe = helpers.filterCustomFields(customFields, form.fieldsShownOnSubscribe, 'exclude');
|
||||
}
|
||||
|
||||
if (form.fieldsShownOnManage) {
|
||||
fieldsShownOnManage = helpers.filterCustomFields(customFields, form.fieldsShownOnManage, 'include');
|
||||
fieldsHiddenOnManage = helpers.filterCustomFields(customFields, form.fieldsShownOnManage, 'exclude');
|
||||
}
|
||||
|
||||
let helpEmailText = _('The plaintext version for this email');
|
||||
let helpMjmlBase = _('Custom forms use MJML for formatting');
|
||||
let helpMjmlDocLink = _('See the MJML documentation <a class="mjml-documentation">here</a>');
|
||||
let helpMjmlGeneral = helpMjmlBase + ' ' + helpMjmlDocLink;
|
||||
|
||||
let templateOptgroups = [
|
||||
{
|
||||
label: _('General'),
|
||||
opts: [{
|
||||
name: 'layout',
|
||||
label: _('Layout'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral,
|
||||
isLayout: true
|
||||
}, {
|
||||
name: 'form_input_style',
|
||||
label: _('Form Input Style'),
|
||||
type: 'css',
|
||||
help: _('This CSS stylesheet defines the appearance of form input elements and alerts')
|
||||
}]
|
||||
}, {
|
||||
label: _('Subscribe'),
|
||||
opts: [{
|
||||
name: 'web_subscribe',
|
||||
label: _('Web - Subscribe'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'web_confirm_subscription_notice',
|
||||
label: _('Web - Confirm Subscription Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_confirm_subscription_html',
|
||||
label: _('Mail - Confirm Subscription (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_confirm_subscription_text',
|
||||
label: _('Mail - Confirm Subscription (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'mail_already_subscribed_html',
|
||||
label: _('Mail - Already Subscribed (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_already_subscribed_text',
|
||||
label: _('Mail - Already Subscribed (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'web_subscribed_notice',
|
||||
label: _('Web - Subscribed Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_subscription_confirmed_html',
|
||||
label: _('Mail - Subscription Confirmed (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_subscription_confirmed_text',
|
||||
label: _('Mail - Subscription Confirmed (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}]
|
||||
}, {
|
||||
label: _('Manage'),
|
||||
opts: [{
|
||||
name: 'web_manage',
|
||||
label: _('Web - Manage Preferences'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'web_manage_address',
|
||||
label: _('Web - Manage Address'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'web_updated_notice',
|
||||
label: _('Web - Updated Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}]
|
||||
}, {
|
||||
label: _('Unsubscribe'),
|
||||
opts: [{
|
||||
name: 'web_unsubscribe',
|
||||
label: _('Web - Unsubscribe'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'web_confirm_unsubscription_notice',
|
||||
label: _('Web - Confirm Unsubscription Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_confirm_unsubscription_html',
|
||||
label: _('Mail - Confirm Unsubscription (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_confirm_unsubscription_text',
|
||||
label: _('Mail - Confirm Unsubscription (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'mail_confirm_address_change_html',
|
||||
label: _('Mail - Confirm Address Change (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_confirm_address_change_text',
|
||||
label: _('Mail - Confirm Address Change (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'web_unsubscribed_notice',
|
||||
label: _('Web - Unsubscribed Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_unsubscription_confirmed_html',
|
||||
label: _('Mail - Unsubscription Confirmed (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_unsubscription_confirmed_text',
|
||||
label: _('Mail - Unsubscription Confirmed (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'web_manual_unsubscribe_notice',
|
||||
label: _('Web - Manual Unsubscribe Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}]
|
||||
}
|
||||
];
|
||||
|
||||
templateOptgroups.forEach(group => {
|
||||
group.opts.forEach(opt => {
|
||||
let key = tools.fromDbKey(opt.name);
|
||||
opt.value = form[key];
|
||||
});
|
||||
});
|
||||
|
||||
subscriptions.listTestUsers(list.id, (err, testUsers) => {
|
||||
res.render('lists/forms/edit', {
|
||||
csrfToken: req.csrfToken(),
|
||||
list,
|
||||
form,
|
||||
templateOptgroups,
|
||||
fieldsShownOnSubscribe,
|
||||
fieldsHiddenOnSubscribe,
|
||||
fieldsShownOnManage,
|
||||
fieldsHiddenOnManage,
|
||||
testUsers,
|
||||
useEditor: true
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
forms.update(req.body.id, req.body, (err, updated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
} else if (updated) {
|
||||
req.flash('success', _('Form settings updated'));
|
||||
} else {
|
||||
req.flash('info', _('Form settings not updated'));
|
||||
}
|
||||
|
||||
if (req.body.id) {
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list) + '/edit/' + encodeURIComponent(req.body.id));
|
||||
} else {
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
forms.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Custom form deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified form'));
|
||||
}
|
||||
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -1,75 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nodeifyFunction } = require('../lib/nodeify');
|
||||
const getSettings = nodeifyFunction(require('../models/settings').get);
|
||||
|
||||
const config = require('config');
|
||||
const express = require('express');
|
||||
const router = new express.Router();
|
||||
const passport = require('../lib/passport');
|
||||
const _ = require('../lib/translate')._;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const editorHelpers = require('../lib/editor-helpers')
|
||||
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/editor', passport.csrfProtection, (req, res) => {
|
||||
getSettings('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);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
try {
|
||||
resource.editorData = JSON.parse(resource.editorData);
|
||||
} catch (err) {
|
||||
resource.editorData = {
|
||||
template: req.query.template || 'demo'
|
||||
}
|
||||
}
|
||||
|
||||
if (!resource.html && !resource.editorData.html && !resource.editorData.mjml) {
|
||||
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) {
|
||||
try {
|
||||
resource.html = fs.readFileSync(path.join(base, 'index.html'), 'utf8');
|
||||
} catch (err) {
|
||||
resource.html = err.message || err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -1,15 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let _ = require('../lib/translate')._;
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
indexPage: true,
|
||||
title: _('Self Hosted Newsletter App')
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
111
routes/links.js
111
routes/links.js
|
@ -1,111 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nodeifyFunction } = require('../lib/nodeify');
|
||||
const getSettings = nodeifyFunction(require('../models/settings').get);
|
||||
|
||||
let links = require('../lib/models/links');
|
||||
let lists = require('../lib/models/lists');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
let tools = require('../lib/tools');
|
||||
let _ = require('../lib/translate')._;
|
||||
|
||||
let log = require('npmlog');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
|
||||
let trackImg = new Buffer('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
|
||||
|
||||
router.get('/:campaign/:list/:subscription', (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'image/gif',
|
||||
'Content-Length': trackImg.length
|
||||
});
|
||||
links.countOpen(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, (err, opened) => {
|
||||
if (err) {
|
||||
log.error('Redirect', err);
|
||||
}
|
||||
if (opened) {
|
||||
log.verbose('Redirect', 'First open for %s:%s:%s', req.params.campaign, req.params.list, req.params.subscription);
|
||||
}
|
||||
});
|
||||
|
||||
res.end(trackImg);
|
||||
});
|
||||
|
||||
router.get('/:campaign/:list/:subscription/:link', (req, res) => {
|
||||
|
||||
let notFound = () => {
|
||||
res.status(404);
|
||||
return res.render('archive/view', {
|
||||
layout: 'archive/layout',
|
||||
message: _('Oops, we couldn\'t find a link for the URL you clicked'),
|
||||
campaign: {
|
||||
subject: 'Error 404'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
links.resolve(req.params.link, (err, linkId, url) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
if (!linkId || !url) {
|
||||
log.error('Redirect', 'Unresolved URL: <%s>', req.url);
|
||||
return notFound();
|
||||
}
|
||||
links.countClick(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, linkId, (err, status) => {
|
||||
if (err) {
|
||||
log.error('Redirect', err);
|
||||
}
|
||||
if (status) {
|
||||
log.verbose('Redirect', 'First click for %s:%s:%s (%s)', req.params.campaign, req.params.list, req.params.subscription, url);
|
||||
}
|
||||
});
|
||||
|
||||
if (!/\[[^\]]+\]/.test(url)) {
|
||||
// no special tags, just pass on the link
|
||||
return res.redirect(url);
|
||||
}
|
||||
|
||||
// url might include variables, need to rewrite those just as we do with message content
|
||||
lists.getByCid(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
log.error('Redirect', 'Could not resolve list for merge tags: <%s>', req.url);
|
||||
return notFound();
|
||||
}
|
||||
|
||||
getSettings('serviceUrl', (err, serviceUrl) => {
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
serviceUrl = (serviceUrl || '').toString().trim();
|
||||
|
||||
subscriptions.getWithMergeTags(list.id, req.params.subscription, (err, subscription) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
log.error('Redirect', 'Could not resolve subscription for merge tags: <%s>', req.url);
|
||||
return notFound();
|
||||
}
|
||||
|
||||
url = tools.formatMessage(serviceUrl, {
|
||||
cid: req.params.campaign
|
||||
}, list, subscription, url, str => encodeURIComponent(str));
|
||||
|
||||
res.redirect(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -1,10 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
clientHelpers.registerRootRoute(router, 'lists', _('Lists'));
|
||||
|
||||
module.exports = router;
|
|
@ -1,839 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let config = require('config');
|
||||
let openpgp = require('openpgp');
|
||||
let passport = require('../lib/passport');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let lists = require('../lib/models/lists');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
let fields = require('../lib/models/fields');
|
||||
let forms = require('../lib/models/forms');
|
||||
let tools = require('../lib/tools');
|
||||
let striptags = require('striptags');
|
||||
let htmlescape = require('escape-html');
|
||||
let multer = require('multer');
|
||||
let os = require('os');
|
||||
let humanize = require('humanize');
|
||||
let mkdirp = require('mkdirp');
|
||||
let pathlib = require('path');
|
||||
let log = require('npmlog');
|
||||
let _ = require('../lib/translate')._;
|
||||
let util = require('util');
|
||||
|
||||
let uploadStorage = multer.diskStorage({
|
||||
destination: (req, file, callback) => {
|
||||
log.verbose('tmpdir', os.tmpdir());
|
||||
let tmp = config.www.tmpdir || os.tmpdir();
|
||||
let dir = pathlib.join(tmp, 'mailtrain');
|
||||
mkdirp(dir, err => {
|
||||
if (err) {
|
||||
log.error('Upload', err);
|
||||
log.verbose('Upload', 'Storing upload to <%s>', tmp);
|
||||
return callback(null, tmp);
|
||||
}
|
||||
log.verbose('Upload', 'Storing upload to <%s>', dir);
|
||||
callback(null, dir);
|
||||
});
|
||||
}
|
||||
});
|
||||
let uploads = multer({
|
||||
storage: uploadStorage
|
||||
});
|
||||
|
||||
let csvparse = require('csv-parse');
|
||||
let fs = require('fs');
|
||||
let moment = require('moment-timezone');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
res.setSelectedMenu('lists');
|
||||
next();
|
||||
});
|
||||
|
||||
/* REPLACED
|
||||
router.get('/', (req, res) => {
|
||||
res.render('lists/lists', {
|
||||
title: _('Lists')
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
/* REPLACED
|
||||
router.get('/create', passport.csrfProtection, (req, res) => {
|
||||
let data = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
data.csrfToken = req.csrfToken();
|
||||
|
||||
if (!('publicSubscribe' in data)) {
|
||||
data.publicSubscribe = true;
|
||||
}
|
||||
|
||||
data.unsubscriptionModeOptions = getUnsubscriptionModeOptions(data.unsubscriptionMode || lists.UnsubscriptionMode.ONE_STEP);
|
||||
|
||||
res.render('lists/create', data);
|
||||
});
|
||||
*/
|
||||
|
||||
/* REPLACED
|
||||
router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
lists.create(req.body, (err, id) => {
|
||||
if (err || !id) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create list'));
|
||||
return res.redirect('/lists/create?' + tools.queryParams(req.body));
|
||||
}
|
||||
req.flash('success', _('List created'));
|
||||
res.redirect('/lists/view/' + id);
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
/* REPLACED
|
||||
router.get('/edit/:id', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.id, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
forms.list(list.id, (err, customForms) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
list.customForms = customForms.map(row => {
|
||||
row.selected = list.defaultForm === row.id;
|
||||
return row;
|
||||
});
|
||||
|
||||
list.unsubscriptionModeOptions = getUnsubscriptionModeOptions(list.unsubscriptionMode);
|
||||
|
||||
list.csrfToken = req.csrfToken();
|
||||
res.render('lists/edit', list);
|
||||
});
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
/* REPLACED
|
||||
router.post('/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
lists.update(req.body.id, req.body, (err, updated) => {
|
||||
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
} else if (updated) {
|
||||
req.flash('success', _('List settings updated'));
|
||||
} else {
|
||||
req.flash('info', _('List settings not updated'));
|
||||
}
|
||||
|
||||
if (req.query.next) {
|
||||
return res.redirect(req.query.next);
|
||||
} else if (req.body.id) {
|
||||
return res.redirect('/lists/edit/' + encodeURIComponent(req.body.id));
|
||||
} else {
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
/* REPLACED
|
||||
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
lists.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('List deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified list'));
|
||||
}
|
||||
|
||||
return res.redirect('/lists');
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
router.post('/ajax', (req, res) => {
|
||||
lists.filter(req.body, Number(req.query.parent) || false, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
(Number(req.body.start) || 0) + 1 + i,
|
||||
'<span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> <a href="/lists/view/' + row.id + '">' + htmlescape(row.name || '') + '</a>',
|
||||
'<code>' + row.cid + '</code>',
|
||||
row.subscribers,
|
||||
htmlescape(striptags(row.description) || ''),
|
||||
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/edit/' + row.id + '">' + _('Edit') + '</a>' ]
|
||||
)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.post('/ajax/:id', (req, res) => {
|
||||
lists.get(req.params.id, (err, list) => {
|
||||
if (err || !list) {
|
||||
return res.json({
|
||||
error: err && err.message || err || _('List not found'),
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
let columns = ['#', 'email', 'first_name', 'last_name'].concat(fieldList.filter(field => field.visible).map(field => field.column)).concat(['status', 'created']);
|
||||
|
||||
subscriptions.filter(list.id, req.body, columns, req.query.segment, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
data.forEach(row => {
|
||||
row.subscriptionStatus = row.status === 1 ? true : false;
|
||||
row.customFields = fields.getRow(fieldList, row);
|
||||
});
|
||||
|
||||
let statuses = [_('Unknown'), _('Subscribed'), _('Unsubscribed'), _('Bounced'), _('Complained')];
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
(Number(req.body.start) || 0) + 1 + i,
|
||||
htmlescape(row.email || ''),
|
||||
htmlescape(row.firstName || ''),
|
||||
htmlescape(row.lastName || '')
|
||||
].concat(fields.getRow(fieldList, row).map(cRow => {
|
||||
if (cRow.type === 'number') {
|
||||
return htmlescape(cRow.value && humanize.numberFormat(cRow.value, 0) || '');
|
||||
} else if (cRow.type === 'longtext') {
|
||||
let value = (cRow.value || '');
|
||||
if (value.length > 50) {
|
||||
value = value.substr(0, 47).trim() + '…';
|
||||
}
|
||||
return htmlescape(value);
|
||||
} else if (cRow.type === 'gpg') {
|
||||
let value = (cRow.value || '').trim();
|
||||
try {
|
||||
value = openpgp.key.readArmored(value);
|
||||
if (value) {
|
||||
|
||||
let keys = value.keys;
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let key = keys[i];
|
||||
switch (key.verifyPrimaryKey()) {
|
||||
case 0:
|
||||
return _('Invalid key');
|
||||
case 1:
|
||||
return _('Expired key');
|
||||
case 2:
|
||||
return _('Revoked key');
|
||||
}
|
||||
}
|
||||
|
||||
value = value.keys && value.keys[0] && value.keys[0].primaryKey.fingerprint;
|
||||
if (value) {
|
||||
value = '0x' + value.substr(-16).toUpperCase();
|
||||
}
|
||||
}
|
||||
} catch (E) {
|
||||
value = 'parse error';
|
||||
}
|
||||
return htmlescape(value || '');
|
||||
} else {
|
||||
return htmlescape(cRow.value || '');
|
||||
}
|
||||
})).concat(statuses[row.status]).concat(row.created && row.created.toISOString ? '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>' : 'N/A').concat('<a href="/lists/subscription/' + list.id + '/edit/' + row.cid + '">' + _('Edit') + '</a>'))
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/view/:id', passport.csrfProtection, (req, res) => {
|
||||
if (Number(req.query.segment) === -1) {
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.id) + '/create');
|
||||
}
|
||||
|
||||
lists.get(req.params.id, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
subscriptions.listImports(list.id, (err, imports) => {
|
||||
if (err) {
|
||||
// not important, ignore
|
||||
imports = [];
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
list.imports = imports.map((entry, i) => {
|
||||
entry.index = i + 1;
|
||||
entry.importType = entry.type === 0 ? _('Subscribe') : (entry.type === 1 ? _('Force Subscribe') : _('Unsubscribe'));
|
||||
switch (entry.status) {
|
||||
case 0:
|
||||
entry.importStatus = _('Initializing');
|
||||
break;
|
||||
case 1:
|
||||
entry.importStatus = _('Initialized');
|
||||
break;
|
||||
case 2:
|
||||
entry.importStatus = _('Importing') + '…';
|
||||
break;
|
||||
case 3:
|
||||
entry.importStatus = _('Finished');
|
||||
break;
|
||||
default:
|
||||
entry.importStatus = _('Errored') + (entry.error ? ' (' + entry.error + ')' : '');
|
||||
entry.error = true;
|
||||
}
|
||||
entry.created = entry.created && entry.created.toISOString();
|
||||
entry.finished = entry.finished && entry.finished.toISOString();
|
||||
entry.updated = entry.processed - entry.new;
|
||||
entry.processed = humanize.numberFormat(entry.processed, 0);
|
||||
return entry;
|
||||
});
|
||||
list.csrfToken = req.csrfToken();
|
||||
list.customFields = fieldList.filter(field => field.visible);
|
||||
list.customSort = list.customFields.length ? ',' + list.customFields.map(() => '0').join(',') : '';
|
||||
|
||||
list.showSubscriptions = req.query.tab === 'subscriptions' || !req.query.tab;
|
||||
list.showImports = req.query.tab === 'imports';
|
||||
|
||||
list.segments.forEach(segment => {
|
||||
if (segment.id === (Number(req.query.segment) || 0)) {
|
||||
segment.selected = true;
|
||||
list.useSegment = req.query.segment;
|
||||
list.segment = segment.id;
|
||||
}
|
||||
});
|
||||
|
||||
res.render('lists/view', list);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/subscription/:id/add', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.id, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
let data = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
data.list = list;
|
||||
data.csrfToken = req.csrfToken();
|
||||
|
||||
data.customFields = fields.getRow(fieldList, data, false, true);
|
||||
data.useEditor = true;
|
||||
|
||||
data.timezones = moment.tz.names().map(tz => {
|
||||
let selected = false;
|
||||
if (tz.toLowerCase().trim() === (data.tz || 'UTC').toLowerCase().trim()) {
|
||||
selected = true;
|
||||
}
|
||||
return {
|
||||
key: tz,
|
||||
value: tz,
|
||||
selected
|
||||
};
|
||||
});
|
||||
|
||||
res.render('lists/subscription/add', data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/subscription/:id/edit/:cid', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.id, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
subscriptions.get(list.id, req.params.cid, (err, subscription) => {
|
||||
if (err || !subscription) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find subscriber with specified ID'));
|
||||
return res.redirect('/lists/view/' + req.params.id);
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
subscription.list = list;
|
||||
subscription.csrfToken = req.csrfToken();
|
||||
|
||||
subscription.customFields = fields.getRow(fieldList, subscription, false, true);
|
||||
subscription.useEditor = true;
|
||||
subscription.isSubscribed = subscription.status === 1;
|
||||
|
||||
let tzfound = false;
|
||||
subscription.timezones = moment.tz.names().map(tz => {
|
||||
let selected = false;
|
||||
if (tz.toLowerCase().trim() === (subscription.tz || '').toLowerCase().trim()) {
|
||||
selected = true;
|
||||
tzfound = true;
|
||||
}
|
||||
return {
|
||||
key: tz,
|
||||
value: tz,
|
||||
selected
|
||||
};
|
||||
});
|
||||
if (!tzfound && subscription.tz) {
|
||||
subscription.timezones.push({
|
||||
key: subscription.tz,
|
||||
value: subscription.tz,
|
||||
selected: true
|
||||
});
|
||||
}
|
||||
|
||||
res.render('lists/subscription/edit', subscription);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/subscription/add', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
subscriptions.insert(req.body.list, false, req.body, (err, response) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err || _('Could not add subscription'));
|
||||
return res.redirect('/lists/subscription/' + encodeURIComponent(req.body.list) + '/add?' + tools.queryParams(req.body));
|
||||
}
|
||||
|
||||
if (response.entryId) {
|
||||
req.flash('success', util.format(_('%s was successfully added to your list'), req.body.email));
|
||||
} else {
|
||||
req.flash('warning', util.format(_('%s was not added to your list'), req.body.email));
|
||||
}
|
||||
|
||||
res.redirect('/lists/subscription/' + encodeURIComponent(req.body.list) + '/add');
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/subscription/unsubscribe', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.body.list, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
subscriptions.get(list.id, req.body.cid, (err, subscription) => {
|
||||
if (err || !subscription) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find subscriber with specified ID'));
|
||||
return res.redirect('/lists/view/' + list.id);
|
||||
}
|
||||
|
||||
subscriptions.changeStatus(list.id, subscription.id, false, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err || _('Could not unsubscribe user'));
|
||||
return res.redirect('/lists/subscription/' + list.id + '/edit/' + subscription.cid);
|
||||
}
|
||||
req.flash('success', util.format(_('%s was successfully unsubscribed from your list'), subscription.email));
|
||||
res.redirect('/lists/view/' + list.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/subscription/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.body.list, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
subscriptions.delete(list.id, req.body.cid, (err, email) => {
|
||||
if (err || !email) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find subscriber with specified ID'));
|
||||
return res.redirect('/lists/view/' + list.id);
|
||||
}
|
||||
|
||||
req.flash('success', util.format(_('%s was successfully removed from your list'), email));
|
||||
res.redirect('/lists/view/' + list.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/subscription/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
req.body['is-test'] = req.body['is-test'] ? '1' : '0';
|
||||
subscriptions.update(req.body.list, req.body.cid, req.body, true, (err, updated) => {
|
||||
|
||||
if (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
req.flash('danger', util.format(_('Another subscriber with email address %s already exists'), req.body.email));
|
||||
return res.redirect('/lists/subscription/' + encodeURIComponent(req.body.list) + '/edit/' + req.body.cid);
|
||||
} else {
|
||||
req.flash('danger', err.message || err);
|
||||
}
|
||||
|
||||
} else if (updated) {
|
||||
req.flash('success', _('Subscription settings updated'));
|
||||
} else {
|
||||
req.flash('info', _('Subscription settings not updated'));
|
||||
}
|
||||
|
||||
if (req.body.list) {
|
||||
return res.redirect('/lists/view/' + encodeURIComponent(req.body.list));
|
||||
} else {
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/subscription/:id/import', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.id, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
let data = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
if (!('delimiter' in data)) {
|
||||
data.delimiter = ',';
|
||||
}
|
||||
|
||||
data.list = list;
|
||||
data.csrfToken = req.csrfToken();
|
||||
|
||||
res.render('lists/subscription/import', data);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/subscription/:id/import/:importId', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.id, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
subscriptions.getImport(req.params.id, req.params.importId, (err, data) => {
|
||||
if (err || !data) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find import data with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
data.list = list;
|
||||
data.csrfToken = req.csrfToken();
|
||||
|
||||
data.customFields = fields.getRow(fieldList, data, false, true);
|
||||
|
||||
res.render('lists/subscription/import-preview', data);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/subscription/import', uploads.single('listimport'), passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.body.list, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
let delimiter = (req.body.delimiter || '').trim().charAt(0) || ',';
|
||||
|
||||
getPreview(req.file.path, req.file.size, delimiter, (err, rows) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err || _('Could not process CSV'));
|
||||
return res.redirect('/lists');
|
||||
} else {
|
||||
|
||||
let type = 0; // Use the existing subscription status or SUBSCRIBED
|
||||
if (req.body.type === 'force_subscribed') {
|
||||
type = subscriptions.Status.SUBSCRIBED;
|
||||
} else if (req.body.type === 'unsubscribed') {
|
||||
type = subscriptions.Status.UNSUBSCRIBED;
|
||||
}
|
||||
|
||||
subscriptions.createImport(list.id, type, req.file.path, req.file.size, delimiter, req.body.emailcheck === 'enabled' ? 1 : 0, {
|
||||
columns: rows[0],
|
||||
example: rows[1] || []
|
||||
}, (err, importId) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create importer'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
return res.redirect('/lists/subscription/' + list.id + '/import/' + importId);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getPreview(path, size, delimiter, callback) {
|
||||
delimiter = (delimiter || '').trim().charAt(0) || ',';
|
||||
size = Number(size);
|
||||
|
||||
fs.open(path, 'r', (err, fd) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let bufLen = size;
|
||||
let maxReadSize = 10 * 1024;
|
||||
|
||||
if (size > maxReadSize) {
|
||||
bufLen = maxReadSize;
|
||||
}
|
||||
|
||||
let buffer = new Buffer(bufLen);
|
||||
fs.read(fd, buffer, 0, buffer.length, 0, (err, bytesRead, buffer) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let input = buffer.toString().trim();
|
||||
|
||||
|
||||
if (size !== bufLen) {
|
||||
// remove last incomplete line
|
||||
input = input.split(/\r?\n/);
|
||||
input.pop();
|
||||
input = input.join('\n');
|
||||
}
|
||||
|
||||
csvparse(input, {
|
||||
comment: '#',
|
||||
delimiter
|
||||
}, (err, data) => {
|
||||
fs.close(fd, () => {
|
||||
// just ignore
|
||||
});
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!data || !data.length) {
|
||||
return callback(new Error(_('Empty file')));
|
||||
}
|
||||
if (data.length < 2) {
|
||||
return callback(new Error(_('Too few rows')));
|
||||
}
|
||||
callback(err, data);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
router.post('/subscription/import-confirm', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.body.list, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
subscriptions.getImport(list.id, req.body.import, (err, data) => {
|
||||
if (err || !data) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find import data with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
let allowedColumns = ['email', 'first_name', 'last_name', 'tz'];
|
||||
fieldList.forEach(field => {
|
||||
if (field.column) {
|
||||
allowedColumns.push(field.column);
|
||||
}
|
||||
if (field.options) {
|
||||
field.options.forEach(subField => {
|
||||
if (subField.column) {
|
||||
allowedColumns.push(subField.column);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
data.mapping.mapping = {};
|
||||
data.mapping.columns.forEach((column, i) => {
|
||||
let colIndex = allowedColumns.indexOf(req.body['column-' + i]);
|
||||
if (colIndex >= 0) {
|
||||
data.mapping.mapping[allowedColumns[colIndex]] = i;
|
||||
}
|
||||
});
|
||||
|
||||
subscriptions.updateImport(list.id, req.body.import, {
|
||||
status: 1,
|
||||
mapping: JSON.stringify(data.mapping)
|
||||
}, (err, importer) => {
|
||||
if (err || !importer) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find import data with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
req.flash('success', _('Import started'));
|
||||
res.redirect('/lists/view/' + list.id + '?tab=imports');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/subscription/import-restart', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.body.list, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
subscriptions.updateImport(list.id, req.body.import, {
|
||||
status: 1,
|
||||
error: null,
|
||||
finished: null,
|
||||
processed: 0,
|
||||
new: 0,
|
||||
failed: 0
|
||||
}, (err, importer) => {
|
||||
if (err || !importer) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find import data with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
req.flash('success', _('Import restarted'));
|
||||
res.redirect('/lists/view/' + list.id + '?tab=imports');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/subscription/:id/import/:importId/failed', (req, res) => {
|
||||
let start = 0;
|
||||
lists.get(req.params.id, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
subscriptions.getImport(req.params.id, req.params.importId, (err, data) => {
|
||||
if (err || !data) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find import data with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
subscriptions.getFailedImports(req.params.importId, (err, rows) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
data.rows = rows.map((row, i) => {
|
||||
row.index = start + i + 1;
|
||||
return row;
|
||||
});
|
||||
data.list = list;
|
||||
|
||||
res.render('lists/subscription/import-failed', data);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/quicklist/ajax', (req, res) => {
|
||||
lists.filterQuicklist(req.body, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => ({
|
||||
"0": (Number(req.body.start) || 0) + 1 + i,
|
||||
"1": '<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> <a href="/lists/view/' + row.id + '">' + htmlescape(row.name || '') + '</a>',
|
||||
"2": row.subscribers,
|
||||
"DT_RowId": row.id
|
||||
}))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getUnsubscriptionModeOptions(unsubscriptionMode) {
|
||||
const options = [];
|
||||
|
||||
options[lists.UnsubscriptionMode.ONE_STEP] = {
|
||||
value: lists.UnsubscriptionMode.ONE_STEP,
|
||||
selected: unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP,
|
||||
label: _('One-step (i.e. no email with confirmation link)')
|
||||
};
|
||||
|
||||
options[lists.UnsubscriptionMode.ONE_STEP_WITH_FORM] = {
|
||||
value: lists.UnsubscriptionMode.ONE_STEP_WITH_FORM,
|
||||
selected: unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM,
|
||||
label: _('One-step with unsubscription form (i.e. no email with confirmation link)')
|
||||
};
|
||||
|
||||
options[lists.UnsubscriptionMode.TWO_STEP] = {
|
||||
value: lists.UnsubscriptionMode.TWO_STEP,
|
||||
selected: unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP,
|
||||
label: _('Two-step (i.e. an email with confirmation link will be sent)')
|
||||
};
|
||||
|
||||
options[lists.UnsubscriptionMode.TWO_STEP_WITH_FORM] = {
|
||||
value: lists.UnsubscriptionMode.TWO_STEP_WITH_FORM,
|
||||
selected: unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM,
|
||||
label: _('Two-step with unsubscription form (i.e. an email with confirmation link will be sent)')
|
||||
};
|
||||
|
||||
options[lists.UnsubscriptionMode.MANUAL] = {
|
||||
value: lists.UnsubscriptionMode.MANUAL,
|
||||
selected: unsubscriptionMode === lists.UnsubscriptionMode.MANUAL,
|
||||
label: _('Manual (i.e. unsubscription has to be performed by the list administrator)')
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
module.exports = router;
|
|
@ -1,57 +1,49 @@
|
|||
'use strict';
|
||||
|
||||
let config = require('config');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let passport = require('../lib/passport');
|
||||
let fs = require('fs');
|
||||
let path = require('path');
|
||||
let _ = require('../lib/translate')._;
|
||||
let editorHelpers = require('../lib/editor-helpers');
|
||||
const config = require('config');
|
||||
const router = require('../lib/router-async').create();
|
||||
const passport = require('../lib/passport');
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
const bluebird = require('bluebird');
|
||||
const fsReadFile = bluebird.promisify(require('fs').readFile);
|
||||
|
||||
const path = require('path');
|
||||
|
||||
// FIXME - add authentication by sandboxToken
|
||||
|
||||
|
||||
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||
const resourceType = req.query.type;
|
||||
const resourceId = req.query.id;
|
||||
|
||||
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context);
|
||||
|
||||
let languageStrings = null;
|
||||
if (config.language && config.language !== 'en') {
|
||||
const lang = config.language.split('_')[0];
|
||||
try {
|
||||
const file = path.join(__dirname, '..', 'client', 'public', 'mosaico', 'lang', 'mosaico-' + lang + '.json');
|
||||
languageStrings = await fsReadFile(file, 'utf8');
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/editor', passport.csrfProtection, (req, res) => {
|
||||
editorHelpers.getResource(req.query.type, req.query.id, (err, resource) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
let getLanguageStrings = language => {
|
||||
if (!language || language === 'en') {
|
||||
return null;
|
||||
}
|
||||
language = language.split('_')[0];
|
||||
try {
|
||||
let file = path.join(__dirname, '..', 'public', 'mosaico', 'dist', 'lang', 'mosaico-' + language + '.json');
|
||||
return fs.readFileSync(file, 'utf8');
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ????
|
||||
resource.editorName = resource.editorName || 'mosaico';
|
||||
resource.editorData = !resource.editorData ?
|
||||
{
|
||||
template: req.query.template || 'versafix-1'
|
||||
} :
|
||||
JSON.parse(resource.editorData);
|
||||
*/
|
||||
|
||||
res.render('mosaico/editor', {
|
||||
layout: 'mosaico/layout-editor',
|
||||
type: req.query.type,
|
||||
resource,
|
||||
editorConfig: config.mosaico,
|
||||
languageStrings: getLanguageStrings(config.language),
|
||||
csrfToken: req.csrfToken(),
|
||||
});
|
||||
res.render('mosaico/root', {
|
||||
layout: 'mosaico/layout',
|
||||
editorConfig: config.mosaico,
|
||||
languageStrings: languageStrings,
|
||||
reactCsrfToken: req.csrfToken(),
|
||||
mailtrainConfig: JSON.stringify(mailtrainConfig)
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
clientHelpers.registerRootRoute(router, 'namespaces', _('Namespaces'));
|
||||
|
||||
module.exports = router;
|
|
@ -1,10 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
clientHelpers.registerRootRoute(router, 'reports', _('Reports'));
|
||||
|
||||
module.exports = router;
|
|
@ -42,7 +42,7 @@ router.postAsync('/access-token-reset', passport.loggedIn, passport.csrfProtecti
|
|||
|
||||
|
||||
router.post('/login', passport.csrfProtection, passport.restLogin);
|
||||
router.post('/logout', passport.csrfProtection, passport.restLogout); // TODO - this endpoint is currently not in use. It will become relevant once we switch to SPA
|
||||
router.post('/logout', passport.csrfProtection, passport.restLogout);
|
||||
|
||||
router.postAsync('/password-reset-send', passport.csrfProtection, async (req, res) => {
|
||||
await users.sendPasswordReset(req.body.usernameOrEmail);
|
||||
|
|
22
routes/root.js
Normal file
22
routes/root.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../lib/passport');
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
router.getAsync('/*', passport.csrfProtection, async (req, res) => {
|
||||
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context);
|
||||
if (req.user) {
|
||||
Object.assign(mailtrainConfig, await clientHelpers.getAuthenticatedConfig(req.context));
|
||||
}
|
||||
|
||||
res.render('root', {
|
||||
reactCsrfToken: req.csrfToken(),
|
||||
mailtrainConfig: JSON.stringify(mailtrainConfig)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -1,437 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let passport = require('../lib/passport');
|
||||
let lists = require('../lib/models/lists');
|
||||
let segments = require('../lib/models/segments');
|
||||
let tools = require('../lib/tools');
|
||||
let _ = require('../lib/translate')._;
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
res.setSelectedMenu('lists');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/:list', (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
segments.list(list.id, (err, rows) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
res.render('lists/segments/segments', {
|
||||
rows: rows.map(row => {
|
||||
row.index = ++index;
|
||||
row.type = row.type === 1 ? 'ALL' : 'ANY';
|
||||
return row;
|
||||
}),
|
||||
list
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/create', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
let segment = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
segment.csrfToken = req.csrfToken();
|
||||
segment.list = list;
|
||||
|
||||
switch (Number(segment.type) || 0) {
|
||||
case 1:
|
||||
segment.matchAll = true;
|
||||
break;
|
||||
case 2:
|
||||
segment.matchAny = true;
|
||||
break;
|
||||
}
|
||||
|
||||
res.render('lists/segments/create', segment);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/create', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
segments.create(req.params.list, req.body, (err, id) => {
|
||||
if (err || !id) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create segment'));
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/create?' + tools.queryParams(req.body));
|
||||
}
|
||||
req.flash('success', _('Segment created'));
|
||||
res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + id);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/view/:id', (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
segments.get(req.params.id, (err, segment) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
req.flash('danger', _('Selected segment ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
segment.list = list;
|
||||
segment.rules.forEach((rule, i) => {
|
||||
rule.index = i + 1;
|
||||
});
|
||||
|
||||
switch (Number(segment.type) || 0) {
|
||||
case 1:
|
||||
segment.type = 'ALL';
|
||||
break;
|
||||
case 2:
|
||||
segment.type = 'ANY';
|
||||
break;
|
||||
}
|
||||
|
||||
segments.subscribers(req.params.id, true, (err, subscribers) => {
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
segment.subscribers = subscribers || 0;
|
||||
|
||||
res.render('lists/segments/view', segment);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/edit/:segment', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
segments.get(req.params.segment, (err, segment) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
req.flash('danger', 'Selected segment not found');
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
segment.csrfToken = req.csrfToken();
|
||||
segment.list = list;
|
||||
|
||||
switch (Number(segment.type) || 0) {
|
||||
case 1:
|
||||
segment.matchAll = true;
|
||||
break;
|
||||
case 2:
|
||||
segment.matchAny = true;
|
||||
break;
|
||||
}
|
||||
|
||||
res.render('lists/segments/edit', segment);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
segments.update(req.body.id, req.body, (err, updated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
} else if (updated) {
|
||||
req.flash('success', _('Segment settings updated'));
|
||||
} else {
|
||||
req.flash('info', _('Segment settings not updated'));
|
||||
}
|
||||
|
||||
if (req.body.id) {
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + encodeURIComponent(req.body.id));
|
||||
} else {
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
segments.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Segment deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified segment'));
|
||||
}
|
||||
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/rules/:segment/create', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
segments.get(req.params.segment, (err, segment) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
req.flash('danger', 'Selected segment not found');
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
segment.csrfToken = req.csrfToken();
|
||||
|
||||
segment.list = list;
|
||||
|
||||
res.render('lists/segments/rule-create', segment);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/rules/:segment/next', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
segments.get(req.params.segment, (err, segment) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
req.flash('danger', _('Selected segment not found'));
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
let column = segment.columns.filter(column => column.column === req.body.column).pop();
|
||||
if (!column) {
|
||||
req.flash('danger', _('Invalid rule type'));
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/rules/' + segment.id + '/create?' + tools.queryParams(req.body));
|
||||
}
|
||||
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/rules/' + segment.id + '/configure?' + tools.queryParams(req.body));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/rules/:segment/configure', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
segments.get(req.params.segment, (err, segment) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
req.flash('danger', _('Selected segment not found'));
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
let column = segment.columns.filter(column => column.column === req.query.column).pop();
|
||||
if (!column) {
|
||||
req.flash('danger', _('Invalid rule type'));
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/rules/' + segment.id + '/create?' + tools.queryParams(req.body));
|
||||
}
|
||||
|
||||
let data = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
segment.csrfToken = req.csrfToken();
|
||||
|
||||
segment.value = data;
|
||||
segment.list = list;
|
||||
segment.column = column;
|
||||
segment['columnType' + column.type.replace(/^[a-z]/, c => c.toUpperCase())] = true;
|
||||
|
||||
segment.useEditor = true;
|
||||
|
||||
res.render('lists/segments/rule-configure', segment);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/rules/:segment/create', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
segments.createRule(req.params.segment, req.body, (err, id) => {
|
||||
if (err || !id) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create rule'));
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/rules/' + encodeURIComponent(req.params.segment) + '/configure?' + tools.queryParams(req.body));
|
||||
}
|
||||
req.flash('success', _('Rule created'));
|
||||
res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + encodeURIComponent(req.params.segment));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/rules/:segment/edit/:rule', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
segments.get(req.params.segment, (err, segment) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
req.flash('danger', _('Selected segment not found'));
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
segments.getRule(req.params.rule, (err, rule) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
req.flash('danger', _('Selected segment not found'));
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
let column = segment.columns.filter(column => column.column === rule.column).pop();
|
||||
if (!column) {
|
||||
req.flash('danger', _('Invalid rule type'));
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + segment.id);
|
||||
}
|
||||
|
||||
rule.csrfToken = req.csrfToken();
|
||||
rule.list = list;
|
||||
rule.segment = segment;
|
||||
rule.column = column;
|
||||
rule['columnType' + column.type.replace(/^[a-z]/, c => c.toUpperCase())] = true;
|
||||
|
||||
rule.useEditor = true;
|
||||
|
||||
res.render('lists/segments/rule-edit', rule);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/rules/:segment/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
segments.updateRule(req.body.id, req.body, (err, updated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
} else if (updated) {
|
||||
req.flash('success', _('Rule settings updated'));
|
||||
} else {
|
||||
req.flash('info', _('Rule settings not updated'));
|
||||
}
|
||||
|
||||
if (req.params.segment) {
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + encodeURIComponent(req.params.segment));
|
||||
} else {
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/rules/:segment/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
segments.deleteRule(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Rule deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified rule'));
|
||||
}
|
||||
|
||||
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + encodeURIComponent(req.params.segment));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -1,231 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
// FIXME: This does not work now because of missing lib/models/settings
|
||||
|
||||
let config = require('config');
|
||||
let passport = require('../lib/passport');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let tools = require('../lib/tools');
|
||||
let nodemailer = require('nodemailer');
|
||||
let mailer = require('../lib/mailer');
|
||||
let url = require('url');
|
||||
let multer = require('multer');
|
||||
let upload = multer();
|
||||
let aws = require('aws-sdk');
|
||||
let util = require('util');
|
||||
let _ = require('../lib/translate')._;
|
||||
const senders = require('../lib/senders');
|
||||
|
||||
let settings = require('../lib/models/settings');
|
||||
|
||||
let allowedKeys = ['service_url', 'smtp_hostname', 'smtp_port', 'smtp_encryption', 'smtp_disable_auth', 'smtp_user', 'smtp_pass', 'admin_email', 'smtp_log', 'smtp_max_connections', 'smtp_max_messages', 'smtp_self_signed', 'default_from', 'default_address', 'default_subject', 'default_homepage', 'default_postaddress', 'default_sender', 'verp_hostname', 'verp_use', 'disable_wysiwyg', 'pgp_private_key', 'pgp_passphrase', 'ua_code', 'shoutout', 'disable_confirmations', 'smtp_throttling', 'dkim_api_key', 'dkim_private_key', 'dkim_selector', 'dkim_domain', 'mail_transport', 'ses_key', 'ses_secret', 'ses_region'];
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
res.setSelectedMenu('/settings');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/', passport.csrfProtection, (req, res, next) => {
|
||||
settings.list((err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
configItems.smtpEncryption = [{
|
||||
checked: configItems.smtpEncryption === 'TLS' || !configItems.smtpEncryption,
|
||||
key: 'TLS',
|
||||
value: _('Use TLS'),
|
||||
description: _('usually selected for port 465')
|
||||
}, {
|
||||
checked: configItems.smtpEncryption === 'STARTTLS',
|
||||
key: 'STARTTLS',
|
||||
value: _('Use STARTTLS'),
|
||||
description: _('usually selected for port 587 and 25')
|
||||
}, {
|
||||
checked: configItems.smtpEncryption === 'NONE',
|
||||
key: 'NONE',
|
||||
value: _('Do not use encryption')
|
||||
}];
|
||||
|
||||
configItems.sesRegion = [{
|
||||
checked: configItems.sesRegion === 'us-east-1' || !configItems.sesRegion,
|
||||
key: 'us-east-1',
|
||||
value: 'US-EAST-1'
|
||||
}, {
|
||||
checked: configItems.sesRegion === 'us-west-2',
|
||||
key: 'us-west-2',
|
||||
value: 'US-WEST-2'
|
||||
}, {
|
||||
checked: configItems.sesRegion === 'eu-west-1',
|
||||
key: 'eu-west-1',
|
||||
value: 'EU-WEST-1'
|
||||
}];
|
||||
|
||||
configItems.useSMTP = configItems.mailTransport === 'smtp' || !configItems.mailTransport;
|
||||
configItems.useSES = configItems.mailTransport === 'ses';
|
||||
|
||||
let urlparts = url.parse(configItems.serviceUrl);
|
||||
configItems.verpHostname = configItems.verpHostname || 'bounces.' + (urlparts.hostname || 'localhost');
|
||||
|
||||
configItems.verpEnabled = config.verp.enabled;
|
||||
configItems.csrfToken = req.csrfToken();
|
||||
res.render('settings', configItems);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/update', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
|
||||
let data = tools.convertKeys(req.body);
|
||||
|
||||
let keys = [];
|
||||
let values = [];
|
||||
|
||||
Object.keys(data).forEach(key => {
|
||||
let value = data[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
// ensure trailing slash for service home page
|
||||
if (key === 'service_url' && value && !/\/$/.test(value)) {
|
||||
value = value + '/';
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
// checkboxs are not included in value listing if left unchecked
|
||||
['smtp_log', 'smtp_self_signed', 'smtp_disable_auth', 'verp_use', 'disable_wysiwyg', 'disable_confirmations'].forEach(key => {
|
||||
if (keys.indexOf(key) < 0) {
|
||||
keys.push(key);
|
||||
values.push('');
|
||||
}
|
||||
});
|
||||
|
||||
let i = 0;
|
||||
let storeSettings = () => {
|
||||
if (i >= keys.length) {
|
||||
mailer.update();
|
||||
senders.workers.forEach(worker => {
|
||||
worker.send({
|
||||
reload: true
|
||||
});
|
||||
});
|
||||
req.flash('success', _('Settings updated'));
|
||||
return res.redirect('/settings');
|
||||
}
|
||||
let key = keys[i];
|
||||
let value = values[i];
|
||||
i++;
|
||||
|
||||
settings.set(key, value, err => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
return res.redirect('/settings');
|
||||
}
|
||||
storeSettings();
|
||||
});
|
||||
};
|
||||
|
||||
storeSettings();
|
||||
});
|
||||
|
||||
router.post('/smtp-verify', upload.array(), passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
|
||||
let data = tools.convertKeys(req.body);
|
||||
|
||||
// checkboxs are not included in value listing if left unchecked
|
||||
['smtpLog', 'smtpSelfSigned', 'smtpDisableAuth'].forEach(key => {
|
||||
if (!data.hasOwnProperty(key)) {
|
||||
data[key] = false;
|
||||
} else {
|
||||
data[key] = true;
|
||||
}
|
||||
});
|
||||
|
||||
let transportOptions;
|
||||
if (data.mailTransport === 'smtp') {
|
||||
transportOptions = {
|
||||
host: data.smtpHostname,
|
||||
port: Number(data.smtpPort) || false,
|
||||
secure: data.smtpEncryption === 'TLS',
|
||||
ignoreTLS: data.smtpEncryption === 'NONE',
|
||||
auth: data.smtpDisableAuth ? false : {
|
||||
user: data.smtpUser,
|
||||
pass: data.smtpPass
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: !data.smtpSelfSigned
|
||||
}
|
||||
};
|
||||
} else if (data.mailTransport === 'ses') {
|
||||
transportOptions = {
|
||||
SES: new aws.SES({
|
||||
apiVersion: '2010-12-01',
|
||||
accessKeyId: data.sesKey,
|
||||
secretAccessKey: data.sesSecret,
|
||||
region: data.sesRegion
|
||||
})
|
||||
};
|
||||
} else {
|
||||
return res.json({
|
||||
error: _('Invalid mail transport type')
|
||||
});
|
||||
}
|
||||
|
||||
let transport = nodemailer.createTransport(transportOptions);
|
||||
|
||||
transport.verify(err => {
|
||||
if (err) {
|
||||
let message = '';
|
||||
switch (err.code) {
|
||||
case 'InvalidClientTokenId':
|
||||
message = _('Invalid Access Key');
|
||||
break;
|
||||
case 'SignatureDoesNotMatch':
|
||||
message = _('Invalid AWS credentials');
|
||||
break;
|
||||
case 'ECONNREFUSED':
|
||||
message = _('Connection refused, check hostname and port.');
|
||||
break;
|
||||
case 'ETIMEDOUT':
|
||||
if ((err.message || '').indexOf('Greeting never received') === 0) {
|
||||
if (data.smtpEncryption !== 'TLS') {
|
||||
message = _('Did not receive greeting message from server. This might happen when connecting to a TLS port without using TLS.');
|
||||
} else {
|
||||
message = _('Did not receive greeting message from server.');
|
||||
}
|
||||
} else {
|
||||
message = _('Connection timed out. Check your firewall settings, destination port is probably blocked.');
|
||||
}
|
||||
break;
|
||||
case 'EAUTH':
|
||||
if (/\b5\.7\.0\b/.test(err.message) && data.smtpEncryption !== 'STARTTLS') {
|
||||
message = _('Authentication not accepted, server expects STARTTLS to be used.');
|
||||
} else {
|
||||
message = _('Authentication failed, check username and password.');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
if (!message && err.reason) {
|
||||
message = err.reason;
|
||||
}
|
||||
|
||||
res.json({
|
||||
error: (message || _('Failed Mailer verification.')) + (err.response ? ' ' + util.format(_('Server responded with: "%s"'), err.response) : '')
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
message: _('Mailer settings verified, ready to send some mail!')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -1,10 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
clientHelpers.registerRootRoute(router, 'templates', _('Templates'));
|
||||
|
||||
module.exports = router;
|
|
@ -1,191 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nodeifyFunction } = require('../lib/nodeify');
|
||||
const getSettings = nodeifyFunction(require('../models/settings').get);
|
||||
|
||||
let config = require('config');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let templates = require('../lib/models/templates');
|
||||
let tools = require('../lib/tools');
|
||||
let helpers = require('../lib/helpers');
|
||||
let striptags = require('striptags');
|
||||
let htmlescape = require('escape-html');
|
||||
let passport = require('../lib/passport');
|
||||
let mailer = require('../lib/mailer');
|
||||
let _ = require('../lib/translate')._;
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
res.setSelectedMenu('templates');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.render('templates/templates', {
|
||||
title: _('Templates')
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/create', passport.csrfProtection, (req, res, next) => {
|
||||
let data = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
data.csrfToken = req.csrfToken();
|
||||
data.useEditor = true;
|
||||
|
||||
getSettings(['defaultPostaddress', 'defaultSender', 'disableWysiwyg'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
mailer.getTemplate('emails/stationery-html.hbs', (err, rendererHtml) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
mailer.getTemplate('emails/stationery-text.hbs', (err, rendererText) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.html = data.html || rendererHtml(configItems);
|
||||
data.text = data.text || rendererText(configItems);
|
||||
data.disableWysiwyg = configItems.disableWysiwyg;
|
||||
|
||||
data.editors = config.editors || [
|
||||
['summernote', 'Summernote']
|
||||
];
|
||||
data.editors = data.editors.map(ed => {
|
||||
let editor = {
|
||||
name: ed[0],
|
||||
label: ed[1]
|
||||
};
|
||||
if (config[editor.name] && config[editor.name].templates) {
|
||||
editor.templates = config[editor.name].templates.map(tmpl => ({
|
||||
name: tmpl[0],
|
||||
label: tmpl[1]
|
||||
}));
|
||||
}
|
||||
return editor;
|
||||
});
|
||||
|
||||
res.render('templates/create', data);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
templates.create(req.body, (err, id) => {
|
||||
if (err || !id) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create template'));
|
||||
return res.redirect('/templates/create?' + tools.queryParams(req.body));
|
||||
}
|
||||
req.flash('success', _('Template created'));
|
||||
res.redirect('/templates/edit/' + id);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
|
||||
templates.get(req.params.id, (err, template) => {
|
||||
if (err || !template) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find template with specified ID'));
|
||||
return res.redirect('/templates');
|
||||
}
|
||||
getSettings(['disableWysiwyg'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getDefaultMergeTags((err, defaultMergeTags) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/templates');
|
||||
}
|
||||
|
||||
template.mergeTags = defaultMergeTags;
|
||||
template.csrfToken = req.csrfToken();
|
||||
template.useEditor = true;
|
||||
template.editorName = template.editorName || 'summernote';
|
||||
template.editorConfig = config[template.editorName];
|
||||
template.disableWysiwyg = configItems.disableWysiwyg;
|
||||
res.render('templates/edit', template);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
templates.update(req.body.id, req.body, (err, updated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
} else if (updated) {
|
||||
req.flash('success', _('Template settings updated'));
|
||||
} else {
|
||||
req.flash('info', _('Template settings not updated'));
|
||||
}
|
||||
|
||||
if (req.body.id) {
|
||||
return res.redirect('/templates/edit/' + encodeURIComponent(req.body.id));
|
||||
} else {
|
||||
return res.redirect('/templates');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/duplicate', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
templates.duplicate(req.body.id, (err, duplicated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (duplicated) {
|
||||
req.flash('success', _('Template duplicated'));
|
||||
} else {
|
||||
req.flash('info', _('Could not duplicate specified template'));
|
||||
}
|
||||
return res.redirect('/templates/edit/' + duplicated);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
templates.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Template deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified template'));
|
||||
}
|
||||
|
||||
return res.redirect('/templates');
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/ajax', (req, res) => {
|
||||
templates.filter(req.body, Number(req.query.parent) || false, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
(Number(req.body.start) || 0) + 1 + i,
|
||||
'<span class="glyphicon glyphicon-file" aria-hidden="true"></span> ' + htmlescape(row.name || ''),
|
||||
htmlescape(striptags(row.description) || ''),
|
||||
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/templates/edit/' + row.id + '">' + _('Edit') + '</a>' ]
|
||||
)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -1,308 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let triggers = require('../lib/models/triggers');
|
||||
let campaigns = require('../lib/models/campaigns');
|
||||
let lists = require('../lib/models/lists');
|
||||
let fields = require('../lib/models/fields');
|
||||
let striptags = require('striptags');
|
||||
let passport = require('../lib/passport');
|
||||
let tools = require('../lib/tools');
|
||||
let htmlescape = require('escape-html');
|
||||
let _ = require('../lib/translate')._;
|
||||
let util = require('util');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
res.setSelectedMenu('triggers');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
triggers.list((err, rows) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
res.render('triggers/triggers', {
|
||||
rows: rows.map((row, i) => {
|
||||
row.index = i + 1;
|
||||
row.description = striptags(row.description);
|
||||
return row;
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.get('/create-select', passport.csrfProtection, (req, res, next) => {
|
||||
let data = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
data.csrfToken = req.csrfToken();
|
||||
|
||||
lists.quicklist((err, listItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
data.listItems = listItems;
|
||||
|
||||
res.render('triggers/create-select', data);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/create-select', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
if (!req.body.list) {
|
||||
req.flash('danger', _('Could not find selected list'));
|
||||
return res.redirect('/triggers/create-select');
|
||||
}
|
||||
res.redirect('/triggers/' + encodeURIComponent(req.body.list) + '/create');
|
||||
});
|
||||
|
||||
|
||||
router.get('/:listId/create', passport.csrfProtection, (req, res, next) => {
|
||||
let data = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
||||
data.csrfToken = req.csrfToken();
|
||||
data.days = Math.max(Number(data.days) || 1, 1);
|
||||
|
||||
lists.get(req.params.listId, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find selected list'));
|
||||
return res.redirect('/triggers/create-select');
|
||||
}
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
data.columns = triggers.defaultColumns.concat(fieldList.filter(field => fields.genericTypes[field.type] === 'date')).map(field => ({
|
||||
column: field.column,
|
||||
name: field.name,
|
||||
selected: data.column === field.column
|
||||
}));
|
||||
|
||||
campaigns.list(0, 300, (err, campaignList) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.sourceCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id).map(campaign => ({
|
||||
id: campaign.id,
|
||||
name: campaign.name,
|
||||
selected: Number(data.sourceCampaign) === campaign.id
|
||||
}));
|
||||
|
||||
data.destCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id && campaign.type === 4).map(campaign => ({
|
||||
id: campaign.id,
|
||||
name: campaign.name,
|
||||
selected: Number(data.destCampaign) === campaign.id
|
||||
}));
|
||||
|
||||
data.list = list;
|
||||
data.isSubscription = data.rule === 'subscription' || !data.rule;
|
||||
data.isCampaign = data.rule === 'campaign';
|
||||
|
||||
data.campaignOptions = triggers.defaultCampaignEvents.map(evt => ({
|
||||
option: evt.option,
|
||||
name: evt.name,
|
||||
selected: Number(data.sourceCampaign) === evt.option
|
||||
}));
|
||||
|
||||
data.isSend = true;
|
||||
|
||||
res.render('triggers/create', data);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
triggers.create(req.body, (err, id) => {
|
||||
if (err || !id) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create trigger'));
|
||||
if (req.body.list) {
|
||||
return res.redirect('/triggers/' + encodeURIComponent(req.body.list) + '/create?' + tools.queryParams(req.body));
|
||||
} else {
|
||||
return res.redirect('/triggers');
|
||||
}
|
||||
}
|
||||
req.flash('success', util.format(_('Trigger “%s” created'), req.body.name));
|
||||
res.redirect('/triggers');
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
|
||||
triggers.get(req.params.id, (err, trigger) => {
|
||||
if (err || !trigger) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
return res.redirect('/campaigns');
|
||||
}
|
||||
trigger.csrfToken = req.csrfToken();
|
||||
trigger.days = Math.round(trigger.seconds / (24 * 3600));
|
||||
|
||||
lists.get(trigger.list, (err, list) => {
|
||||
if (err || !list) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find selected list'));
|
||||
return res.redirect('/triggers');
|
||||
}
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
campaigns.list(0, 300, (err, campaignList) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
trigger.sourceCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id).map(campaign => ({
|
||||
id: campaign.id,
|
||||
name: campaign.name,
|
||||
selected: Number(trigger.sourceCampaign) === campaign.id
|
||||
}));
|
||||
|
||||
trigger.destCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id && campaign.type === 4).map(campaign => ({
|
||||
id: campaign.id,
|
||||
name: campaign.name,
|
||||
selected: Number(trigger.destCampaign) === campaign.id
|
||||
}));
|
||||
|
||||
trigger.list = list;
|
||||
trigger.isSubscription = trigger.rule === 'subscription' || !trigger.rule;
|
||||
trigger.isCampaign = trigger.rule === 'campaign';
|
||||
|
||||
trigger.columns = triggers.defaultColumns.concat(fieldList.filter(field => fields.genericTypes[field.type] === 'date')).map(field => ({
|
||||
column: field.column,
|
||||
name: field.name,
|
||||
selected: trigger.isSubscription && trigger.column === field.column
|
||||
}));
|
||||
|
||||
trigger.campaignOptions = triggers.defaultCampaignEvents.map(evt => ({
|
||||
option: evt.option,
|
||||
name: evt.name,
|
||||
selected: trigger.isCampaign && trigger.column === evt.option
|
||||
}));
|
||||
|
||||
if (trigger.rule !== 'subscription') {
|
||||
trigger.column = null;
|
||||
}
|
||||
|
||||
trigger.isSend = true;
|
||||
|
||||
res.render('triggers/edit', trigger);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
triggers.update(req.body.id, req.body, (err, updated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/triggers/edit/' + encodeURIComponent(req.body.id));
|
||||
} else if (updated) {
|
||||
req.flash('success', _('Trigger settings updated'));
|
||||
} else {
|
||||
req.flash('info', _('Trigger settings not updated'));
|
||||
}
|
||||
|
||||
return res.redirect('/triggers');
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
triggers.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Trigger deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified trigger'));
|
||||
}
|
||||
|
||||
return res.redirect('/triggers');
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/status/:id', passport.csrfProtection, (req, res) => {
|
||||
let id = Number(req.params.id) || 0;
|
||||
|
||||
triggers.get(id, (err, trigger) => {
|
||||
if (err || !trigger) {
|
||||
req.flash('danger', err && err.message || err || _('Could not find trigger with specified ID'));
|
||||
return res.redirect('/triggers');
|
||||
}
|
||||
|
||||
trigger.csrfToken = req.csrfToken();
|
||||
res.render('triggers/triggered', trigger);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/status/ajax/:id', (req, res) => {
|
||||
triggers.get(req.params.id, (err, trigger) => {
|
||||
if (err || !trigger) {
|
||||
return res.json({
|
||||
error: err && err.message || err || _('Trigger not found'),
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
let columns = ['#', 'email', 'first_name', 'last_name', 'trigger__' + trigger.id + '.created'];
|
||||
triggers.filterSubscribers(trigger, req.body, columns, (err, data, total, filteredTotal) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
campaigns.get(trigger.destCampaign, false, (err, campaign) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err && err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
lists.get(trigger.list, (err, list) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err && err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
let campaignCid = campaign && campaign.cid;
|
||||
let listCid = list && list.cid;
|
||||
|
||||
res.json({
|
||||
draw: req.body.draw,
|
||||
recordsTotal: total,
|
||||
recordsFiltered: filteredTotal,
|
||||
data: data.map((row, i) => [
|
||||
'<a href="/archive/' + encodeURIComponent(campaignCid) + '/' + encodeURIComponent(listCid) + '/' + encodeURIComponent(row.cid) + '?track=no">' + ((Number(req.body.start) || 0) + 1 + i) + '</a>',
|
||||
htmlescape(row.email || ''),
|
||||
htmlescape(row.firstName || ''),
|
||||
htmlescape(row.lastName || ''),
|
||||
'<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>',
|
||||
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/subscription/' + trigger.list + '/edit/' + row.cid + '">' + _('Edit') + '</a>'
|
||||
])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -1,10 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
clientHelpers.registerRootRoute(router, 'users', _('Users'));
|
||||
|
||||
module.exports = router;
|
|
@ -1,334 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nodeifyFunction } = require('../lib/nodeify');
|
||||
const getSettings = nodeifyFunction(require('../models/settings').get);
|
||||
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let request = require('request');
|
||||
let campaigns = require('../lib/models/campaigns');
|
||||
let log = require('npmlog');
|
||||
let multer = require('multer');
|
||||
let uploads = multer();
|
||||
|
||||
router.post('/aws', (req, res, next) => {
|
||||
if (typeof req.body === 'string') {
|
||||
try {
|
||||
req.body = JSON.parse(req.body);
|
||||
} catch (E) {
|
||||
return next(new Error('Could not parse input'));
|
||||
}
|
||||
}
|
||||
|
||||
switch (req.body.Type) {
|
||||
|
||||
case 'SubscriptionConfirmation':
|
||||
if (req.body.SubscribeURL) {
|
||||
request(req.body.SubscribeURL, () => false);
|
||||
break;
|
||||
} else {
|
||||
let err = new Error('SubscribeURL not set');
|
||||
err.status = 400;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
case 'Notification':
|
||||
if (req.body.Message) {
|
||||
if (typeof req.body.Message === 'string') {
|
||||
try {
|
||||
req.body.Message = JSON.parse(req.body.Message);
|
||||
} catch (E) {
|
||||
return next('Could not parse Message object');
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.Message.mail && req.body.Message.mail.messageId) {
|
||||
campaigns.findMailByResponse(req.body.Message.mail.messageId, (err, message) => {
|
||||
if (err || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (req.body.Message.notificationType) {
|
||||
case 'Bounce':
|
||||
campaigns.updateMessage(message, 'bounced', req.body.Message.bounce.bounceType === 'Permanent', (err, updated) => {
|
||||
if (err) {
|
||||
log.error('AWS', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('AWS', 'Marked message %s as bounced', req.body.Message.mail.messageId);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'Complaint':
|
||||
if (req.body.Message.complaint) {
|
||||
campaigns.updateMessage(message, 'complained', true, (err, updated) => {
|
||||
if (err) {
|
||||
log.error('AWS', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('AWS', 'Marked message %s as complaint', req.body.Message.mail.messageId);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/sparkpost', (req, res, next) => {
|
||||
let events = [].concat(req.body || []);
|
||||
let pos = 0;
|
||||
|
||||
let processEvents = () => {
|
||||
if (pos >= events.length) {
|
||||
return res.json({
|
||||
success: true
|
||||
});
|
||||
}
|
||||
let curEvent = events[pos++];
|
||||
|
||||
let msys = curEvent && curEvent.msys;
|
||||
let evt;
|
||||
|
||||
if (msys && msys.message_event) {
|
||||
evt = msys.message_event;
|
||||
} else if (msys && msys.unsubscribe_event) {
|
||||
evt = msys.unsubscribe_event;
|
||||
}
|
||||
|
||||
if (!evt) {
|
||||
return processEvents();
|
||||
}
|
||||
|
||||
campaigns.findMailByCampaign(evt.campaign_id, (err, message) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return processEvents();
|
||||
}
|
||||
|
||||
switch (evt.type) {
|
||||
case 'bounce':
|
||||
// https://support.sparkpost.com/customer/portal/articles/1929896
|
||||
return campaigns.updateMessage(message, 'bounced', [1, 10, 25, 30, 50].indexOf(Number(evt.bounce_class)) >= 0, (err, updated) => {
|
||||
if (err) {
|
||||
log.error('Sparkpost', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('Sparkpost', 'Marked message %s as bounced', evt.campaign_id);
|
||||
}
|
||||
return processEvents();
|
||||
});
|
||||
case 'spam_complaint':
|
||||
return campaigns.updateMessage(message, 'complained', true, (err, updated) => {
|
||||
if (err) {
|
||||
log.error('Sparkpost', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('Sparkpost', 'Marked message %s as complaint', evt.campaign_id);
|
||||
}
|
||||
return processEvents();
|
||||
});
|
||||
case 'link_unsubscribe':
|
||||
return campaigns.updateMessage(message, 'unsubscribed', true, (err, updated) => {
|
||||
if (err) {
|
||||
log.error('Sparkpost', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('Sparkpost', 'Marked message %s as unsubscribed', evt.campaign_id);
|
||||
}
|
||||
return processEvents();
|
||||
});
|
||||
default:
|
||||
return processEvents();
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
processEvents();
|
||||
});
|
||||
|
||||
router.post('/sendgrid', (req, res, next) => {
|
||||
let events = [].concat(req.body || []);
|
||||
let pos = 0;
|
||||
|
||||
let processEvents = () => {
|
||||
if (pos >= events.length) {
|
||||
return res.json({
|
||||
success: true
|
||||
});
|
||||
}
|
||||
let evt = events[pos++];
|
||||
|
||||
if (!evt) {
|
||||
return processEvents();
|
||||
}
|
||||
|
||||
campaigns.findMailByCampaign(evt.campaign_id, (err, message) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return processEvents();
|
||||
}
|
||||
|
||||
switch (evt.event) {
|
||||
case 'bounce':
|
||||
// https://support.sparkpost.com/customer/portal/articles/1929896
|
||||
return campaigns.updateMessage(message, 'bounced', true, (err, updated) => {
|
||||
if (err) {
|
||||
log.error('Sendgrid', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('Sendgrid', 'Marked message %s as bounced', evt.campaign_id);
|
||||
}
|
||||
return processEvents();
|
||||
});
|
||||
case 'spamreport':
|
||||
return campaigns.updateMessage(message, 'complained', true, (err, updated) => {
|
||||
if (err) {
|
||||
log.error('Sendgrid', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('Sendgrid', 'Marked message %s as complaint', evt.campaign_id);
|
||||
}
|
||||
return processEvents();
|
||||
});
|
||||
case 'group_unsubscribe':
|
||||
case 'unsubscribe':
|
||||
return campaigns.updateMessage(message, 'unsubscribed', true, (err, updated) => {
|
||||
if (err) {
|
||||
log.error('Sendgrid', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('Sendgrid', 'Marked message %s as unsubscribed', evt.campaign_id);
|
||||
}
|
||||
return processEvents();
|
||||
});
|
||||
default:
|
||||
return processEvents();
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
processEvents();
|
||||
});
|
||||
|
||||
router.post('/mailgun', uploads.any(), (req, res) => {
|
||||
let evt = req.body;
|
||||
campaigns.findMailByCampaign([].concat(evt && evt.campaign_id || []).shift(), (err, message) => {
|
||||
if (err || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (evt.event) {
|
||||
case 'bounced':
|
||||
return campaigns.updateMessage(message, 'bounced', true, (err, updated) => {
|
||||
if (err) {
|
||||
log.error('Mailgun', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('Mailgun', 'Marked message %s as bounced', evt.campaign_id);
|
||||
}
|
||||
});
|
||||
case 'complained':
|
||||
return campaigns.updateMessage(message, 'complained', true, (err, updated) => {
|
||||
if (err) {
|
||||
log.error('Mailgun', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('Mailgun', 'Marked message %s as complaint', evt.campaign_id);
|
||||
}
|
||||
});
|
||||
case 'unsubscribed':
|
||||
return campaigns.updateMessage(message, 'unsubscribed', true, (err, updated) => {
|
||||
if (err) {
|
||||
log.error('Mailgun', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('Mailgun', 'Marked message %s as unsubscribed', evt.campaign_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/zone-mta', (req, res, next) => {
|
||||
if (typeof req.body === 'string') {
|
||||
try {
|
||||
req.body = JSON.parse(req.body);
|
||||
} catch (E) {
|
||||
return next(new Error('Could not parse input'));
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.id) {
|
||||
campaigns.findMailByResponse(req.body.id, (err, message) => {
|
||||
if (err || !message) {
|
||||
return;
|
||||
}
|
||||
campaigns.updateMessage(message, 'bounced', true, (err, updated) => {
|
||||
if (err) {
|
||||
log.error('ZoneMTA', 'Failed updating message: %s', err);
|
||||
} else if (updated) {
|
||||
log.verbose('ZoneMTA', 'Marked message %s as bounced', req.body.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/zone-mta/sender-config', (req, res) => {
|
||||
if (!req.query.api_token) {
|
||||
return res.json({
|
||||
error: 'api_token value not set'
|
||||
});
|
||||
}
|
||||
getSettings(['dkim_api_key', 'dkim_private_key', 'dkim_selector', 'dkim_domain'], (err, configItems) => {
|
||||
if (err) {
|
||||
return res.json({
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (configItems.dkimApiKey !== req.query.api_token) {
|
||||
return res.json({
|
||||
error: 'invalid api_token value'
|
||||
});
|
||||
}
|
||||
|
||||
configItems.dkimSelector = (configItems.dkimSelector || '').trim();
|
||||
configItems.dkimPrivateKey = (configItems.dkimPrivateKey || '').trim();
|
||||
|
||||
if (!configItems.dkimSelector || !configItems.dkimPrivateKey) {
|
||||
// empty response
|
||||
return res.json({});
|
||||
}
|
||||
|
||||
let from = (req.body.from || '').trim();
|
||||
let domain = from.split('@').pop().toLowerCase().trim();
|
||||
|
||||
res.json({
|
||||
dkim: {
|
||||
keys: [{
|
||||
domainName: configItems.dkimDomain || domain,
|
||||
keySelector: configItems.dkimSelector,
|
||||
privateKey: configItems.dkimPrivateKey
|
||||
}]
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
Loading…
Add table
Add a link
Reference in a new issue