From 6c5c47ac2ee5f9d6b74bed036cd0dbe5b69c0b6a Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sat, 30 Dec 2017 12:23:16 +0100 Subject: [PATCH] Refactored subscriptions. Not even executed. --- lib/subscription-mail-helpers.js | 14 +- models/subscriptions.js | 134 ++++++-- routes/subscription.js | 565 ++++++++++++------------------- 3 files changed, 326 insertions(+), 387 deletions(-) diff --git a/lib/subscription-mail-helpers.js b/lib/subscription-mail-helpers.js index 98b7882a..f6778eab 100644 --- a/lib/subscription-mail-helpers.js +++ b/lib/subscription-mail-helpers.js @@ -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); diff --git a/models/subscriptions.js b/models/subscriptions.js index 355d6a5d..e19e855c 100644 --- a/models/subscriptions.js +++ b/models/subscriptions.js @@ -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 }; \ No newline at end of file diff --git a/routes/subscription.js b/routes/subscription.js index bbb6b914..6a68a5d8 100644 --- a/routes/subscription.js +++ b/routes/subscription.js @@ -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;