New project structure

Beta of extract.js for extracting english locale
This commit is contained in:
Tomas Bures 2018-11-18 15:38:52 +01:00
parent e18d2b2f84
commit 2edbd67205
247 changed files with 6405 additions and 4237 deletions

283
server/routes/api.js Normal file
View file

@ -0,0 +1,283 @@
'use strict';
const config = require('config');
const lists = require('../models/lists');
const tools = require('../lib/tools');
const blacklist = require('../models/blacklist');
const fields = require('../models/fields');
const { SubscriptionStatus, SubscriptionSource } = require('../../shared/lists');
const subscriptions = require('../models/subscriptions');
const confirmations = require('../models/confirmations');
const log = require('../lib/log');
const router = require('../lib/router-async').create();
const mailHelpers = require('../lib/subscription-mail-helpers');
const interoperableErrors = require('../../shared/interoperable-errors');
const contextHelpers = require('../lib/context-helpers');
const shares = require('../models/shares');
const slugify = require('slugify');
const passport = require('../lib/passport');
class APIError extends Error {
constructor(msg, status) {
super(msg);
this.status = status;
}
}
router.postAsync('/subscribe/:listCid', passport.loggedIn, async (req, res) => {
const list = await lists.getByCid(req.context, req.params.listCid);
await shares.enforceEntityPermission(req.context, 'list', list.id, 'manageSubscriptions');
const input = {};
Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
});
if (!input.EMAIL) {
throw new APIError('Missing EMAIL', 400);
}
const emailErr = await tools.validateEmail(input.EMAIL);
if (emailErr) {
const errMsg = tools.validateEmailGetMessage(emailErr, input.email);
log.error('API', errMsg);
throw new APIError(errMsg, 400);
}
const subscription = await fields.fromAPI(req.context, list.id, input);
if (input.TIMEZONE) {
subscription.tz = (input.TIMEZONE || '').toString().trim();
}
if (/^(yes|true|1)$/i.test(input.FORCE_SUBSCRIBE)) {
subscription.status = SubscriptionStatus.SUBSCRIBED;
}
if (/^(yes|true|1)$/i.test(input.REQUIRE_CONFIRMATION)) { // if REQUIRE_CONFIRMATION is set, we assume that the user is not subscribed and will be subscribed
const data = {
email: input.EMAIL,
subscriptionData: subscription
};
const confirmCid = await confirmations.addConfirmation(list.id, 'subscribe', req.ip, data);
await mailHelpers.sendConfirmSubscription(config.language, list, input.EMAIL, confirmCid, subscription);
res.status(200);
res.json({
data: {
id: confirmCid
}
});
} else {
subscription.email = input.EMAIL;
const meta = {
updateAllowed: true,
subscribeIfNoExisting: true
};
await subscriptions.create(req.context, list.id, subscription, SubscriptionSource.API, meta);
res.status(200);
res.json({
data: {
id: meta.cid
}
});
}
});
router.postAsync('/unsubscribe/:listCid', passport.loggedIn, async (req, res) => {
const list = await lists.getByCid(req.context, req.params.listCid);
const input = {};
Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
});
if (!input.EMAIL) {
throw new APIError('Missing EMAIL', 400);
}
const subscription = await subscriptions.unsubscribeByEmailAndGet(req.context, list.id, input.EMAIL);
res.status(200);
res.json({
data: {
id: subscription.id,
unsubscribed: true
}
});
});
router.postAsync('/delete/:listCid', passport.loggedIn, async (req, res) => {
const list = await lists.getByCid(req.context, req.params.listCid);
const input = {};
Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
});
if (!input.EMAIL) {
throw new APIError('Missing EMAIL', 400);
}
const subscription = await subscriptions.removeByEmailAndGet(req.context, list.id, input.EMAIL);
res.status(200);
res.json({
data: {
id: subscription.id,
deleted: true
}
});
});
router.getAsync('/subscriptions/:listCid', passport.loggedIn, async (req, res) => {
const list = await lists.getByCid(req.context, req.params.listCid);
const start = parseInt(req.query.start || 0, 10);
const limit = parseInt(req.query.limit || 10000, 10);
const { subscriptions, total } = await subscriptions.list(list.id, false, start, limit);
res.status(200);
res.json({
data: {
total: total,
start: start,
limit: limit,
subscriptions
}
});
});
router.getAsync('/lists/:email', passport.loggedIn, async (req, res) => {
const lists = await subscriptions.getListsWithEmail(req.context, req.params.email);
res.status(200);
res.json({
data: lists
});
});
router.postAsync('/field/:listCid', passport.loggedIn, async (req, res) => {
const list = await lists.getByCid(req.context, req.params.listCid);
const input = {};
Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
});
const key = (input.NAME || '').toString().trim() || slugify('merge ' + name, '_').toUpperCase();
const visible = ['false', 'no', '0', ''].indexOf((input.VISIBLE || '').toString().toLowerCase().trim()) < 0;
const groupTemplate = (input.GROUP_TEMPLATE || '').toString().toLowerCase().trim();
let type = (input.TYPE || '').toString().toLowerCase().trim();
const settings = {};
if (type === 'checkbox') {
type = 'checkbox-grouped';
settings.groupTemplate = groupTemplate;
} else if (type === 'dropdown') {
type = 'dropdown-grouped';
settings.groupTemplate = groupTemplate;
} else if (type === 'radio') {
type = 'radio-grouped';
settings.groupTemplate = groupTemplate;
} else if (type === 'json') {
settings.groupTemplate = groupTemplate;
} else if (type === 'date-us') {
type = 'date';
settings.dateFormat = 'us';
} else if (type === 'date-eur') {
type = 'date';
settings.dateFormat = 'eur';
} else if (type === 'birthday-us') {
type = 'birthday';
settings.birthdayFormat = 'us';
} else if (type === 'birthday-eur') {
type = 'birthday';
settings.birthdayFormat = 'eur';
}
const field = {
name: (input.NAME || '').toString().trim(),
key,
default_value: (input.DEFAULT || '').toString().trim() || null,
type,
settings,
group: Number(input.GROUP) || null,
orderListBefore: visible ? 'end' : 'none',
orderSubscribeBefore: visible ? 'end' : 'none',
orderManageBefore: visible ? 'end' : 'none'
};
const id = await fields.create(req.context, list.id, field);
res.status(200);
res.json({
data: {
id,
tag: key
}
});
});
router.postAsync('/blacklist/add', passport.loggedIn, async (req, res) => {
let input = {};
Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
});
if (!(input.EMAIL) || (input.EMAIL === '')) {
throw new Error('EMAIL argument is required');
}
await blacklist.add(req.context, input.EMAIL);
res.json({
data: []
});
});
router.postAsync('/blacklist/delete', passport.loggedIn, async (req, res) => {
let input = {};
Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
});
if (!(input.EMAIL) || (input.EMAIL === '')) {
throw new Error('EMAIL argument is required');
}
await blacklist.remove(req.oontext, input.EMAIL);
res.json({
data: []
});
});
router.getAsync('/blacklist/get', passport.loggedIn, async (req, res) => {
let start = parseInt(req.query.start || 0, 10);
let limit = parseInt(req.query.limit || 10000, 10);
let search = req.query.search || '';
const { emails, total } = await blacklist.search(req.context, start, limit, search);
return res.json({
data: {
total,
start: start,
limit: limit,
emails
}
});
});
module.exports = router;

40
server/routes/archive.js Normal file
View file

@ -0,0 +1,40 @@
'use strict';
const router = require('../lib/router-async').create();
const CampaignSender = require('../lib/campaign-sender');
router.get('/:campaign/:list/:subscription', (req, res, next) => {
const cs = new CampaignSender();
cs.init({campaignCid: req.params.campaign})
.then(() => cs.getMessage(req.params.list, req.params.subscription))
.then(result => {
const {html} = result;
if (html.match(/<\/body\b/i)) {
res.render('partials/tracking-scripts', {
layout: 'archive/layout-raw'
}, (err, scripts) => {
if (err) {
return next(err);
}
const htmlWithScripts = scripts ? html.replace(/<\/body\b/i, match => scripts + match) : html;
res.render('archive/view', {
layout: 'archive/layout-raw',
message: htmlWithScripts
});
});
} else {
res.render('archive/view', {
layout: 'archive/layout-wrapped',
message: html
});
}
})
.catch(err => next(err));
});
module.exports = router;

13
server/routes/files.js Normal file
View file

@ -0,0 +1,13 @@
'use strict';
const router = require('../lib/router-async').create();
const files = require('../models/files');
const contextHelpers = require('../lib/context-helpers');
router.getAsync('/:type/:subType/:entityId/:fileName', async (req, res) => {
const file = await files.getFileByFilename(contextHelpers.getAdminContext(), req.params.type, req.params.subType, req.params.entityId, req.params.fileName);
res.type(file.mimetype);
return res.download(file.path, file.name);
});
module.exports = router;

35
server/routes/index.js Normal file
View file

@ -0,0 +1,35 @@
'use strict';
const passport = require('../lib/passport');
const _ = require('../lib/translate')._;
const clientHelpers = require('../lib/client-helpers');
const { getTrustedUrl } = require('../lib/urls');
const { AppType } = require('../../shared/app');
const routerFactory = require('../lib/router-async');
function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.TRUSTED) {
router.getAsync('/*', passport.csrfProtection, async (req, res) => {
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
if (req.user) {
Object.assign(mailtrainConfig, await clientHelpers.getAuthenticatedConfig(req.context));
}
res.render('root', {
reactCsrfToken: req.csrfToken(),
mailtrainConfig: JSON.stringify(mailtrainConfig),
scriptFiles: [
getTrustedUrl('mailtrain/root.js')
]
});
});
}
return router;
}
module.exports.getRouter = getRouter;

37
server/routes/links.js Normal file
View file

@ -0,0 +1,37 @@
'use strict';
const log = require('../lib/log');
const config = require('config');
const router = require('../lib/router-async').create();
const links = require('../models/links');
const interoperableErrors = require('../../shared/interoperable-errors');
const trackImg = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
router.getAsync('/:campaign/:list/:subscription', async (req, res) => {
res.writeHead(200, {
'Content-Type': 'image/gif',
'Content-Length': trackImg.length
});
res.end(trackImg);
await links.countLink(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, links.LinkId.OPEN);
});
router.getAsync('/:campaign/:list/:subscription/:link', async (req, res) => {
const link = await links.resolve(req.params.link);
if (link) {
// In Mailtrain v1 we would do the URL expansion here based on merge tags. We don't do it here anymore. Instead, the URLs are expanded when message is sent out (in links.updateLinks)
res.redirect(url);
await links.countLink(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, link.id);
} else {
log.error('Redirect', 'Unresolved URL: <%s>', req.url);
throw new interoperableErrors.NotFoundError('Oops, we couldn\'t find a link for the URL you clicked');
}
});
module.exports = router;

34
server/routes/reports.js Normal file
View file

@ -0,0 +1,34 @@
'use strict';
const passport = require('../lib/passport');
const reports = require('../models/reports');
const reportHelpers = require('../lib/report-helpers');
const shares = require('../models/shares');
const contextHelpers = require('../lib/context-helpers');
const router = require('../lib/router-async').create();
const fileSuffixes = {
'text/html': '.html',
'text/csv': '.csv'
};
router.getAsync('/:id/download', passport.loggedIn, async (req, res) => {
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewContent');
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), req.params.id);
if (report.state == reports.ReportState.FINISHED) {
const headers = {
'Content-Disposition': 'attachment;filename=' + reportHelpers.nameToFileName(report.name) + (fileSuffixes[report.mime_type] || ''),
'Content-Type': report.mime_type
};
res.sendFile(reportHelpers.getReportContentFile(report), {headers: headers});
} else {
return res.status(404).send('Report not found');
}
});
module.exports = router;

View file

@ -0,0 +1,73 @@
'use strict';
const passport = require('../../lib/passport');
const _ = require('../../lib/translate')._;
const users = require('../../models/users');
const contextHelpers = require('../../lib/context-helpers');
const router = require('../../lib/router-async').create();
router.getAsync('/account', passport.loggedIn, async (req, res) => {
const user = await users.getById(contextHelpers.getAdminContext(), req.user.id);
user.hash = users.hash(user);
return res.json(user);
});
router.postAsync('/account', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const data = req.body;
data.id = req.user.id;
await users.updateWithConsistencyCheck(contextHelpers.getAdminContext(), req.body, true);
return res.json();
});
router.postAsync('/account-validate', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const data = req.body;
data.id = req.user.id;
return res.json(await users.serverValidate(contextHelpers.getAdminContext(), data, true));
});
router.getAsync('/access-token', passport.loggedIn, async (req, res) => {
const accessToken = await users.getAccessToken(req.user.id);
return res.json(accessToken);
});
router.postAsync('/access-token-reset', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const accessToken = await users.resetAccessToken(req.user.id);
return res.json(accessToken);
});
router.post('/login', passport.csrfProtection, passport.restLogin);
router.post('/logout', passport.csrfProtection, passport.restLogout);
router.postAsync('/password-reset-send', passport.csrfProtection, async (req, res) => {
await users.sendPasswordReset(req.language, req.body.usernameOrEmail);
return res.json();
});
router.postAsync('/password-reset-validate', passport.csrfProtection, async (req, res) => {
const isValid = await users.isPasswordResetTokenValid(req.body.username, req.body.resetToken);
return res.json(isValid);
});
router.postAsync('/password-reset', passport.csrfProtection, async (req, res) => {
await users.resetPassword(req.body.username, req.body.resetToken, req.body.password);
return res.json();
});
router.postAsync('/restricted-access-token', passport.loggedIn, async (req, res) => {
const restrictedAccessToken = await users.getRestrictedAccessToken(req.context, req.body.method, req.body.params);
return res.json(restrictedAccessToken);
});
router.putAsync('/restricted-access-token', passport.loggedIn, async (req, res) => {
await users.refreshRestrictedAccessToken(req.context, req.body.token);
return res.json();
});
module.exports = router;

View file

@ -0,0 +1,26 @@
'use strict';
const passport = require('../../lib/passport');
const blacklist = require('../../models/blacklist');
const router = require('../../lib/router-async').create();
router.postAsync('/blacklist-table', passport.loggedIn, async (req, res) => {
return res.json(await blacklist.listDTAjax(req.context, req.body));
});
router.postAsync('/blacklist', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await blacklist.add(req.context, req.body.email));
});
router.deleteAsync('/blacklist/:email', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await blacklist.remove(req.context, req.params.email);
return res.json();
});
router.postAsync('/blacklist-validate', passport.loggedIn, async (req, res) => {
return res.json(await blacklist.serverValidate(req.context, req.body));
});
module.exports = router;

View file

@ -0,0 +1,98 @@
'use strict';
const passport = require('../../lib/passport');
const campaigns = require('../../models/campaigns');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.postAsync('/campaigns-table', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.listDTAjax(req.context, req.body));
});
router.postAsync('/campaigns-with-content-table', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.listWithContentDTAjax(req.context, req.body));
});
router.postAsync('/campaigns-others-by-list-table/:campaignId/:listIds', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.listOthersWhoseListsAreIncludedDTAjax(req.context, castToInteger(req.params.campaignId), req.params.listIds.split(';').map(x => castToInteger(x)), req.body));
});
router.postAsync('/campaigns-children/:campaignId', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.listChildrenDTAjax(req.context, castToInteger(req.params.campaignId), req.body));
});
router.postAsync('/campaigns-test-users-table/:campaignId', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.listTestUsersDTAjax(req.context, castToInteger(req.params.campaignId), req.body));
});
router.getAsync('/campaigns-settings/:campaignId', passport.loggedIn, async (req, res) => {
const campaign = await campaigns.getById(req.context, castToInteger(req.params.campaignId), true, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
campaign.hash = campaigns.hash(campaign, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
return res.json(campaign);
});
router.getAsync('/campaigns-stats/:campaignId', passport.loggedIn, async (req, res) => {
const campaign = await campaigns.getById(req.context, castToInteger(req.params.campaignId), true, campaigns.Content.SETTINGS_WITH_STATS);
return res.json(campaign);
});
router.getAsync('/campaigns-content/:campaignId', passport.loggedIn, async (req, res) => {
const campaign = await campaigns.getById(req.context, castToInteger(req.params.campaignId), true, campaigns.Content.ONLY_SOURCE_CUSTOM);
campaign.hash = campaigns.hash(campaign, campaigns.Content.ONLY_SOURCE_CUSTOM);
return res.json(campaign);
});
router.postAsync('/campaigns', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await campaigns.create(req.context, req.body));
});
router.putAsync('/campaigns-settings/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const entity = req.body;
entity.id = castToInteger(req.params.campaignId);
await campaigns.updateWithConsistencyCheck(req.context, entity, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
return res.json();
});
router.putAsync('/campaigns-content/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const entity = req.body;
entity.id = castToInteger(req.params.campaignId);
await campaigns.updateWithConsistencyCheck(req.context, entity, campaigns.Content.ONLY_SOURCE_CUSTOM);
return res.json();
});
router.deleteAsync('/campaigns/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await campaigns.remove(req.context, castToInteger(req.params.campaignId));
return res.json();
});
router.postAsync('/campaign-start/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await campaigns.start(req.context, castToInteger(req.params.campaignId), null));
});
router.postAsync('/campaign-start-at/:campaignId/:dateTime', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await campaigns.start(req.context, castToInteger(req.params.campaignId), new Date(Number.parseInt(req.params.dateTime))));
});
router.postAsync('/campaign-stop/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await campaigns.stop(req.context, castToInteger(req.params.campaignId)));
});
router.postAsync('/campaign-reset/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await campaigns.reset(req.context, castToInteger(req.params.campaignId)));
});
router.postAsync('/campaign-enable/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await campaigns.enable(req.context, castToInteger(req.params.campaignId), null));
});
router.postAsync('/campaign-disable/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await campaigns.disable(req.context, castToInteger(req.params.campaignId), null));
});
module.exports = router;

View file

@ -0,0 +1,25 @@
'use strict';
const passport = require('../../lib/passport');
const bluebird = require('bluebird');
const premailerApi = require('premailer-api');
const premailerPrepareAsync = bluebird.promisify(premailerApi.prepare);
const router = require('../../lib/router-async').create();
router.postAsync('/html-to-text', passport.loggedIn, passport.csrfProtection, async (req, res) => {
if (!req.body.html) {
return res.json({text: ''}); // Premailer crashes very hard when html is empty
}
const email = await premailerPrepareAsync({
html: req.body.html,
fetchHTML: false
});
res.json({text: email.text.replace(/%5B/g, '[').replace(/%5D/g, ']')});
});
module.exports = router;

View file

@ -0,0 +1,56 @@
'use strict';
const passport = require('../../lib/passport');
const fields = require('../../models/fields');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.postAsync('/fields-table/:listId', passport.loggedIn, async (req, res) => {
return res.json(await fields.listDTAjax(req.context, castToInteger(req.params.listId), req.body));
});
router.postAsync('/fields-grouped-table/:listId', passport.loggedIn, async (req, res) => {
return res.json(await fields.listGroupedDTAjax(req.context, castToInteger(req.params.listId), req.body));
});
router.getAsync('/fields/:listId/:fieldId', passport.loggedIn, async (req, res) => {
const entity = await fields.getById(req.context, castToInteger(req.params.listId), castToInteger(req.params.fieldId));
entity.hash = fields.hash(entity);
return res.json(entity);
});
router.getAsync('/fields/:listId', passport.loggedIn, async (req, res) => {
const rows = await fields.list(req.context, castToInteger(req.params.listId));
return res.json(rows);
});
router.getAsync('/fields-grouped/:listId', passport.loggedIn, async (req, res) => {
const rows = await fields.listGrouped(req.context, castToInteger(req.params.listId));
return res.json(rows);
});
router.postAsync('/fields/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await fields.create(req.context, castToInteger(req.params.listId), req.body));
});
router.putAsync('/fields/:listId/:fieldId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const entity = req.body;
entity.id = castToInteger(req.params.fieldId);
await fields.updateWithConsistencyCheck(req.context, castToInteger(req.params.listId), entity);
return res.json();
});
router.deleteAsync('/fields/:listId/:fieldId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await fields.remove(req.context, castToInteger(req.params.listId), castToInteger(req.params.fieldId));
return res.json();
});
router.postAsync('/fields-validate/:listId', passport.loggedIn, async (req, res) => {
return res.json(await fields.serverValidate(req.context, castToInteger(req.params.listId), req.body));
});
module.exports = router;

View file

@ -0,0 +1,31 @@
'use strict';
const passport = require('../../lib/passport');
const files = require('../../models/files');
const router = require('../../lib/router-async').create();
const fileHelpers = require('../../lib/file-helpers');
const {castToInteger} = require('../../lib/helpers');
router.postAsync('/files-table/:type/:subType/:entityId', passport.loggedIn, async (req, res) => {
return res.json(await files.listDTAjax(req.context, req.params.type, req.params.subType, castToInteger(req.params.entityId), req.body));
});
router.getAsync('/files-list/:type/:subType/:entityId', passport.loggedIn, async (req, res) => {
return res.json(await files.list(req.context, req.params.type, req.params.subType, castToInteger(req.params.entityId)));
});
router.getAsync('/files/:type/:subType/:fileId', passport.loggedIn, async (req, res) => {
const file = await files.getFileById(req.context, req.params.type, req.params.subType, castToInteger(req.params.fileId));
res.type(file.mimetype);
return res.download(file.path, file.name);
});
router.deleteAsync('/files/:type/:subType/:fileId', passport.loggedIn, async (req, res) => {
await files.removeFile(req.context, req.params.type, req.params.subType, castToInteger(req.params.fileId));
return res.json();
});
fileHelpers.installUploadHandler(router, '/files/:type/:subType/:entityId');
module.exports = router;

View file

@ -0,0 +1,42 @@
'use strict';
const passport = require('../../lib/passport');
const forms = require('../../models/forms');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.postAsync('/forms-table', passport.loggedIn, async (req, res) => {
return res.json(await forms.listDTAjax(req.context, req.body));
});
router.getAsync('/forms/:formId', passport.loggedIn, async (req, res) => {
const entity = await forms.getById(req.context, castToInteger(req.params.formId));
entity.hash = forms.hash(entity);
return res.json(entity);
});
router.postAsync('/forms', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await forms.create(req.context, req.body));
});
router.putAsync('/forms/:formId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const entity = req.body;
entity.id = castToInteger(req.params.formId);
await forms.updateWithConsistencyCheck(req.context, entity);
return res.json();
});
router.deleteAsync('/forms/:formId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await forms.remove(req.context, castToInteger(req.params.formId));
return res.json();
});
router.postAsync('/forms-validate', passport.loggedIn, async (req, res) => {
return res.json(await forms.serverValidate(req.context, req.body));
});
module.exports = router;

View file

@ -0,0 +1,22 @@
'use strict';
const passport = require('../../lib/passport');
const importRuns = require('../../models/import-runs');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.postAsync('/import-runs-table/:listId/:importId', passport.loggedIn, async (req, res) => {
return res.json(await importRuns.listDTAjax(req.context, castToInteger(req.params.listId), castToInteger(req.params.importId), req.body));
});
router.postAsync('/import-run-failed-table/:listId/:importId/:importRunId', passport.loggedIn, async (req, res) => {
return res.json(await importRuns.listFailedDTAjax(req.context, castToInteger(req.params.listId), castToInteger(req.params.importId), castToInteger(req.params.importRunId), req.body));
});
router.getAsync('/import-runs/:listId/:importId/:runId', passport.loggedIn, async (req, res) => {
const entity = await importRuns.getById(req.context, castToInteger(req.params.listId), castToInteger(req.params.importId), castToInteger(req.params.runId));
return res.json(entity);
});
module.exports = router;

View file

@ -0,0 +1,59 @@
'use strict';
const passport = require('../../lib/passport');
const imports = require('../../models/imports');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
const path = require('path');
const files = require('../../models/files');
const uploadedFilesDir = path.join(files.filesDir, 'uploaded');
const multer = require('multer')({
dest: uploadedFilesDir
});
router.postAsync('/imports-table/:listId', passport.loggedIn, async (req, res) => {
return res.json(await imports.listDTAjax(req.context, castToInteger(req.params.listId), req.body));
});
router.getAsync('/imports/:listId/:importId', passport.loggedIn, async (req, res) => {
const entity = await imports.getById(req.context, castToInteger(req.params.listId), castToInteger(req.params.importId), true);
entity.hash = imports.hash(entity);
return res.json(entity);
});
const fileFields = [
{name: 'csvFile', maxCount: 1}
];
router.postAsync('/imports/:listId', passport.loggedIn, passport.csrfProtection, multer.fields(fileFields), async (req, res) => {
const entity = JSON.parse(req.body.entity);
return res.json(await imports.create(req.context, castToInteger(req.params.listId), entity, req.files));
});
router.putAsync('/imports/:listId/:importId', passport.loggedIn, passport.csrfProtection, multer.fields(fileFields), async (req, res) => {
const entity = JSON.parse(req.body.entity);
entity.id = castToInteger(req.params.importId);
await imports.updateWithConsistencyCheck(req.context, castToInteger(req.params.listId), entity, req.files);
return res.json();
});
router.deleteAsync('/imports/:listId/:importId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await imports.remove(req.context, castToInteger(req.params.listId), castToInteger(req.params.importId));
return res.json();
});
router.postAsync('/import-start/:listId/:importId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await imports.start(req.context, castToInteger(req.params.listId), castToInteger(req.params.importId)));
});
router.postAsync('/import-stop/:listId/:importId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await imports.stop(req.context, castToInteger(req.params.listId), castToInteger(req.params.importId)));
});
module.exports = router;

View file

@ -0,0 +1,42 @@
'use strict';
const passport = require('../../lib/passport');
const lists = require('../../models/lists');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.postAsync('/lists-table', passport.loggedIn, async (req, res) => {
return res.json(await lists.listDTAjax(req.context, req.body));
});
router.postAsync('/lists-with-segment-by-campaign-table/:campaignId', passport.loggedIn, async (req, res) => {
return res.json(await lists.listWithSegmentByCampaignDTAjax(req.context, castToInteger(req.params.campaignId), req.body));
});
router.getAsync('/lists/:listId', passport.loggedIn, async (req, res) => {
const list = await lists.getByIdWithListFields(req.context, castToInteger(req.params.listId));
list.hash = lists.hash(list);
return res.json(list);
});
router.postAsync('/lists', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await lists.create(req.context, req.body));
});
router.putAsync('/lists/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const entity = req.body;
entity.id = castToInteger(req.params.listId);
await lists.updateWithConsistencyCheck(req.context, entity);
return res.json();
});
router.deleteAsync('/lists/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await lists.remove(req.context, castToInteger(req.params.listId));
return res.json();
});
module.exports = router;

View file

@ -0,0 +1,36 @@
'use strict';
const passport = require('../../lib/passport');
const mosaicoTemplates = require('../../models/mosaico-templates');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.getAsync('/mosaico-templates/:mosaicoTemplateId', passport.loggedIn, async (req, res) => {
const mosaicoTemplate = await mosaicoTemplates.getById(req.context, castToInteger(req.params.mosaicoTemplateId));
mosaicoTemplate.hash = mosaicoTemplates.hash(mosaicoTemplate);
return res.json(mosaicoTemplate);
});
router.postAsync('/mosaico-templates', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await mosaicoTemplates.create(req.context, req.body));
});
router.putAsync('/mosaico-templates/:mosaicoTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const mosaicoTemplate = req.body;
mosaicoTemplate.id = castToInteger(req.params.mosaicoTemplateId);
await mosaicoTemplates.updateWithConsistencyCheck(req.context, mosaicoTemplate);
return res.json();
});
router.deleteAsync('/mosaico-templates/:mosaicoTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await mosaicoTemplates.remove(req.context, castToInteger(req.params.mosaicoTemplateId));
return res.json();
});
router.postAsync('/mosaico-templates-table', passport.loggedIn, async (req, res) => {
return res.json(await mosaicoTemplates.listDTAjax(req.context, req.body));
});
module.exports = router;

View file

@ -0,0 +1,44 @@
'use strict';
const passport = require('../../lib/passport');
const _ = require('../../lib/translate')._;
const namespaces = require('../../models/namespaces');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.getAsync('/namespaces/:nsId', passport.loggedIn, async (req, res) => {
const ns = await namespaces.getById(req.context, castToInteger(req.params.nsId));
ns.hash = namespaces.hash(ns);
return res.json(ns);
});
router.postAsync('/namespaces', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await namespaces.create(req.context, req.body));
});
router.putAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const ns = req.body;
ns.id = castToInteger(req.params.nsId);
await namespaces.updateWithConsistencyCheck(req.context, ns);
return res.json();
});
router.deleteAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await namespaces.remove(req.context, castToInteger(req.params.nsId));
return res.json();
});
router.getAsync('/namespaces-tree', passport.loggedIn, async (req, res) => {
const tree = await namespaces.listTree(req.context);
return res.json(tree);
});
module.exports = router;

View file

@ -0,0 +1,43 @@
'use strict';
const passport = require('../../lib/passport');
const _ = require('../../lib/translate')._;
const reportTemplates = require('../../models/report-templates');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.getAsync('/report-templates/:reportTemplateId', passport.loggedIn, async (req, res) => {
const reportTemplate = await reportTemplates.getById(req.context, castToInteger(req.params.reportTemplateId));
reportTemplate.hash = reportTemplates.hash(reportTemplate);
return res.json(reportTemplate);
});
router.postAsync('/report-templates', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await reportTemplates.create(req.context, req.body));
});
router.putAsync('/report-templates/:reportTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const reportTemplate = req.body;
reportTemplate.id = castToInteger(req.params.reportTemplateId);
await reportTemplates.updateWithConsistencyCheck(req.context, reportTemplate);
return res.json();
});
router.deleteAsync('/report-templates/:reportTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await reportTemplates.remove(req.context, castToInteger(req.params.reportTemplateId));
return res.json();
});
router.postAsync('/report-templates-table', passport.loggedIn, async (req, res) => {
return res.json(await reportTemplates.listDTAjax(req.context, req.body));
});
router.getAsync('/report-template-user-fields/:reportTemplateId', passport.loggedIn, async (req, res) => {
const userFields = await reportTemplates.getUserFieldsById(req.context, castToInteger(req.params.reportTemplateId));
return res.json(userFields);
});
module.exports = router;

View file

@ -0,0 +1,97 @@
'use strict';
const passport = require('../../lib/passport');
const _ = require('../../lib/translate')._;
const reports = require('../../models/reports');
const reportProcessor = require('../../lib/report-processor');
const reportHelpers = require('../../lib/report-helpers');
const shares = require('../../models/shares');
const contextHelpers = require('../../lib/context-helpers');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
const fs = require('fs-extra');
router.getAsync('/reports/:reportId', passport.loggedIn, async (req, res) => {
const report = await reports.getByIdWithTemplate(req.context, castToInteger(req.params.reportId));
report.hash = reports.hash(report);
return res.json(report);
});
router.postAsync('/reports', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await reports.create(req.context, req.body));
});
router.putAsync('/reports/:reportId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const report = req.body;
report.id = castToInteger(req.params.reportId);
await reports.updateWithConsistencyCheck(req.context, report);
return res.json();
});
router.deleteAsync('/reports/:reportId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await reports.remove(req.context, castToInteger(req.params.reportId));
return res.json();
});
router.postAsync('/reports-table', passport.loggedIn, async (req, res) => {
return res.json(await reports.listDTAjax(req.context, req.body));
});
router.postAsync('/report-start/:id', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const id = castToInteger(req.params.id);
await shares.enforceEntityPermission(req.context, 'report', id, 'execute');
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id, false);
await shares.enforceEntityPermission(req.context, 'reportTemplate', report.report_template, 'execute');
await reportProcessor.start(id);
res.json();
});
router.postAsync('/report-stop/:id', async (req, res) => {
const id = castToInteger(req.params.id);
await shares.enforceEntityPermission(req.context, 'report', id, 'execute');
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id, false);
await shares.enforceEntityPermission(req.context, 'reportTemplate', report.report_template, 'execute');
await reportProcessor.stop(id);
res.json();
});
router.getAsync('/report-content/:id', async (req, res) => {
const id = castToInteger(req.params.id);
await shares.enforceEntityPermission(req.context, 'report', id, 'viewContent');
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id, false);
const file = reportHelpers.getReportContentFile(report);
if (await fs.pathExists(file)) {
res.sendFile(file);
} else {
res.send('');
}
});
router.getAsync('/report-output/:id', async (req, res) => {
const id = castToInteger(req.params.id);
await shares.enforceEntityPermission(req.context, 'report', id, 'viewOutput');
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id, false);
const file = reportHelpers.getReportOutputFile(report);
if (await fs.pathExists(file)) {
res.sendFile(file);
} else {
res.send('');
}
});
module.exports = router;

View file

@ -0,0 +1,42 @@
'use strict';
const passport = require('../../lib/passport');
const segments = require('../../models/segments');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.postAsync('/segments-table/:listId', passport.loggedIn, async (req, res) => {
return res.json(await segments.listDTAjax(req.context, castToInteger(req.params.listId), req.body));
});
router.getAsync('/segments/:listId', passport.loggedIn, async (req, res) => {
return res.json(await segments.listIdName(req.context, castToInteger(req.params.listId)));
});
router.getAsync('/segments/:listId/:segmentId', passport.loggedIn, async (req, res) => {
const segment = await segments.getById(req.context, castToInteger(req.params.listId), castToInteger(req.params.segmentId));
segment.hash = segments.hash(segment);
return res.json(segment);
});
router.postAsync('/segments/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await segments.create(req.context, castToInteger(req.params.listId), req.body));
});
router.putAsync('/segments/:listId/:segmentId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const entity = req.body;
entity.id = castToInteger(req.params.segmentId);
await segments.updateWithConsistencyCheck(req.context, castToInteger(req.params.listId), entity);
return res.json();
});
router.deleteAsync('/segments/:listId/:segmentId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await segments.remove(req.context, castToInteger(req.params.listId), castToInteger(req.params.segmentId));
return res.json();
});
module.exports = router;

View file

@ -0,0 +1,47 @@
'use strict';
const passport = require('../../lib/passport');
const sendConfigurations = require('../../models/send-configurations');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.getAsync('/send-configurations-private/:sendConfigurationId', passport.loggedIn, async (req, res) => {
const sendConfiguration = await sendConfigurations.getById(req.context, castToInteger(req.params.sendConfigurationId), true, true);
sendConfiguration.hash = sendConfigurations.hash(sendConfiguration);
return res.json(sendConfiguration);
});
router.getAsync('/send-configurations-public/:sendConfigurationId', passport.loggedIn, async (req, res) => {
const sendConfiguration = await sendConfigurations.getById(req.context, castToInteger(req.params.sendConfigurationId), true, false);
sendConfiguration.hash = sendConfigurations.hash(sendConfiguration);
return res.json(sendConfiguration);
});
router.postAsync('/send-configurations', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await sendConfigurations.create(req.context, req.body));
});
router.putAsync('/send-configurations/:sendConfigurationId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const sendConfiguration = req.body;
sendConfiguration.id = castToInteger(req.params.sendConfigurationId);
await sendConfigurations.updateWithConsistencyCheck(req.context, sendConfiguration);
return res.json();
});
router.deleteAsync('/send-configurations/:sendConfigurationId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await sendConfigurations.remove(req.context, castToInteger(req.params.sendConfigurationId));
return res.json();
});
router.postAsync('/send-configurations-table', passport.loggedIn, async (req, res) => {
return res.json(await sendConfigurations.listDTAjax(req.context, req.body));
});
router.postAsync('/send-configurations-with-send-permission-table', passport.loggedIn, async (req, res) => {
return res.json(await sendConfigurations.listWithSendPermissionDTAjax(req.context, req.body));
});
module.exports = router;

View file

@ -0,0 +1,22 @@
'use strict';
const passport = require('../../lib/passport');
const settings = require('../../models/settings');
const router = require('../../lib/router-async').create();
router.getAsync('/settings', passport.loggedIn, async (req, res) => {
const configItems = await settings.get(req.context);
configItems.hash = settings.hash(configItems);
return res.json(configItems);
});
router.putAsync('/settings', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const configItems = req.body;
await settings.set(req.context, configItems);
return res.json();
});
module.exports = router;

View file

@ -0,0 +1,78 @@
'use strict';
const passport = require('../../lib/passport');
const _ = require('../../lib/translate')._;
const shares = require('../../models/shares');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.postAsync('/shares-table-by-entity/:entityTypeId/:entityId', passport.loggedIn, async (req, res) => {
return res.json(await shares.listByEntityDTAjax(req.context, req.params.entityTypeId, castToInteger(req.params.entityId), req.body));
});
router.postAsync('/shares-table-by-user/:entityTypeId/:userId', passport.loggedIn, async (req, res) => {
return res.json(await shares.listByUserDTAjax(req.context, req.params.entityTypeId, castToInteger(req.params.userId), req.body));
});
router.postAsync('/shares-unassigned-users-table/:entityTypeId/:entityId', passport.loggedIn, async (req, res) => {
return res.json(await shares.listUnassignedUsersDTAjax(req.context, req.params.entityTypeId, castToInteger(req.params.entityId), req.body));
});
router.postAsync('/shares-roles-table/:entityTypeId', passport.loggedIn, async (req, res) => {
return res.json(await shares.listRolesDTAjax(req.params.entityTypeId, req.body));
});
router.putAsync('/shares', passport.loggedIn, async (req, res) => {
const body = req.body;
await shares.assign(req.context, body.entityTypeId, body.entityId, body.userId, body.role);
return res.json();
});
/*
Checks if entities with a given permission exist.
Accepts format:
{
XXX1: {
entityTypeId: ...
requiredOperations: [ ... ]
},
XXX2: {
entityTypeId: ...
requiredOperations: [ ... ]
}
}
Returns:
{
XXX1: true
XXX2: false
}
*/
router.postAsync('/permissions-check', passport.loggedIn, async (req, res) => {
const body = req.body;
const result = {};
for (const reqKey in body) {
if (body[reqKey].entityId) {
result[reqKey] = await shares.checkEntityPermission(req.context, body[reqKey].entityTypeId, body[reqKey].entityId, body[reqKey].requiredOperations);
} else {
result[reqKey] = await shares.checkTypePermission(req.context, body[reqKey].entityTypeId, body[reqKey].requiredOperations);
}
}
return res.json(result);
});
router.postAsync('/permissions-rebuild', passport.loggedIn, async (req, res) => {
shares.enforceGlobalPermission(req.context, 'rebuildPermissions');
await shares.rebuildPermissions();
return res.json(result);
});
module.exports = router;

View file

@ -0,0 +1,52 @@
'use strict';
const passport = require('../../lib/passport');
const subscriptions = require('../../models/subscriptions');
const { SubscriptionSource } = require('../../../shared/lists');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.postAsync('/subscriptions-table/:listId/:segmentId?', passport.loggedIn, async (req, res) => {
return res.json(await subscriptions.listDTAjax(req.context, castToInteger(req.params.listId), req.params.segmentId ? castToInteger(req.params.segmentId) : null, req.body));
});
router.postAsync('/subscriptions-test-user-table/:listCid', passport.loggedIn, async (req, res) => {
return res.json(await subscriptions.listTestUsersDTAjax(req.context, req.params.listCid, req.body));
});
router.getAsync('/subscriptions/:listId/:subscriptionId', passport.loggedIn, async (req, res) => {
const entity = await subscriptions.getById(req.context, castToInteger(req.params.listId), castToInteger(req.params.subscriptionId));
entity.hash = await subscriptions.hashByList(castToInteger(req.params.listId), entity);
return res.json(entity);
});
router.postAsync('/subscriptions/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await subscriptions.create(req.context, castToInteger(req.params.listId), req.body, SubscriptionSource.ADMIN_FORM));
});
router.putAsync('/subscriptions/:listId/:subscriptionId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const entity = req.body;
entity.id = castToInteger(req.params.subscriptionId);
await subscriptions.updateWithConsistencyCheck(req.context, castToInteger(req.params.listId), entity, SubscriptionSource.ADMIN_FORM);
return res.json();
});
router.deleteAsync('/subscriptions/:listId/:subscriptionId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await subscriptions.remove(req.context, castToInteger(req.params.listId), castToInteger(req.params.subscriptionId));
return res.json();
});
router.postAsync('/subscriptions-validate/:listId', passport.loggedIn, async (req, res) => {
return res.json(await subscriptions.serverValidate(req.context, castToInteger(req.params.listId), req.body));
});
router.postAsync('/subscriptions-unsubscribe/:listId/:subscriptionId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await subscriptions.unsubscribeByIdAndGet(req.context, castToInteger(req.params.listId), castToInteger(req.params.subscriptionId));
return res.json();
});
module.exports = router;

View file

@ -0,0 +1,44 @@
'use strict';
const passport = require('../../lib/passport');
const templates = require('../../models/templates');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
const CampaignSender = require('../../lib/campaign-sender');
router.getAsync('/templates/:templateId', passport.loggedIn, async (req, res) => {
const template = await templates.getById(req.context, castToInteger(req.params.templateId));
template.hash = templates.hash(template);
return res.json(template);
});
router.postAsync('/templates', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await templates.create(req.context, req.body));
});
router.putAsync('/templates/:templateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const template = req.body;
template.id = castToInteger(req.params.templateId);
await templates.updateWithConsistencyCheck(req.context, template);
return res.json();
});
router.deleteAsync('/templates/:templateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await templates.remove(req.context, castToInteger(req.params.templateId));
return res.json();
});
router.postAsync('/templates-table', passport.loggedIn, async (req, res) => {
return res.json(await templates.listDTAjax(req.context, req.body));
});
router.postAsync('/template-test-send', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const data = req.body;
const result = await CampaignSender.testSend(req.context, data.listCid, data.subscriptionCid, data.campaignId, data.sendConfigurationId, data.html, data.text);
return res.json(result);
});
module.exports = router;

View file

@ -0,0 +1,41 @@
'use strict';
const passport = require('../../lib/passport');
const triggers = require('../../models/triggers');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.postAsync('/triggers-by-campaign-table/:campaignId', passport.loggedIn, async (req, res) => {
return res.json(await triggers.listByCampaignDTAjax(req.context, castToInteger(req.params.campaignId), req.body));
});
router.postAsync('/triggers-by-list-table/:listId', passport.loggedIn, async (req, res) => {
return res.json(await triggers.listByListDTAjax(req.context, castToInteger(req.params.listId), req.body));
});
router.getAsync('/triggers/:campaignId/:triggerId', passport.loggedIn, async (req, res) => {
const entity = await triggers.getById(req.context, castToInteger(req.params.campaignId), req.params.triggerId);
entity.hash = triggers.hash(entity);
return res.json(entity);
});
router.postAsync('/triggers/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await triggers.create(req.context, castToInteger(req.params.campaignId), req.body));
});
router.putAsync('/triggers/:campaignId/:triggerId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const entity = req.body;
entity.id = castToInteger(req.params.triggerId);
await triggers.updateWithConsistencyCheck(req.context, castToInteger(req.params.campaignId), entity);
return res.json();
});
router.deleteAsync('/triggers/:campaignId/:triggerId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await triggers.remove(req.context, castToInteger(req.params.campaignId), castToInteger(req.params.triggerId));
return res.json();
});
module.exports = router;

View file

@ -0,0 +1,45 @@
'use strict';
const config = require('config');
const passport = require('../../lib/passport');
const _ = require('../../lib/translate')._;
const users = require('../../models/users');
const shares = require('../../models/shares');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
router.getAsync('/users/:userId', passport.loggedIn, async (req, res) => {
const user = await users.getById(req.context, castToInteger(req.params.userId));
user.hash = users.hash(user);
return res.json(user);
});
router.postAsync('/users', passport.loggedIn, passport.csrfProtection, async (req, res) => {
return res.json(await users.create(req.context, req.body));
});
router.putAsync('/users/:userId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const user = req.body;
user.id = castToInteger(req.params.userId);
await users.updateWithConsistencyCheck(req.context, user);
return res.json();
});
router.deleteAsync('/users/:userId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await users.remove(req.context, castToInteger(req.params.userId));
return res.json();
});
router.postAsync('/users-validate', passport.loggedIn, async (req, res) => {
return res.json(await users.serverValidate(req.context, req.body));
});
router.postAsync('/users-table', passport.loggedIn, async (req, res) => {
return res.json(await users.listDTAjax(req.context, req.body));
});
module.exports = router;

View file

@ -0,0 +1,58 @@
'use strict';
const routerFactory = require('../lib/router-async');
const passport = require('../lib/passport');
const clientHelpers = require('../lib/client-helpers');
const users = require('../models/users');
const files = require('../models/files');
const fileHelpers = require('../lib/file-helpers');
const templates = require('../models/templates');
const contextHelpers = require('../lib/context-helpers');
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
const { AppType } = require('../../shared/app');
users.registerRestrictedAccessTokenMethod('ckeditor', async ({entityTypeId, entityId}) => {
if (entityTypeId === 'template') {
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
if (tmpl.type === 'ckeditor4') {
return {
permissions: {
'template': {
[entityId]: new Set(['manageFiles', 'view'])
}
}
};
}
}
});
function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.SANDBOXED) {
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
res.render('ckeditor/root', {
layout: 'ckeditor/layout',
reactCsrfToken: req.csrfToken(),
mailtrainConfig: JSON.stringify(mailtrainConfig),
scriptFiles: [
getSandboxUrl('mailtrain/ckeditor-root.js')
],
publicPath: getSandboxUrl()
});
});
}
return router;
}
module.exports.getRouter = getRouter;

View file

@ -0,0 +1,58 @@
'use strict';
const routerFactory = require('../lib/router-async');
const passport = require('../lib/passport');
const clientHelpers = require('../lib/client-helpers');
const users = require('../models/users');
const files = require('../models/files');
const fileHelpers = require('../lib/file-helpers');
const templates = require('../models/templates');
const contextHelpers = require('../lib/context-helpers');
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
const { AppType } = require('../../shared/app');
users.registerRestrictedAccessTokenMethod('codeeditor', async ({entityTypeId, entityId}) => {
if (entityTypeId === 'template') {
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
if (tmpl.type === 'codeeditor') {
return {
permissions: {
'template': {
[entityId]: new Set(['manageFiles', 'view'])
}
}
};
}
}
});
function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.SANDBOXED) {
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
res.render('ckeditor/root', {
layout: 'ckeditor/layout',
reactCsrfToken: req.csrfToken(),
mailtrainConfig: JSON.stringify(mailtrainConfig),
scriptFiles: [
getSandboxUrl('mailtrain/codeeditor-root.js')
],
publicPath: getSandboxUrl()
});
});
}
return router;
}
module.exports.getRouter = getRouter;

View file

@ -0,0 +1,65 @@
'use strict';
const routerFactory = require('../lib/router-async');
const passport = require('../lib/passport');
const clientHelpers = require('../lib/client-helpers');
const users = require('../models/users');
const files = require('../models/files');
const fileHelpers = require('../lib/file-helpers');
const templates = require('../models/templates');
const contextHelpers = require('../lib/context-helpers');
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
const { AppType } = require('../../shared/app');
users.registerRestrictedAccessTokenMethod('grapesjs', async ({entityTypeId, entityId}) => {
if (entityTypeId === 'template') {
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
if (tmpl.type === 'grapesjs') {
return {
permissions: {
'template': {
[entityId]: new Set(['viewFiles', 'manageFiles', 'view'])
}
}
};
}
}
});
function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.SANDBOXED) {
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
res.render('grapesjs/root', {
layout: 'grapesjs/layout',
reactCsrfToken: req.csrfToken(),
mailtrainConfig: JSON.stringify(mailtrainConfig),
scriptFiles: [
getSandboxUrl('mailtrain/grapesjs-root.js')
],
publicPath: getSandboxUrl()
});
});
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file', resp => {
return {
data: resp.files.map( f => ({type: 'image', src: f.url}) )
};
});
}
return router;
}
module.exports.getRouter = getRouter;

View file

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

View file

@ -0,0 +1,692 @@
'use strict';
const log = require('../lib/log');
const config = require('config');
const router = require('../lib/router-async').create();
const confirmations = require('../models/confirmations');
const subscriptions = require('../models/subscriptions');
const lists = require('../models/lists');
const fields = require('../models/fields');
const shares = require('../models/shares');
const settings = require('../models/settings');
const { tUI } = require('../lib/translate');
const contextHelpers = require('../lib/context-helpers');
const forms = require('../models/forms');
const {getTrustedUrl} = require('../lib/urls');
const bluebird = require('bluebird');
const { SubscriptionStatus, SubscriptionSource } = require('../../shared/lists');
const openpgp = require('openpgp');
const cors = require('cors');
const cache = require('memory-cache');
const geoip = require('geoip-ultralight');
const passport = require('../lib/passport');
const tools = require('../lib/tools');
const mailHelpers = require('../lib/subscription-mail-helpers');
const interoperableErrors = require('../../shared/interoperable-errors');
const { cleanupFromPost } = require('../lib/helpers');
const originWhitelist = config.cors && config.cors.origins || [];
const corsOptions = {
allowedHeaders: ['Content-Type', 'Origin', 'Accept', 'X-Requested-With'],
methods: ['GET', 'POST'],
optionsSuccessStatus: 200, // IE11 chokes on 204
origin: (origin, callback) => {
if (originWhitelist.includes(origin)) {
callback(null, true);
} else {
const err = new Error('Not allowed by CORS');
err.status = 403;
callback(err);
}
}
};
const corsOrCsrfProtection = (req, res, next) => {
if (req.get('X-Requested-With') === 'XMLHttpRequest') {
cors(corsOptions)(req, res, next);
} else {
passport.csrfProtection(req, res, next);
}
};
async function takeConfirmationAndValidate(req, action, errorFactory) {
const confirmation = await confirmations.takeConfirmation(req.params.cid);
if (!confirmation || confirmation.action !== action) {
throw errorFactory();
}
return confirmation;
}
async function injectCustomFormData(customFormId, viewKey, data) {
function sortAndFilterCustomFieldsBy(key) {
data.customFields = data.customFields.filter(fld => fld[key] !== null);
data.customFields.sort((a, b) => a[key] - b[key]);
}
if (viewKey === 'web_subscribe') {
sortAndFilterCustomFieldsBy('order_subscribe');
} else if (viewKey === 'web_manage') {
sortAndFilterCustomFieldsBy('order_manage');
}
if (!customFormId) {
data.formInputStyle = '@import url(/static/subscription/form-input-style.css);';
return;
}
const form = await forms.getById(contextHelpers.getAdminContext(), customFormId);
data.template.template = form[viewKey] || data.template.template;
data.template.layout = form.layout || data.template.layout;
data.formInputStyle = form.formInputStyle || '@import url(/static/subscription/form-input-style.css);';
const configItems = await settings.get(contextHelpers.getAdminContext(), ['uaCode']);
data.uaCode = configItems.uaCode;
data.customSubscriptionScripts = config.customSubscriptionScripts || [];
}
async function captureFlashMessages(res) {
const renderAsync = bluebird.promisify(res.render.bind(res));
return await renderAsync('subscription/capture-flash-messages', { layout: null });
}
router.getAsync('/confirm/subscribe/:cid', async (req, res) => {
const confirmation = await takeConfirmationAndValidate(req, 'subscribe', () => new interoperableErrors.InvalidConfirmationForSubscriptionError('Request invalid or already completed. If your subscription request is still pending, please subscribe again.'));
const data = confirmation.data;
const meta = {
ip: confirmation.ip,
country: geoip.lookupCountry(confirmation.ip) || null,
updateOfUnsubscribedAllowed: true
};
const subscription = data.subscriptionData;
subscription.email = data.email;
subscription.status = SubscriptionStatus.SUBSCRIBED;
try {
await subscriptions.create(contextHelpers.getAdminContext(), confirmation.list, subscription, SubscriptionSource.SUBSCRIPTION_FORM, meta);
} catch (err) {
if (err instanceof interoperableErrors.DuplicitEmailError) {
throw new interoperableErrors.DuplicitEmailError('Subscription already present'); // This is here to provide some meaningful error message.
} else {
throw err;
}
}
const list = await lists.getById(contextHelpers.getAdminContext(), confirmation.list);
subscription.cid = meta.cid;
await mailHelpers.sendSubscriptionConfirmed(req.language, list, subscription.email, subscription);
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/subscribed-notice');
});
router.getAsync('/confirm/change-address/:cid', async (req, res) => {
const confirmation = await takeConfirmationAndValidate(req, 'change-address', () => new interoperableErrors.InvalidConfirmationForAddressChangeError('Request invalid or already completed. If your address change request is still pending, please change the address again.'));
const list = await lists.getById(contextHelpers.getAdminContext(), confirmation.list);
const data = confirmation.data;
const subscription = await subscriptions.updateAddressAndGet(contextHelpers.getAdminContext(), list.id, data.subscriptionId, data.emailNew);
await mailHelpers.sendSubscriptionConfirmed(req.language, list, data.emailNew, subscription);
req.flash('info', tUI('subscription.emailChanged', req.language));
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/manage/' + subscription.cid);
});
router.getAsync('/confirm/unsubscribe/:cid', async (req, res) => {
const confirmation = await takeConfirmationAndValidate(req, 'unsubscribe', () => new interoperableErrors.InvalidConfirmationForUnsubscriptionError('Request invalid or already completed. If your unsubscription request is still pending, please unsubscribe again.'));
const list = await lists.getById(contextHelpers.getAdminContext(), confirmation.list);
const data = confirmation.data;
const subscription = await subscriptions.unsubscribeByCidAndGet(contextHelpers.getAdminContext(), list.id, data.subscriptionCid, data.campaignCid);
await mailHelpers.sendUnsubscriptionConfirmed(req.language, list, subscription.email, subscription);
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/unsubscribed-notice');
});
async function _renderSubscribe(req, res, list, subscription) {
const data = {};
data.email = subscription && subscription.email;
data.layout = 'subscription/layout';
data.title = list.name;
data.cid = list.cid;
data.csrfToken = req.csrfToken();
data.customFields = await fields.forHbs(contextHelpers.getAdminContext(), list.id, subscription);
data.useEditor = true;
const configItems = await settings.get(contextHelpers.getAdminContext(), ['pgpPrivateKey']);
data.hasPubkey = !!configItems.pgpPrivateKey;
data.template = {
template: 'subscription/web-subscribe.mjml.hbs',
layout: 'subscription/layout.mjml.hbs',
type: 'mjml'
};
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-subscribe', data);
const htmlRenderer = await tools.getTemplate(data.template);
data.isWeb = true;
data.needsJsWarning = true;
data.flashMessages = await captureFlashMessages(res);
const result = htmlRenderer(data);
res.send(result);
}
router.getAsync('/:cid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
if (!list.public_subscribe) {
shares.throwPermissionDenied();
}
const ucid = req.query.cid;
let subscription;
if (ucid) {
try {
subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, ucid);
if (subscription.status === SubscriptionStatus.SUBSCRIBED) {
subscription = null;
}
} catch (err) {
if (err instanceof interoperableErrors.NotFoundError) {
} else {
throw err;
}
}
}
await _renderSubscribe(req, res, list, subscription);
});
router.options('/:cid/subscribe', cors(corsOptions));
router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, async (req, res) => {
if (req.xhr) {
req.needsAPIJSONResponse = true;
}
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
if (!list.public_subscribe) {
shares.throwPermissionDenied();
}
const subscriptionData = await fields.fromPost(contextHelpers.getAdminContext(), list.id, req.body);
const email = cleanupFromPost(req.body.EMAIL);
if (!email) {
if (req.xhr) {
throw new Error('Email address not set');
}
req.flash('danger', tUI('subscription.addressNotSet', req.language));
return await _renderSubscribe(req, res, list, subscriptionData);
}
const emailErr = await tools.validateEmail(email);
if (emailErr) {
const errMsg = tools.validateEmailGetMessage(emailErr, email);
if (req.xhr) {
throw new Error(errMsg);
}
req.flash('danger', errMsg);
subscriptionData.email = email;
return await _renderSubscribe(req, res, list, subscriptionData);
}
// Check if the subscriber seems legit. This is a really simple check, the only requirement is that
// the subscriber has JavaScript turned on and thats it. If Mailtrain gets more targeted then this
// simple check should be replaced with an actual captcha
let subTime = Number(req.body.sub) || 0;
// allow clock skew 24h in the past and 24h to the future
let subTimeTest = !!(subTime > Date.now() - 24 * 3600 * 1000 && subTime < Date.now() + 24 * 3600 * 1000);
let addressTest = !req.body.address;
let testsPass = subTimeTest && addressTest;
let existingSubscription;
try {
existingSubscription = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, email);
} catch (err) {
if (err instanceof interoperableErrors.NotFoundError) {
} else {
throw err;
}
}
if (existingSubscription && existingSubscription.status === SubscriptionStatus.SUBSCRIBED) {
await mailHelpers.sendAlreadySubscribed(req.language, list, email, existingSubscription);
res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '/confirm-subscription-notice');
} else {
const data = {
email,
subscriptionData
};
const confirmCid = await confirmations.addConfirmation(list.id, 'subscribe', req.ip, data);
if (!testsPass) {
log.info('Subscription', 'Confirmation message for %s marked to be skipped (%s)', email, JSON.stringify(data));
} else {
await mailHelpers.sendConfirmSubscription(req.language, list, email, confirmCid, subscriptionData);
}
if (req.xhr) {
return res.status(200).json({
msg: tUI('subscription.confirmSubscription', req.language)
});
}
res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '/confirm-subscription-notice');
}
});
router.options('/:cid/widget', cors(corsOptions));
router.getAsync('/:cid/widget', cors(corsOptions), async (req, res) => {
req.needsAPIJSONResponse = true;
const cached = cache.get(req.path);
if (cached) {
return res.status(200).json(cached);
}
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
const configItems = await settings.get(contextHelpers.getAdminContext(), ['pgpPrivateKey']);
const data = {
title: list.name,
cid: list.cid,
publicKeyUrl: getTrustedUrl('subscription/publickey'),
subscribeUrl: getTrustedUrl(`subscription/${list.cid}/subscribe`),
hasPubkey: !!configItems.pgpPrivateKey,
customFields: await fields.forHbs(contextHelpers.getAdminContext(), list.id),
template: {},
layout: null,
};
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-subscribe', data);
const renderAsync = bluebird.promisify(res.render);
const html = await renderAsync('subscription/widget-subscribe', data);
const response = {
data: {
title: data.title,
cid: data.cid,
html
}
};
cache.put(req.path, response, 30000); // ms
res.status(200).json(response);
});
router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid);
if (subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
const data = {};
data.email = subscription.email;
data.cid = subscription.cid;
data.lcid = req.params.lcid;
data.title = list.name;
data.csrfToken = req.csrfToken();
data.layout = 'data/layout';
data.customFields = await fields.forHbs(contextHelpers.getAdminContext(), list.id, subscription);
data.useEditor = true;
const configItems = await settings.get(contextHelpers.getAdminContext(), ['pgpPrivateKey']);
data.hasPubkey = !!configItems.pgpPrivateKey;
data.template = {
template: 'subscription/web-manage.mjml.hbs',
layout: 'subscription/layout.mjml.hbs',
type: 'mjml'
};
await injectCustomFormData(req.query.fid || list.default_form, 'data/web-manage', data);
const htmlRenderer = await tools.getTemplate(data.template);
data.isWeb = true;
data.needsJsWarning = true;
data.isManagePreferences = true;
data.flashMessages = await captureFlashMessages(res);
res.send(htmlRenderer(data));
});
router.postAsync('/:lcid/manage', passport.parseForm, passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
try {
const subscriptionData = await fields.fromPost(contextHelpers.getAdminContext(), list.id, req.body);
await subscriptions.updateManaged(contextHelpers.getAdminContext(), list.id, req.body.cid, subscriptionData);
} catch (err) {
if (err instanceof interoperableErrors.NotFoundError) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
} else {
throw err;
}
}
res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/updated-notice');
});
router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
if (subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
const data = {};
data.email = subscription.email;
data.cid = subscription.cid;
data.lcid = req.params.lcid;
data.title = list.name;
data.csrfToken = req.csrfToken();
data.template = {
template: 'subscription/web-manage-address.mjml.hbs',
layout: 'subscription/layout.mjml.hbs',
type: 'mjml'
};
await injectCustomFormData(req.query.fid || list.default_form, 'data/web-manage-address', data);
const htmlRenderer = await tools.getTemplate(data.template);
data.isWeb = true;
data.needsJsWarning = true;
data.isManagePreferences = true;
data.flashMessages = await captureFlashMessages(res);
res.send(htmlRenderer(data));
});
router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
const emailNew = cleanupFromPost(req.body['EMAIL_NEW']);
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.body.cid, false);
if (subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
if (subscription.email === emailNew) {
req.flash('info', tUI('subscription.nothingChanged', req.language));
} else {
const emailErr = await tools.validateEmail(emailNew);
if (emailErr) {
const errMsg = tools.validateEmailGetMessage(emailErr, email);
req.flash('danger', errMsg);
} else {
let newSubscription;
try {
newSubscription = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, emailNew, false);
} catch (err) {
if (err instanceof interoperableErrors.NotFoundError) {
} else {
throw err;
}
}
if (newSubscription && newSubscription.status === SubscriptionStatus.SUBSCRIBED) {
await mailHelpers.sendAlreadySubscribed(req.language, list, emailNew, subscription);
} else {
const data = {
subscriptionId: subscription.id,
emailNew
};
const confirmCid = await confirmations.addConfirmation(list.id, 'change-address', req.ip, data);
await mailHelpers.sendConfirmAddressChange(req.language, list, emailNew, confirmCid, subscription);
}
req.flash('info', tUI('subscription.furtherInstructionsSent', req.language));
}
}
res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/manage/' + encodeURIComponent(req.body.cid));
});
router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
const configItems = await settings.get(contextHelpers.getAdminContext(), ['defaultAddress']);
const autoUnsubscribe = req.query.auto === 'yes';
if (autoUnsubscribe) {
await handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, req, res);
} else if (req.query.formTest ||
list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
if (subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
const data = {};
data.email = subscription.email;
data.lcid = req.params.lcid;
data.ucid = req.params.ucid;
data.title = list.name;
data.csrfToken = req.csrfToken();
data.campaign = req.query.c;
data.defaultAddress = configItems.defaultAddress;
data.template = {
template: 'subscription/web-unsubscribe.mjml.hbs',
layout: 'subscription/layout.mjml.hbs',
type: 'mjml'
};
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-unsubscribe', data);
const htmlRenderer = await tools.getTemplate(data.template);
data.isWeb = true;
data.needsJsWarning = true;
data.isManagePreferences = true;
data.flashMessages = await captureFlashMessages(res);
res.send(htmlRenderer(data));
} else { // UnsubscriptionMode.ONE_STEP || UnsubscriptionMode.TWO_STEP || UnsubscriptionMode.MANUAL
await handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, req, res);
}
});
router.postAsync('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
const campaignCid = cleanupFromPost(req.body.campaign);
await handleUnsubscribe(list, req.body.ucid, false, campaignCid, req.ip, req, res);
});
async function handleUnsubscribe(list, subscriptionCid, autoUnsubscribe, campaignCid, ip, req, res) {
if ((list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP || list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) ||
(autoUnsubscribe && (list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM)) ) {
try {
const subscription = await subscriptions.unsubscribeByCidAndGet(contextHelpers.getAdminContext(), list.id, subscriptionCid, campaignCid);
await mailHelpers.sendUnsubscriptionConfirmed(req.language, list, subscription.email, subscription);
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/unsubscribed-notice');
} catch (err) {
if (err instanceof interoperableErrors.NotFoundError) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list'); // This is here to provide some meaningful error message.
}
}
} else {
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid, false);
if (subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
if (list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
const data = {
subscriptionCid,
campaignCid
};
const confirmCid = await confirmations.addConfirmation(list.id, 'unsubscribe', ip, data);
await mailHelpers.sendConfirmUnsubscription(req.language, list, subscription.email, confirmCid, subscription);
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/confirm-unsubscription-notice');
} else { // UnsubscriptionMode.MANUAL
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/manual-unsubscribe-notice');
}
}
}
router.getAsync('/:cid/confirm-subscription-notice', async (req, res) => {
await webNotice('confirm-subscription', req, res);
});
router.getAsync('/:cid/confirm-unsubscription-notice', async (req, res) => {
await webNotice('confirm-unsubscription', req, res);
});
router.getAsync('/:cid/subscribed-notice', async (req, res) => {
await webNotice('subscribed', req, res);
});
router.getAsync('/:cid/updated-notice', async (req, res) => {
await webNotice('updated', req, res);
});
router.getAsync('/:cid/unsubscribed-notice', async (req, res) => {
await webNotice('unsubscribed', req, res);
});
router.getAsync('/:cid/manual-unsubscribe-notice', async (req, res) => {
await webNotice('manual-unsubscribe', req, res);
});
router.postAsync('/publickey', passport.parseForm, async (req, res) => {
const configItems = await settings.get(contextHelpers.getAdminContext(), ['pgpPassphrase', 'pgpPrivateKey']);
if (!configItems.pgpPrivateKey) {
const err = new Error('Public key is not set');
err.status = 404;
throw err;
}
let privKey;
try {
privKey = openpgp.key.readArmored(configItems.pgpPrivateKey).keys[0];
if (configItems.pgpPassphrase && !privKey.decrypt(configItems.pgpPassphrase)) {
privKey = false;
}
} catch (E) {
// just ignore if failed
}
if (!privKey) {
const err = new Error('Public key is not set');
err.status = 404;
throw err;
}
const pubkey = privKey.toPublic().armor();
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename=public.asc'
});
res.end(pubkey);
});
async function webNotice(type, req, res) {
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
const configItems = await settings.get(contextHelpers.getAdminContext(), ['defaultHomepage', 'adminEmail']);
const data = {
title: list.name,
homepage: configItems.defaultHomepage || getTrustedUrl(),
contactAddress: list.from_email || configItems.adminEmail,
template: {
template: 'subscription/web-' + type + '-notice.mjml.hbs',
layout: 'subscription/layout.mjml.hbs',
type: 'mjml'
}
};
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-' + type + '-notice', data);
const htmlRenderer = await tools.getTemplate(data.template);
data.isWeb = true;
data.isConfirmNotice = true; // FIXME: Not sure what this does. Check it in a browser with disabled JS
data.isManagePreferences = true;
data.flashMessages = await captureFlashMessages(res);
res.send(htmlRenderer(data));
}
module.exports = router;

View file

@ -0,0 +1,59 @@
'use strict';
const passport = require('../lib/passport');
const shares = require('../models/shares');
const contextHelpers = require('../lib/context-helpers');
const router = require('../lib/router-async').create();
const subscriptions = require('../models/subscriptions');
const {castToInteger} = require('../lib/helpers');
const stringify = require('csv-stringify')
const fields = require('../models/fields');
const lists = require('../models/lists');
const moment = require('moment');
router.getAsync('/export/:listId/:segmentId', passport.loggedIn, async (req, res) => {
const listId = castToInteger(req.params.listId);
const segmentId = castToInteger(req.params.segmentId);
const flds = await fields.list(req.context, listId);
const columns = [
{key: 'cid', header: 'cid'},
{key: 'hash_email', header: 'HASH_EMAIL'},
{key: 'email', header: 'EMAIL'},
];
for (const fld of flds) {
if (fld.column) {
columns.push({
key: fld.column,
header: fld.key
});
}
}
const list = await lists.getById(req.context, listId);
const headers = {
'Content-Disposition': `attachment;filename=subscriptions-${list.cid}-${segmentId}-${moment().toISOString()}.csv`,
'Content-Type': 'text/csv'
};
res.set(headers);
const stringifier = stringify({
columns,
header: true,
delimiter: ','
});
stringifier.pipe(res);
for await (const subscription of subscriptions.listIterator(req.context, listId, segmentId, false)) {
stringifier.write(subscription);
}
stringifier.end();
});
module.exports = router;

240
server/routes/webhooks.js Normal file
View file

@ -0,0 +1,240 @@
'use strict';
const router = require('../lib/router-async').create();
const request = require('request-promise');
const campaigns = require('../models/campaigns');
const sendConfigurations = require('../models/send-configurations');
const contextHelpers = require('../lib/context-helpers');
const {SubscriptionStatus} = require('../../shared/lists');
const {MailerType} = require('../../shared/send-configurations');
const log = require('../lib/log');
const multer = require('multer');
const uploads = multer();
router.postAsync('/aws', async (req, res) => {
if (typeof req.body === 'string') {
req.body = JSON.parse(req.body);
}
switch (req.body.Type) {
case 'SubscriptionConfirmation':
if (req.body.SubscribeURL) {
await request(req.body.SubscribeURL);
break;
} else {
const err = new Error('SubscribeURL not set');
err.status = 400;
throw err;
}
case 'Notification':
if (req.body.Message) {
if (typeof req.body.Message === 'string') {
req.body.Message = JSON.parse(req.body.Message);
}
if (req.body.Message.mail && req.body.Message.mail.messageId) {
const message = await campaigns.getMessageByResponseId(req.body.Message.mail.messageId);
if (!message) {
return;
}
switch (req.body.Message.notificationType) {
case 'Bounce':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, req.body.Message.bounce.bounceType === 'Permanent');
log.verbose('AWS', 'Marked message %s as bounced', req.body.Message.mail.messageId);
break;
case 'Complaint':
if (req.body.Message.complaint) {
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true);
log.verbose('AWS', 'Marked message %s as complaint', req.body.Message.mail.messageId);
}
break;
}
}
}
break;
}
res.json({
success: true
});
});
router.postAsync('/sparkpost', async (req, res) => {
const events = [].concat(req.body || []); // This is just a cryptic way getting an array regardless whether req.body is empty, one item, or array
for (const curEvent of events) {
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;
} else {
continue;
}
const message = await campaigns.getMessageByCid(evt.campaign_id);
if (!message) {
continue;
}
switch (evt.type) {
case 'bounce':
// https://support.sparkpost.com/customer/portal/articles/1929896
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, [1, 10, 25, 30, 50].indexOf(Number(evt.bounce_class)) >= 0);
log.verbose('Sparkpost', 'Marked message %s as bounced', evt.campaign_id);
break;
case 'spam_complaint':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true);
log.verbose('Sparkpost', 'Marked message %s as complaint', evt.campaign_id);
break;
case 'link_unsubscribe':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.UNSUBSCRIBED, true);
log.verbose('Sparkpost', 'Marked message %s as unsubscribed', evt.campaign_id);
break;
}
}
return res.json({
success: true
});
});
router.postAsync('/sendgrid', async (req, res) => {
let events = [].concat(req.body || []);
for (const evt of events) {
if (!evt) {
continue;
}
const message = await campaigns.getMessageByCid(evt.campaign_id);
if (!message) {
continue;
}
switch (evt.event) {
case 'bounce':
// https://support.sparkpost.com/customer/portal/articles/1929896
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('Sendgrid', 'Marked message %s as bounced', evt.campaign_id);
break;
case 'spamreport':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true);
log.verbose('Sendgrid', 'Marked message %s as complaint', evt.campaign_id);
break;
case 'group_unsubscribe':
case 'unsubscribe':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.UNSUBSCRIBED, true);
log.verbose('Sendgrid', 'Marked message %s as unsubscribed', evt.campaign_id);
break;
}
}
return res.json({
success: true
});
});
router.postAsync('/mailgun', uploads.any(), async (req, res) => {
const evt = req.body;
const message = await campaigns.getMessageByCid([].concat(evt && evt.campaign_id || []).shift());
if (message) {
switch (evt.event) {
case 'bounced':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('Mailgun', 'Marked message %s as bounced', evt.campaign_id);
break;
case 'complained':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true);
log.verbose('Mailgun', 'Marked message %s as complaint', evt.campaign_id);
break;
case 'unsubscribed':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.UNSUBSCRIBED, true);
log.verbose('Mailgun', 'Marked message %s as unsubscribed', evt.campaign_id);
break;
}
}
return res.json({
success: true
});
});
router.postAsync('/zone-mta', async (req, res) => {
if (typeof req.body === 'string') {
req.body = JSON.parse(req.body);
}
if (req.body.id) {
const message = await campaigns.getMessageByCid(req.body.id);
if (message) {
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('ZoneMTA', 'Marked message %s as bounced', req.body.id);
}
}
res.json({
success: true
});
});
router.postAsync('/zone-mta/sender-config/:sendConfigurationCid', async (req, res) => {
if (!req.query.api_token) {
return res.json({
error: 'api_token value not set'
});
}
const sendConfiguration = await sendConfigurations.getByCid(contextHelpers.getAdminContext(), req.params.sendConfigurationCid, false, true);
if (sendConfiguration.mailer_type !== MailerType.ZONE_MTA || sendConfiguration.mailer_settings.dkimApiKey !== req.query.api_token) {
return res.json({
error: 'invalid api_token value'
});
}
const dkimDomain = sendConfiguration.mailer_settings.dkimDomain;
const dkimSelector = (sendConfiguration.mailer_settings.dkimSelector || '').trim();
const dkimPrivateKey = (sendConfiguration.mailer_settings.dkimPrivateKey || '').trim();
if (!dkimSelector || !dkimPrivateKey) {
// empty response
return res.json({});
}
const from = (req.body.from || '').trim();
const domain = from.split('@').pop().toLowerCase().trim();
res.json({
dkim: {
keys: [{
domainName: dkimDomain || domain,
keySelector: dkimSelector,
privateKey: dkimPrivateKey
}]
}
});
});
module.exports = router;