Refactored subscriptions. Not even executed.

This commit is contained in:
Tomas Bures 2017-12-30 12:23:16 +01:00
parent b22a87e712
commit 6c5c47ac2e
3 changed files with 326 additions and 387 deletions

View file

@ -29,7 +29,7 @@ async function sendSubscriptionConfirmed(list, email, subscription) {
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
};
await sendMail(list, email, 'subscription_confirmed', _('%s: Subscription Confirmed'), relativeUrls, {}, subscription);
await _sendMail(list, email, 'subscription_confirmed', _('%s: Subscription Confirmed'), relativeUrls, {}, subscription);
}
async function sendAlreadySubscribed(list, email, subscription) {
@ -40,7 +40,7 @@ async function sendAlreadySubscribed(list, email, subscription) {
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
};
await sendMail(list, email, 'already_subscribed', _('%s: Email Address Already Registered'), relativeUrls, mailOpts, subscription);
await _sendMail(list, email, 'already_subscribed', _('%s: Email Address Already Registered'), relativeUrls, mailOpts, subscription);
}
async function sendConfirmAddressChange(list, email, cid, subscription) {
@ -50,7 +50,7 @@ async function sendConfirmAddressChange(list, email, cid, subscription) {
const relativeUrls = {
confirmUrl: '/subscription/confirm/change-address/' + cid
};
await sendMail(list, email, 'confirm_address_change', _('%s: Please Confirm Email Change in Subscription'), relativeUrls, mailOpts, subscription);
await _sendMail(list, email, 'confirm_address_change', _('%s: Please Confirm Email Change in Subscription'), relativeUrls, mailOpts, subscription);
}
async function sendConfirmSubscription(list, email, cid, subscription) {
@ -60,7 +60,7 @@ async function sendConfirmSubscription(list, email, cid, subscription) {
const relativeUrls = {
confirmUrl: '/subscription/confirm/subscribe/' + cid
};
await sendMail(list, email, 'confirm_subscription', _('%s: Please Confirm Subscription'), relativeUrls, mailOpts, subscription);
await _sendMail(list, email, 'confirm_subscription', _('%s: Please Confirm Subscription'), relativeUrls, mailOpts, subscription);
}
async function sendConfirmUnsubscription(list, email, cid, subscription) {
@ -70,14 +70,14 @@ async function sendConfirmUnsubscription(list, email, cid, subscription) {
const relativeUrls = {
confirmUrl: '/subscription/confirm/unsubscribe/' + cid
};
await sendMail(list, email, 'confirm_unsubscription', _('%s: Please Confirm Unsubscription'), relativeUrls, mailOpts, subscription);
await _sendMail(list, email, 'confirm_unsubscription', _('%s: Please Confirm Unsubscription'), relativeUrls, mailOpts, subscription);
}
async function sendUnsubscriptionConfirmed(list, email, subscription) {
const relativeUrls = {
subscribeUrl: '/subscription/' + list.cid + '?cid=' + subscription.cid
};
await sendMail(list, email, 'unsubscription_confirmed', _('%s: Unsubscription Confirmed'), relativeUrls, {}, subscription);
await _sendMail(list, email, 'unsubscription_confirmed', _('%s: Unsubscription Confirmed'), relativeUrls, {}, subscription);
}
function getDisplayName(flds, subscription) {
@ -110,7 +110,7 @@ function getDisplayName(flds, subscription) {
}
}
async function sendMail(list, email, template, subject, relativeUrls, mailOpts, subscription) {
async function _sendMail(list, email, template, subject, relativeUrls, mailOpts, subscription) {
console.log(subscription);
const flds = await fields.list(contextHelpers.getAdminContext(), list.id);

View file

@ -73,10 +73,14 @@ fieldTypes.birthday = {
function getTableName(listId) {
function getSubscriptionTableName(listId) {
return `subscription__${listId}`;
}
function getCampaignTableName(campaignId) {
return `campaign__${campaignId}`;
}
async function getGroupedFieldsMap(tx, listId) {
const groupedFields = await fields.listGroupedTx(tx, listId);
const result = {};
@ -191,37 +195,60 @@ async function hashByList(listId, entity) {
});
}
async function _getBy(context, listId, key, value) {
async function _getStatusBy(context, listId, key, value) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
const entity = await tx(getTableName(listId)).where(key, value).first();
const entity = await tx(getSubscriptionTableName(listId)).where(key, value).select(['status']).first();
if (!entity) {
throw new interoperableErrors.NotFoundError();
}
return entity.status;
});
}
async function getStatusByCid(context, listId, cid) {
return await _getStatusBy(context, listId, 'cid', cid);
}
async function _getBy(context, listId, key, value, grouped) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
const entity = await tx(getSubscriptionTableName(listId)).where(key, value).first();
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
groupSubscription(groupedFieldsMap, entity);
if (grouped) {
groupSubscription(groupedFieldsMap, entity);
}
return entity;
});
}
async function getById(context, listId, id) {
return await _getBy(context, listId, 'id', id);
async function getById(context, listId, id, grouped = true) {
return await _getBy(context, listId, 'id', id, grouped);
}
async function getByEmail(context, listId, email) {
return await _getBy(context, listId, 'email', email);
async function getByEmail(context, listId, email, grouped = true) {
return await _getBy(context, listId, 'email', email, grouped);
}
async function getByCid(context, listId, cid) {
return await _getBy(context, listId, 'cid', cid);
async function getByCid(context, listId, cid, grouped = true) {
return await _getBy(context, listId, 'cid', cid, grouped);
}
async function listDTAjax(context, listId, segmentId, params) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
const listTable = getTableName(listId);
const listTable = getSubscriptionTableName(listId);
// All the data transformation below is to reuse ajaxListTx and groupSubscription methods so as to keep the code DRY
// We first construct the columns to contain all which is supposed to be show and extraColumns which contain
@ -326,7 +353,7 @@ async function list(context, listId) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
const entities = await tx(getTableName(listId));
const entities = await tx(getSubscriptionTableName(listId));
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
@ -344,7 +371,7 @@ async function serverValidate(context, listId, data) {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
if (data.email) {
const existingKeyQuery = tx(getTableName(listId)).where('email', data.email);
const existingKeyQuery = tx(getSubscriptionTableName(listId)).where('email', data.email);
if (data.id) {
existingKeyQuery.whereNot('id', data.id);
@ -363,7 +390,7 @@ async function serverValidate(context, listId, data) {
async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, isCreate) {
enforce(entity.email, 'Email must be set');
const existingWithKeyQuery = tx(getTableName(listId)).where('email', entity.email);
const existingWithKeyQuery = tx(getSubscriptionTableName(listId)).where('email', entity.email);
if (!isCreate) {
existingWithKeyQuery.whereNot('id', entity.id);
@ -401,7 +428,7 @@ async function create(context, listId, entity, meta = {}) {
filteredEntity.opt_in_country = meta.country;
filteredEntity.imported = meta.imported || false;
const ids = await tx(getTableName(listId)).insert(filteredEntity);
const ids = await tx(getSubscriptionTableName(listId)).insert(filteredEntity);
const id = ids[0];
if (entity.status === SubscriptionStatus.SUBSCRIBED) {
@ -416,7 +443,7 @@ async function updateWithConsistencyCheck(context, listId, entity) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
const existing = await tx(getTableName(listId)).where('id', entity.id).first();
const existing = await tx(getSubscriptionTableName(listId)).where('id', entity.id).first();
if (!existing) {
throw new interoperableErrors.NotFoundError();
}
@ -441,7 +468,7 @@ async function updateWithConsistencyCheck(context, listId, entity) {
filteredEntity.status_change = new Date();
}
await tx(getTableName(listId)).where('id', entity.id).update(filteredEntity);
await tx(getSubscriptionTableName(listId)).where('id', entity.id).update(filteredEntity);
let countIncrement = 0;
@ -460,12 +487,12 @@ async function updateWithConsistencyCheck(context, listId, entity) {
async function removeTx(tx, context, listId, id) {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
const existing = await tx(getTableName(listId)).where('id', id).first();
const existing = await tx(getSubscriptionTableName(listId)).where('id', id).first();
if (!existing) {
throw new interoperableErrors.NotFoundError();
}
await tx(getTableName(listId)).where('id', id).del();
await tx(getSubscriptionTableName(listId)).where('id', id).del();
if (existing.status === SubscriptionStatus.SUBSCRIBED) {
await tx('lists').where('id', listId).decrement('subscribers', 1);
@ -478,23 +505,37 @@ async function remove(context, listId, id) {
});
}
async function unsubscribeAndGet(context, listId, subscriptionId) {
async function unsubscribeByCidAndGet(context, listId, subscriptionCid, campaignCid) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
const existing = await tx(getTableName(listId)).where('id', subscriptionId).first();
if (!existing) {
const existing = await tx(getSubscriptionTableName(listId)).where('cid', subscriptionCid).first();
if (!(existing && existing.status === SubscriptionStatus.SUBSCRIBED)) {
throw new interoperableErrors.NotFoundError();
}
if (existing.status === SubscriptionStatus.SUBSCRIBED) {
existing.status = SubscriptionStatus.UNSUBSCRIBED;
existing.status = SubscriptionStatus.UNSUBSCRIBED;
await tx(getTableName(listId)).where('id', subscriptionId).update({
status: SubscriptionStatus.UNSUBSCRIBED
});
await tx(getSubscriptionTableName(listId)).where('cid', subscriptionCid).update({
status: SubscriptionStatus.UNSUBSCRIBED
});
await tx('lists').where('id', listId).decrement('subscribers', 1);
await tx('lists').where('id', listId).decrement('subscribers', 1);
if (campaignCid) {
const campaign = await tx('campaigns').where('cid', campaignCid);
const subscriptionInCampaign = await tx(getCampaignTableName(campaign.id)).where({subscription: existing.id, list: listId});
if (!subscriptionInCampaign) {
throw new Error('Invalid campaign.')
}
if (subscriptionInCampaign.status === SubscriptionStatus.SUBSCRIBED) {
await tx('campaigns').where('id', campaign.id).increment('unsubscribed', 1);
await tx(getCampaignTableName(campaign.id)).where({subscription: existing.id, list: listId}).update({
status: SubscriptionStatus.UNSUBSCRIBED
});
}
}
return existing;
@ -505,12 +546,12 @@ async function updateAddressAndGet(context, listId, subscriptionId, emailNew) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
const existing = await tx(getTableName(listId)).where('id', subscriptionId).first();
const existing = await tx(getSubscriptionTableName(listId)).where('id', subscriptionId).first();
if (!existing) {
throw new interoperableErrors.NotFoundError();
}
await tx(getTableName(listId)).where('id', subscriptionId).update({
await tx(getSubscriptionTableName(listId)).where('id', subscriptionId).update({
email: emailNew
});
@ -519,17 +560,46 @@ async function updateAddressAndGet(context, listId, subscriptionId, emailNew) {
});
}
async function updateManagedUngrouped(context, listId, entity) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
const existing = await tx(getSubscriptionTableName(listId)).where('id', entity.id).first();
if (!existing) {
throw new interoperableErrors.NotFoundError();
}
const flds = await fields.listTx(tx, listId);
const update = {};
for (const fld of flds) {
if (fld.order_manage) {
if (!fld.group) { // fieldTypes is primarily meant only for groupedFields, so we don't try it for fields that would be grouped (i.e. option), because there is nothing to be done for them anyway
fieldTypes[fld.type].afterJSON(fld, entity);
}
update[fld.column] = entity[fld.column];
}
}
await tx(getSubscriptionTableName(listId)).where('id', entity.id).update(update);
});
}
module.exports = {
hashByList,
getById,
getByCid,
getByEmail,
getStatusByCid,
list,
listDTAjax,
serverValidate,
create,
updateWithConsistencyCheck,
remove,
unsubscribeAndGet,
updateAddressAndGet
unsubscribeByCidAndGet,
updateAddressAndGet,
updateManagedUngrouped
};

View file

@ -12,6 +12,8 @@ const _ = require('../lib/translate')._;
const contextHelpers = require('../lib/context-helpers');
const forms = require('../models/forms');
const { SubscriptionStatus } = require('../shared/lists');
const openpgp = require('openpgp');
const util = require('util');
const cors = require('cors');
@ -153,12 +155,13 @@ router.getAsync('/confirm/subscribe/:cid', async (req, res) => {
const list = await lists.getById(contextHelpers.getAdminContext(), confirmation.list);
await mailHelpers.sendSubscriptionConfirmed(list, subscription.email, subscription);
res.redirect('/subscription/' + list.cid + '/subscribed-notice');
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);
@ -166,19 +169,20 @@ router.getAsync('/confirm/change-address/:cid', async (req, res) => {
await mailHelpers.sendSubscriptionConfirmed(list, data.emailNew, subscription);
req.flash('info', _('Email address changed'));
res.redirect('/subscription/' + list.cid + '/manage/' + subscription.cid);
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.unsubscribeAndGet(contextHelpers.getAdminContext(), list.id, data.subscriptionId);
const subscription = await subscriptions.unsubscribeByCidAndGet(contextHelpers.getAdminContext(), list.id, data.subscriptionCid, data.campaignCid);
await mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription);
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/unsubscribed-notice');
});
@ -199,7 +203,7 @@ router.getAsync('/:cid', passport.csrfProtection, async (req, res) => {
let subscription;
if (ucid) {
subscription = await subscriptions.getById(contextHelpers.getAdminContext(), list.id, ucid);
subscription = await subscriptions.getById(contextHelpers.getAdminContext(), list.id, ucid, false);
}
data.customFields = fields.getRow(contextHelpers.getAdminContext(), list.id, subscription);
@ -215,7 +219,7 @@ router.getAsync('/:cid', passport.csrfProtection, async (req, res) => {
layout: 'subscription/layout.mjml.hbs'
};
await injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribe', data);
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-subscribe', data);
const htmlRenderer = await getMjmlTemplate(data.template);
@ -251,7 +255,7 @@ router.getAsync('/:cid/widget', cors(corsOptions), async (req, res) => {
layout: null,
};
await injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribe', data);
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);
@ -278,7 +282,6 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
req.needsAPIJSONResponse = true;
}
if (!email) {
if (req.xhr) {
throw new Error('Email address not set');
@ -313,7 +316,6 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
throw new interoperableErrors.SubscriptionNotAllowedError('The list does not allow public subscriptions.');
}
let subscriptionData = {};
Object.keys(req.body).forEach(key => {
if (key !== 'email' && key.charAt(0) !== '_') {
@ -321,11 +323,11 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
}
});
const subscription = subscriptions.getByEmail(list.id, email)
const subscription = subscriptions.getByEmail(list.id, email, false)
if (subscription && subscription.status === subscriptions.Status.SUBSCRIBED) {
if (subscription && subscription.status === SubscriptionStatus.SUBSCRIBED) {
await mailHelpers.sendAlreadySubscribed(list, email, subscription);
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '/confirm-subscription-notice');
} else {
const data = {
@ -346,16 +348,16 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
msg: _('Please Confirm Subscription')
});
}
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '/confirm-subscription-notice');
}
});
router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid);
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid);
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
if (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED) {
throw new Error(_('Subscription not found in this list'));
if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
subscription.lcid = req.params.lcid;
@ -377,7 +379,7 @@ router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res)
layout: 'subscription/layout.mjml.hbs'
};
await injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-manage', subscription);
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-manage', subscription);
const htmlRenderer = await getMjmlTemplate(data.template);
@ -391,411 +393,278 @@ router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res)
router.postAsync('/:lcid/manage', passport.parseForm, passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid);
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.body.cid);
const status = await subscriptions.getStatusByCid(contextHelpers.getAdminContext(), list.id, req.body.cid);
if (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED) {
throw new Error(_('Subscription not found in this list'));
if (status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
await subscriptions.updateManagedUngrouped(contextHelpers.getAdminContext(), list.id, req.body)
delete req.body.email; // email change is not allowed
delete req.body.status; // status change is not allowed
// FIXME - az sem
// FIXME, allow update of only fields that have order_manage
await subscriptions.updateWithConsistencyCheck(contextHelpers.getAdminContext(), list.id, subscription)
subscriptions.update(list.id, subscription.cid, req.body, false, err => {
if (err) {
return next(err);
}
res.redirect('/subscription/' + req.params.lcid + '/updated-notice');
});
res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/updated-notice');
});
router.get('/:lcid/manage-address/:ucid', passport.csrfProtection, (req, res, next) => {
lists.getByCid(req.params.lcid, (err, list) => {
if (!err && !list) {
err = new Error(_('Selected list not found'));
err.status = 404;
}
router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid);
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
if (err) {
return next(err);
}
if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
settings.list(['defaultAddress', 'defaultPostaddress'], (err, configItems) => {
if (err) {
return next(err);
}
const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
err = new Error(_('Subscription not found in this list'));
err.status = 404;
}
subscription.lcid = req.params.lcid;
subscription.title = list.name;
subscription.csrfToken = req.csrfToken();
subscription.defaultAddress = configItems.defaultAddress;
subscription.defaultPostaddress = configItems.defaultPostaddress;
subscription.lcid = req.params.lcid;
subscription.title = list.name;
subscription.csrfToken = req.csrfToken();
subscription.defaultAddress = configItems.defaultAddress;
subscription.defaultPostaddress = configItems.defaultPostaddress;
subscription.template = {
template: 'subscription/web-manage-address.mjml.hbs',
layout: 'subscription/layout.mjml.hbs'
};
subscription.template = {
template: 'subscription/web-manage-address.mjml.hbs',
layout: 'subscription/layout.mjml.hbs'
};
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-manage-address', subscription);
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-manage-address', subscription, (err, data) => {
if (err) {
return next(err);
}
const htmlRenderer = await getMjmlTemplate(data.template);
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
if (err) {
return next(err);
}
data.isWeb = true;
data.needsJsWarning = true;
data.isManagePreferences = true;
data.flashMessages = await captureFlashMessages(res);
helpers.captureFlashMessages(req, res, (err, flash) => {
if (err) {
return next(err);
}
data.isWeb = true;
data.needsJsWarning = true;
data.flashMessages = flash;
res.send(htmlRenderer(data));
});
});
});
});
});
});
res.send(htmlRenderer(data));
});
router.post('/:lcid/manage-address', passport.parseForm, passport.csrfProtection, (req, res, next) => {
lists.getByCid(req.params.lcid, (err, list) => {
if (!err && !list) {
err = new Error(_('Selected list not found'));
err.status = 404;
}
if (err) {
return next(err);
}
router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid);
let bodyData = tools.convertKeys(req.body); // This is here to convert "email-new" to "emailNew"
const emailOld = (bodyData.email || '').toString().trim();
const emailNew = (bodyData.emailNew || '').toString().trim();
const emailNew = (req.body['email-new'] || '').toString().trim();
if (emailOld === emailNew) {
req.flash('info', _('Nothing seems to be changed'));
res.redirect('/subscription/' + req.params.lcid + '/manage/' + req.body.cid);
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.body.cid, false);
if (status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
if (subscription.email === emailNew) {
req.flash('info', _('Nothing seems to be changed'));
} else {
const emailErr = await tools.validateEmail(emailNew);
if (emailErr) {
req.flash('danger', emailErr.message);
} else {
subscriptions.updateAddressCheck(list, req.body.cid, emailNew, req.ip, (err, subscription, newEmailAvailable) => {
if (err) {
return next(err);
}
const newSubscription = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, emailNew, false);
function sendWebResponse(err) {
if (err) {
return next(err);
}
req.flash('info', _('An email with further instructions has been sent to the provided address'));
res.redirect('/subscription/' + req.params.lcid + '/manage/' + req.body.cid);
}
if (newEmailAvailable) {
const data = {
subscriptionId: subscription.id,
emailNew
};
confirmations.addConfirmation(list.id, 'change-address', req.ip, data, (err, confirmCid) => {
if (err) {
return next(err);
}
mailHelpers.sendConfirmAddressChange(list, emailNew, confirmCid, subscription, sendWebResponse);
});
} else {
mailHelpers.sendAlreadySubscribed(list, emailNew, subscription, sendWebResponse);
}
});
}
});
});
router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next) => {
lists.getByCid(req.params.lcid, (err, list) => {
if (!err && !list) {
err = new Error(_('Selected list not found'));
err.status = 404;
}
if (err) {
return next(err);
}
settings.list(['defaultAddress', 'defaultPostaddress'], (err, configItems) => {
if (err) {
return next(err);
if (newSubscription && newSubscription.status === SubscriptionStatus.SUBSCRIBED) {
await mailHelpers.sendAlreadySubscribed(list, emailNew, subscription);
} else {
const confirmCid = await confirmations.addConfirmation(list.id, 'change-address', req.ip, data);
await mailHelpers.sendConfirmAddressChange(list, emailNew, confirmCid, subscription, sendWebResponse);
}
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
err = new Error(_('Subscription not found in this list'));
err.status = 404;
}
req.flash('info', _('An email with further instructions has been sent to the provided address'));
}
}
if (err) {
return next(err);
}
const autoUnsubscribe = req.query.auto === 'yes';
if (autoUnsubscribe) {
handleUnsubscribe(list, subscription, autoUnsubscribe, req.query.c, req.ip, res, next);
} else if (req.query.formTest ||
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
subscription.lcid = req.params.lcid;
subscription.ucid = req.params.ucid;
subscription.title = list.name;
subscription.csrfToken = req.csrfToken();
subscription.campaign = req.query.c;
subscription.defaultAddress = configItems.defaultAddress;
subscription.defaultPostaddress = configItems.defaultPostaddress;
subscription.template = {
template: 'subscription/web-unsubscribe.mjml.hbs',
layout: 'subscription/layout.mjml.hbs'
};
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-unsubscribe', subscription, (err, data) => {
if (err) {
return next(err);
}
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
if (err) {
return next(err);
}
helpers.captureFlashMessages(req, res, (err, flash) => {
if (err) {
return next(err);
}
data.isWeb = true;
data.flashMessages = flash;
res.send(htmlRenderer(data));
});
});
});
} else { // UnsubscriptionMode.ONE_STEP || UnsubscriptionMode.TWO_STEP || UnsubscriptionMode.MANUAL
handleUnsubscribe(list, subscription, autoUnsubscribe, req.query.c, req.ip, res, next);
}
});
});
});
res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/manage/' + encodeURIComponent(req.body.cid));
});
router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (req, res, next) => {
lists.getByCid(req.params.lcid, (err, list) => {
if (!err && !list) {
err = new Error(_('Selected list not found'));
err.status = 404;
router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid);
const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
const autoUnsubscribe = req.query.auto === 'yes';
if (autoUnsubscribe) {
handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, res, next);
} else if (req.query.formTest ||
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
if (err) {
return next(err);
}
subscription.lcid = req.params.lcid;
subscription.ucid = req.params.ucid;
subscription.title = list.name;
subscription.csrfToken = req.csrfToken();
subscription.campaign = req.query.c;
subscription.defaultAddress = configItems.defaultAddress;
subscription.defaultPostaddress = configItems.defaultPostaddress;
const campaignId = (req.body.campaign || '').toString().trim() || false;
subscription.template = {
template: 'subscription/web-unsubscribe.mjml.hbs',
layout: 'subscription/layout.mjml.hbs'
};
subscriptions.get(list.id, req.body.ucid, (err, subscription) => {
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
err = new Error(_('Subscription not found in this list'));
err.status = 404;
}
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-unsubscribe', subscription);
if (err) {
return next(err);
}
const htmlRenderer = await getMjmlTemplate(data.template);
handleUnsubscribe(list, subscription, false, campaignId, req.ip, res, next);
});
});
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, res);
}
});
function handleUnsubscribe(list, subscription, autoUnsubscribe, campaignId, ip, res, next) {
router.postAsync('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid);
const campaignCid = (req.body.campaign || '').toString().trim() || false;
await handleUnsubscribe(list, req.body.ucid, false, campaignCid, req.ip, res);
});
async function handleUnsubscribe(list, subscriptionCid, autoUnsubscribe, campaignCid, ip, res) {
if ((list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) ||
(autoUnsubscribe && (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM)) ) {
subscriptions.changeStatus(list.id, subscription.id, campaignId, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
if (err) {
return next(err);
try {
const subscription = await subscriptions.unsubscribeByCidAndGet(contextHelpers.getAdminContext(), list.id, subscriptionCid, campaignCid);
await mailHelpers.sendUnsubscriptionConfirmed(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.
}
}
// TODO: Shall we do anything with "found"?
} else {
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid, false);
mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription, err => {
if (err) {
return next(err);
}
if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
});
});
if (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
} else if (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
const data = {
subscriptionCid,
campaignCid
};
const data = {
subscriptionId: subscription.id,
campaignId
};
const confirmCid = await confirmations.addConfirmation(list.id, 'unsubscribe', ip, data);
await mailHelpers.sendConfirmUnsubscription(list, subscription.email, confirmCid, subscription);
confirmations.addConfirmation(list.id, 'unsubscribe', ip, data, (err, confirmCid) => {
if (err) {
return next(err);
}
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/confirm-unsubscription-notice');
mailHelpers.sendConfirmUnsubscription(list, subscription.email, confirmCid, subscription, err => {
if (err) {
return next(err);
}
res.redirect('/subscription/' + list.cid + '/confirm-unsubscription-notice');
});
});
} else { // UnsubscriptionMode.MANUAL
res.redirect('/subscription/' + list.cid + '/manual-unsubscribe-notice');
} else { // UnsubscriptionMode.MANUAL
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/manual-unsubscribe-notice');
}
}
}
router.get('/:cid/confirm-subscription-notice', (req, res, next) => {
webNotice('confirm-subscription', req, res, next);
router.getAsync('/:cid/confirm-subscription-notice', async (req, res) => {
await webNotice('confirm-subscription', req, res);
});
router.get('/:cid/confirm-unsubscription-notice', (req, res, next) => {
webNotice('confirm-unsubscription', req, res, next);
router.getAsync('/:cid/confirm-unsubscription-notice', async (req, res) => {
await webNotice('confirm-unsubscription', req, res);
});
router.get('/:cid/subscribed-notice', (req, res, next) => {
webNotice('subscribed', req, res, next);
router.getAsync('/:cid/subscribed-notice', async (req, res) => {
await webNotice('subscribed', req, res);
});
router.get('/:cid/updated-notice', (req, res, next) => {
webNotice('updated', req, res, next);
router.getAsync('/:cid/updated-notice', async (req, res) => {
await webNotice('updated', req, res);
});
router.get('/:cid/unsubscribed-notice', (req, res, next) => {
webNotice('unsubscribed', req, res, next);
router.getAsync('/:cid/unsubscribed-notice', async (req, res) => {
await webNotice('unsubscribed', req, res);
});
router.get('/:cid/manual-unsubscribe-notice', (req, res, next) => {
webNotice('manual-unsubscribe', req, res, next);
router.getAsync('/:cid/manual-unsubscribe-notice', async (req, res) => {
await webNotice('manual-unsubscribe', req, res);
});
router.post('/publickey', passport.parseForm, (req, res, next) => {
settings.list(['pgpPassphrase', 'pgpPrivateKey'], (err, configItems) => {
if (err) {
return next(err);
}
if (!configItems.pgpPrivateKey) {
err = new Error(_('Public key is not set'));
err.status = 404;
return next(err);
router.postAsync('/publickey', passport.parseForm, async (req, res) => {
const configItems = await settings.get(['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
}
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;
}
if (!privKey) {
err = new Error(_('Public key is not set'));
err.status = 404;
return next(err);
}
const pubkey = privKey.toPublic().armor();
let pubkey = privKey.toPublic().armor();
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename=public.asc'
});
res.end(pubkey);
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename=public.asc'
});
res.end(pubkey);
});
function webNotice(type, req, res, next) {
lists.getByCid(req.params.cid, (err, list) => {
if (!err && !list) {
err = new Error(_('Selected list not found'));
err.status = 404;
async function webNotice(type, req, res) {
const list = await lists.getByCid(req.params.cid);
const configItems = await settings.get(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail']);
const data = {
title: list.name,
homepage: configItems.defaultHomepage || configItems.serviceUrl,
defaultAddress: configItems.defaultAddress,
defaultPostaddress: configItems.defaultPostaddress,
contactAddress: configItems.defaultAddress,
template: {
template: 'subscription/web-' + type + '-notice.mjml.hbs',
layout: 'subscription/layout.mjml.hbs'
}
};
if (err) {
return next(err);
}
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-' + type + '-notice', data);
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail'], (err, configItems) => {
if (err) {
return next(err);
}
const htmlRenderer = await getMjmlTemplate(data.template);
let data = {
title: list.name,
homepage: configItems.defaultHomepage || configItems.serviceUrl,
defaultAddress: configItems.defaultAddress,
defaultPostaddress: configItems.defaultPostaddress,
contactAddress: configItems.defaultAddress,
template: {
template: 'subscription/web-' + type + '-notice.mjml.hbs',
layout: 'subscription/layout.mjml.hbs'
}
};
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);
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-' + type + '-notice', data, (err, data) => {
if (err) {
return next(err);
}
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
if (err) {
return next(err);
}
helpers.captureFlashMessages(req, res, (err, flash) => {
if (err) {
return next(err);
}
data.isWeb = true;
data.isConfirmNotice = true;
data.flashMessages = flash;
res.send(htmlRenderer(data));
});
});
});
});
});
res.send(htmlRenderer(data));
}
module.exports = router;