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 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) { async function sendAlreadySubscribed(list, email, subscription) {
@ -40,7 +40,7 @@ async function sendAlreadySubscribed(list, email, subscription) {
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid, preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + 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) { async function sendConfirmAddressChange(list, email, cid, subscription) {
@ -50,7 +50,7 @@ async function sendConfirmAddressChange(list, email, cid, subscription) {
const relativeUrls = { const relativeUrls = {
confirmUrl: '/subscription/confirm/change-address/' + cid 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) { async function sendConfirmSubscription(list, email, cid, subscription) {
@ -60,7 +60,7 @@ async function sendConfirmSubscription(list, email, cid, subscription) {
const relativeUrls = { const relativeUrls = {
confirmUrl: '/subscription/confirm/subscribe/' + cid 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) { async function sendConfirmUnsubscription(list, email, cid, subscription) {
@ -70,14 +70,14 @@ async function sendConfirmUnsubscription(list, email, cid, subscription) {
const relativeUrls = { const relativeUrls = {
confirmUrl: '/subscription/confirm/unsubscribe/' + cid 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) { async function sendUnsubscriptionConfirmed(list, email, subscription) {
const relativeUrls = { const relativeUrls = {
subscribeUrl: '/subscription/' + list.cid + '?cid=' + subscription.cid 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) { 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); console.log(subscription);
const flds = await fields.list(contextHelpers.getAdminContext(), list.id); 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}`; return `subscription__${listId}`;
} }
function getCampaignTableName(campaignId) {
return `campaign__${campaignId}`;
}
async function getGroupedFieldsMap(tx, listId) { async function getGroupedFieldsMap(tx, listId) {
const groupedFields = await fields.listGroupedTx(tx, listId); const groupedFields = await fields.listGroupedTx(tx, listId);
const result = {}; 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 => { return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions'); 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); const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
if (grouped) {
groupSubscription(groupedFieldsMap, entity); groupSubscription(groupedFieldsMap, entity);
}
return entity; return entity;
}); });
} }
async function getById(context, listId, id) { async function getById(context, listId, id, grouped = true) {
return await _getBy(context, listId, 'id', id); return await _getBy(context, listId, 'id', id, grouped);
} }
async function getByEmail(context, listId, email) { async function getByEmail(context, listId, email, grouped = true) {
return await _getBy(context, listId, 'email', email); return await _getBy(context, listId, 'email', email, grouped);
} }
async function getByCid(context, listId, cid) { async function getByCid(context, listId, cid, grouped = true) {
return await _getBy(context, listId, 'cid', cid); return await _getBy(context, listId, 'cid', cid, grouped);
} }
async function listDTAjax(context, listId, segmentId, params) { async function listDTAjax(context, listId, segmentId, params) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions'); 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 // 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 // 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 => { return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions'); 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); const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
@ -344,7 +371,7 @@ async function serverValidate(context, listId, data) {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions'); await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
if (data.email) { if (data.email) {
const existingKeyQuery = tx(getTableName(listId)).where('email', data.email); const existingKeyQuery = tx(getSubscriptionTableName(listId)).where('email', data.email);
if (data.id) { if (data.id) {
existingKeyQuery.whereNot('id', 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) { async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, isCreate) {
enforce(entity.email, 'Email must be set'); 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) { if (!isCreate) {
existingWithKeyQuery.whereNot('id', entity.id); existingWithKeyQuery.whereNot('id', entity.id);
@ -401,7 +428,7 @@ async function create(context, listId, entity, meta = {}) {
filteredEntity.opt_in_country = meta.country; filteredEntity.opt_in_country = meta.country;
filteredEntity.imported = meta.imported || false; 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]; const id = ids[0];
if (entity.status === SubscriptionStatus.SUBSCRIBED) { if (entity.status === SubscriptionStatus.SUBSCRIBED) {
@ -416,7 +443,7 @@ async function updateWithConsistencyCheck(context, listId, entity) {
await knex.transaction(async tx => { await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions'); 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) { if (!existing) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
@ -441,7 +468,7 @@ async function updateWithConsistencyCheck(context, listId, entity) {
filteredEntity.status_change = new Date(); 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; let countIncrement = 0;
@ -460,12 +487,12 @@ async function updateWithConsistencyCheck(context, listId, entity) {
async function removeTx(tx, context, listId, id) { async function removeTx(tx, context, listId, id) {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions'); 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) { if (!existing) {
throw new interoperableErrors.NotFoundError(); 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) { if (existing.status === SubscriptionStatus.SUBSCRIBED) {
await tx('lists').where('id', listId).decrement('subscribers', 1); 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 => { return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions'); 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('cid', subscriptionCid).first();
if (!existing) { if (!(existing && existing.status === SubscriptionStatus.SUBSCRIBED)) {
throw new interoperableErrors.NotFoundError(); 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({ await tx(getSubscriptionTableName(listId)).where('cid', subscriptionCid).update({
status: SubscriptionStatus.UNSUBSCRIBED 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; return existing;
@ -505,12 +546,12 @@ async function updateAddressAndGet(context, listId, subscriptionId, emailNew) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions'); 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) { if (!existing) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
await tx(getTableName(listId)).where('id', subscriptionId).update({ await tx(getSubscriptionTableName(listId)).where('id', subscriptionId).update({
email: emailNew 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 = { module.exports = {
hashByList, hashByList,
getById, getById,
getByCid, getByCid,
getByEmail, getByEmail,
getStatusByCid,
list, list,
listDTAjax, listDTAjax,
serverValidate, serverValidate,
create, create,
updateWithConsistencyCheck, updateWithConsistencyCheck,
remove, remove,
unsubscribeAndGet, unsubscribeByCidAndGet,
updateAddressAndGet updateAddressAndGet,
updateManagedUngrouped
}; };

View file

@ -12,6 +12,8 @@ const _ = require('../lib/translate')._;
const contextHelpers = require('../lib/context-helpers'); const contextHelpers = require('../lib/context-helpers');
const forms = require('../models/forms'); const forms = require('../models/forms');
const { SubscriptionStatus } = require('../shared/lists');
const openpgp = require('openpgp'); const openpgp = require('openpgp');
const util = require('util'); const util = require('util');
const cors = require('cors'); 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); const list = await lists.getById(contextHelpers.getAdminContext(), confirmation.list);
await mailHelpers.sendSubscriptionConfirmed(list, subscription.email, subscription); 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) => { 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 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 data = confirmation.data;
const subscription = await subscriptions.updateAddressAndGet(contextHelpers.getAdminContext(), list.id, data.subscriptionId, data.emailNew); 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); await mailHelpers.sendSubscriptionConfirmed(list, data.emailNew, subscription);
req.flash('info', _('Email address changed')); 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) => { 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 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 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); 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; let subscription;
if (ucid) { 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); 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' 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); const htmlRenderer = await getMjmlTemplate(data.template);
@ -251,7 +255,7 @@ router.getAsync('/:cid/widget', cors(corsOptions), async (req, res) => {
layout: null, 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 renderAsync = bluebird.promisify(res.render);
const html = await renderAsync('subscription/widget-subscribe', data); const html = await renderAsync('subscription/widget-subscribe', data);
@ -278,7 +282,6 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
req.needsAPIJSONResponse = true; req.needsAPIJSONResponse = true;
} }
if (!email) { if (!email) {
if (req.xhr) { if (req.xhr) {
throw new Error('Email address not set'); 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.'); throw new interoperableErrors.SubscriptionNotAllowedError('The list does not allow public subscriptions.');
} }
let subscriptionData = {}; let subscriptionData = {};
Object.keys(req.body).forEach(key => { Object.keys(req.body).forEach(key => {
if (key !== 'email' && key.charAt(0) !== '_') { 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); 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 { } else {
const data = { const data = {
@ -346,16 +348,16 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
msg: _('Please Confirm Subscription') 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) => { router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid); 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) { if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new Error(_('Subscription not found in this list')); throw new interoperableErrors.NotFoundError('Subscription not found in this list');
} }
subscription.lcid = req.params.lcid; subscription.lcid = req.params.lcid;
@ -377,7 +379,7 @@ router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res)
layout: 'subscription/layout.mjml.hbs' 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); const htmlRenderer = await getMjmlTemplate(data.template);
@ -391,49 +393,26 @@ router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res)
router.postAsync('/:lcid/manage', passport.parseForm, 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 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) { if (status !== SubscriptionStatus.SUBSCRIBED) {
throw new Error(_('Subscription not found in this list')); 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 res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/updated-notice');
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');
});
}); });
router.get('/:lcid/manage-address/:ucid', passport.csrfProtection, (req, res, next) => { router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (req, res) => {
lists.getByCid(req.params.lcid, (err, list) => { const list = await lists.getByCid(req.params.lcid);
if (!err && !list) { const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
err = new Error(_('Selected list not found'));
err.status = 404; if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
} }
if (err) { const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
return next(err);
}
settings.list(['defaultAddress', 'defaultPostaddress'], (err, configItems) => {
if (err) {
return next(err);
}
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.lcid = req.params.lcid;
subscription.title = list.name; subscription.title = list.name;
@ -446,125 +425,76 @@ router.get('/:lcid/manage-address/:ucid', passport.csrfProtection, (req, res, ne
layout: 'subscription/layout.mjml.hbs' layout: 'subscription/layout.mjml.hbs'
}; };
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-manage-address', subscription, (err, data) => { await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-manage-address', subscription);
if (err) {
return next(err);
}
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => { const htmlRenderer = await getMjmlTemplate(data.template);
if (err) {
return next(err);
}
helpers.captureFlashMessages(req, res, (err, flash) => {
if (err) {
return next(err);
}
data.isWeb = true; data.isWeb = true;
data.needsJsWarning = true; data.needsJsWarning = true;
data.flashMessages = flash; data.isManagePreferences = true;
data.flashMessages = await captureFlashMessages(res);
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) => { router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProtection, async (req, res) => {
if (!err && !list) { const list = await lists.getByCid(req.params.lcid);
err = new Error(_('Selected list not found'));
err.status = 404; const emailNew = (req.body['email-new'] || '').toString().trim();
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 (err) { if (subscription.email === emailNew) {
return next(err);
}
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();
if (emailOld === emailNew) {
req.flash('info', _('Nothing seems to be changed')); req.flash('info', _('Nothing seems to be changed'));
res.redirect('/subscription/' + req.params.lcid + '/manage/' + req.body.cid);
} else { } else {
subscriptions.updateAddressCheck(list, req.body.cid, emailNew, req.ip, (err, subscription, newEmailAvailable) => { const emailErr = await tools.validateEmail(emailNew);
if (err) { if (emailErr) {
return next(err); req.flash('danger', emailErr.message);
}
function sendWebResponse(err) { } else {
if (err) { const newSubscription = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, emailNew, false);
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);
} }
req.flash('info', _('An email with further instructions has been sent to the provided address')); 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) { res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/manage/' + encodeURIComponent(req.body.cid));
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) { router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req, res) => {
return next(err); const list = await lists.getByCid(req.params.lcid);
}
settings.list(['defaultAddress', 'defaultPostaddress'], (err, configItems) => {
if (err) {
return next(err);
}
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;
}
if (err) {
return next(err);
}
const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
const autoUnsubscribe = req.query.auto === 'yes'; const autoUnsubscribe = req.query.auto === 'yes';
if (autoUnsubscribe) { if (autoUnsubscribe) {
handleUnsubscribe(list, subscription, autoUnsubscribe, req.query.c, req.ip, res, next); handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, res, next);
} else if (req.query.formTest || } else if (req.query.formTest ||
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM || list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_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');
}
subscription.lcid = req.params.lcid; subscription.lcid = req.params.lcid;
subscription.ucid = req.params.ucid; subscription.ucid = req.params.ucid;
subscription.title = list.name; subscription.title = list.name;
@ -578,142 +508,106 @@ router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next)
layout: 'subscription/layout.mjml.hbs' layout: 'subscription/layout.mjml.hbs'
}; };
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-unsubscribe', subscription, (err, data) => { await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-unsubscribe', subscription);
if (err) {
return next(err);
}
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => { const htmlRenderer = await getMjmlTemplate(data.template);
if (err) {
return next(err);
}
helpers.captureFlashMessages(req, res, (err, flash) => {
if (err) {
return next(err);
}
data.isWeb = true; data.isWeb = true;
data.flashMessages = flash; data.needsJsWarning = true;
data.isManagePreferences = true;
data.flashMessages = await captureFlashMessages(res);
res.send(htmlRenderer(data)); res.send(htmlRenderer(data));
});
});
});
} else { // UnsubscriptionMode.ONE_STEP || UnsubscriptionMode.TWO_STEP || UnsubscriptionMode.MANUAL } else { // UnsubscriptionMode.ONE_STEP || UnsubscriptionMode.TWO_STEP || UnsubscriptionMode.MANUAL
handleUnsubscribe(list, subscription, autoUnsubscribe, req.query.c, req.ip, res, next); await handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, res);
} }
});
});
});
}); });
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;
}
if (err) { router.postAsync('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, async (req, res) => {
return next(err); const list = await lists.getByCid(req.params.lcid);
}
const campaignId = (req.body.campaign || '').toString().trim() || false; const campaignCid = (req.body.campaign || '').toString().trim() || false;
subscriptions.get(list.id, req.body.ucid, (err, subscription) => { await handleUnsubscribe(list, req.body.ucid, false, campaignCid, req.ip, res);
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
err = new Error(_('Subscription not found in this list'));
err.status = 404;
}
if (err) {
return next(err);
}
handleUnsubscribe(list, subscription, false, campaignId, req.ip, res, next);
});
});
}); });
function handleUnsubscribe(list, subscription, autoUnsubscribe, campaignId, ip, res, next) {
async function handleUnsubscribe(list, subscriptionCid, autoUnsubscribe, campaignCid, ip, res) {
if ((list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) || 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)) ) { (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) => { try {
if (err) { const subscription = await subscriptions.unsubscribeByCidAndGet(contextHelpers.getAdminContext(), list.id, subscriptionCid, campaignCid);
return next(err);
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 (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
if (err) { throw new interoperableErrors.NotFoundError('Subscription not found in this list');
return next(err);
} }
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 = { const data = {
subscriptionId: subscription.id, subscriptionCid,
campaignId campaignCid
}; };
confirmations.addConfirmation(list.id, 'unsubscribe', ip, data, (err, confirmCid) => { const confirmCid = await confirmations.addConfirmation(list.id, 'unsubscribe', ip, data);
if (err) { await mailHelpers.sendConfirmUnsubscription(list, subscription.email, confirmCid, subscription);
return next(err);
}
mailHelpers.sendConfirmUnsubscription(list, subscription.email, confirmCid, subscription, err => { res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/confirm-unsubscription-notice');
if (err) {
return next(err);
}
res.redirect('/subscription/' + list.cid + '/confirm-unsubscription-notice');
});
});
} else { // UnsubscriptionMode.MANUAL } else { // UnsubscriptionMode.MANUAL
res.redirect('/subscription/' + list.cid + '/manual-unsubscribe-notice'); 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) => { router.getAsync('/:cid/confirm-unsubscription-notice', async (req, res) => {
webNotice('confirm-unsubscription', req, res, next); await webNotice('confirm-unsubscription', req, res);
}); });
router.get('/:cid/subscribed-notice', (req, res, next) => { router.getAsync('/:cid/subscribed-notice', async (req, res) => {
webNotice('subscribed', req, res, next); await webNotice('subscribed', req, res);
}); });
router.get('/:cid/updated-notice', (req, res, next) => { router.getAsync('/:cid/updated-notice', async (req, res) => {
webNotice('updated', req, res, next); await webNotice('updated', req, res);
}); });
router.get('/:cid/unsubscribed-notice', (req, res, next) => { router.getAsync('/:cid/unsubscribed-notice', async (req, res) => {
webNotice('unsubscribed', req, res, next); await webNotice('unsubscribed', req, res);
}); });
router.get('/:cid/manual-unsubscribe-notice', (req, res, next) => { router.getAsync('/:cid/manual-unsubscribe-notice', async (req, res) => {
webNotice('manual-unsubscribe', req, res, next); await webNotice('manual-unsubscribe', req, res);
}); });
router.post('/publickey', passport.parseForm, (req, res, next) => { router.postAsync('/publickey', passport.parseForm, async (req, res) => {
settings.list(['pgpPassphrase', 'pgpPrivateKey'], (err, configItems) => { const configItems = await settings.get(['pgpPassphrase', 'pgpPrivateKey']);
if (err) {
return next(err);
}
if (!configItems.pgpPrivateKey) { if (!configItems.pgpPrivateKey) {
err = new Error(_('Public key is not set')); const err = new Error(_('Public key is not set'));
err.status = 404; err.status = 404;
return next(err); throw err;
} }
let privKey; let privKey;
@ -727,12 +621,12 @@ router.post('/publickey', passport.parseForm, (req, res, next) => {
} }
if (!privKey) { if (!privKey) {
err = new Error(_('Public key is not set')); const err = new Error(_('Public key is not set'));
err.status = 404; err.status = 404;
return next(err); throw err;
} }
let pubkey = privKey.toPublic().armor(); const pubkey = privKey.toPublic().armor();
res.writeHead(200, { res.writeHead(200, {
'Content-Type': 'application/octet-stream', 'Content-Type': 'application/octet-stream',
@ -740,27 +634,16 @@ router.post('/publickey', passport.parseForm, (req, res, next) => {
}); });
res.end(pubkey); res.end(pubkey);
});
}); });
function webNotice(type, req, res, next) { async function webNotice(type, req, res) {
lists.getByCid(req.params.cid, (err, list) => { const list = await lists.getByCid(req.params.cid);
if (!err && !list) {
err = new Error(_('Selected list not found'));
err.status = 404;
}
if (err) { const configItems = await settings.get(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail']);
return next(err);
}
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail'], (err, configItems) => {
if (err) {
return next(err);
}
let data = { const data = {
title: list.name, title: list.name,
homepage: configItems.defaultHomepage || configItems.serviceUrl, homepage: configItems.defaultHomepage || configItems.serviceUrl,
defaultAddress: configItems.defaultAddress, defaultAddress: configItems.defaultAddress,
@ -772,30 +655,16 @@ function webNotice(type, req, res, next) {
} }
}; };
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-' + type + '-notice', data, (err, data) => { await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-' + type + '-notice', data);
if (err) {
return next(err);
}
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => { const htmlRenderer = await getMjmlTemplate(data.template);
if (err) {
return next(err);
}
helpers.captureFlashMessages(req, res, (err, flash) => {
if (err) {
return next(err);
}
data.isWeb = true; data.isWeb = true;
data.isConfirmNotice = true; data.isConfirmNotice = true; // FIXME: Not sure what this does. Check it in a browser with disabled JS
data.flashMessages = flash; data.isManagePreferences = true;
data.flashMessages = await captureFlashMessages(res);
res.send(htmlRenderer(data)); res.send(htmlRenderer(data));
});
});
});
});
});
} }
module.exports = router; module.exports = router;