Client's public folder renamed to static
Regular campaign sender seems to have most of the code in place. (Not tested.)
This commit is contained in:
parent
89eabea0de
commit
63765f7222
354 changed files with 836 additions and 324 deletions
|
|
@ -255,6 +255,22 @@ async function getById(context, id, withPermissions = true, content = Content.AL
|
|||
});
|
||||
}
|
||||
|
||||
async function getByCidTx(tx, context, cid) {
|
||||
const entity = await tx('campaigns').where('cid', cid).first();
|
||||
if (!entity) {
|
||||
shares.throwPermissionDenied();
|
||||
}
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.id, 'view');
|
||||
return entity;
|
||||
}
|
||||
|
||||
async function getByCid(context, cid) {
|
||||
return await knex.transaction(async tx => {
|
||||
return getByCidTx(tx, context, cid);
|
||||
});
|
||||
}
|
||||
|
||||
async function _validateAndPreprocess(tx, context, entity, isCreate, content) {
|
||||
if (content === Content.ALL || content === Content.WITHOUT_SOURCE_CUSTOM || content === Content.RSS_ENTRY) {
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
|
@ -698,6 +714,8 @@ module.exports.listOthersWhoseListsAreIncludedDTAjax = listOthersWhoseListsAreIn
|
|||
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
|
||||
module.exports.getByIdTx = getByIdTx;
|
||||
module.exports.getById = getById;
|
||||
module.exports.getByCidTx = getByCidTx;
|
||||
module.exports.getByCid = getByCid;
|
||||
module.exports.create = create;
|
||||
module.exports.createRssTx = createRssTx;
|
||||
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
|
||||
|
|
|
|||
|
|
@ -557,8 +557,7 @@ async function removeAllByListIdTx(tx, context, listId) {
|
|||
}
|
||||
}
|
||||
|
||||
// Returns an array that can be used for rendering by Handlebars
|
||||
async function forHbs(context, listId, subscription) { // assumes grouped subscription
|
||||
function forHbsWithFieldsGrouped(fieldsGrouped, subscription) { // assumes grouped subscription
|
||||
const customFields = [{
|
||||
name: 'Email Address',
|
||||
column: 'email',
|
||||
|
|
@ -569,9 +568,7 @@ async function forHbs(context, listId, subscription) { // assumes grouped subscr
|
|||
order_manage: -1
|
||||
}];
|
||||
|
||||
const flds = await listGrouped(context, listId);
|
||||
|
||||
for (const fld of flds) {
|
||||
for (const fld of fieldsGrouped) {
|
||||
const type = fieldTypes[fld.type];
|
||||
const fldCol = getFieldColumn(fld);
|
||||
|
||||
|
|
@ -630,6 +627,13 @@ async function forHbs(context, listId, subscription) { // assumes grouped subscr
|
|||
}
|
||||
|
||||
return customFields;
|
||||
|
||||
}
|
||||
|
||||
// Returns an array that can be used for rendering by Handlebars
|
||||
async function forHbs(context, listId, subscription) { // assumes grouped subscription
|
||||
const flds = await listGrouped(context, listId);
|
||||
return forHbsWithFieldsGrouped(flds, subscription);
|
||||
}
|
||||
|
||||
// Converts subscription data received via (1) POST request from subscription form, (2) via subscribe request to API v1 to subscription structure supported by subscriptions model,
|
||||
|
|
@ -741,6 +745,7 @@ module.exports.remove = remove;
|
|||
module.exports.removeAllByListIdTx = removeAllByListIdTx;
|
||||
module.exports.serverValidate = serverValidate;
|
||||
module.exports.forHbs = forHbs;
|
||||
module.exports.forHbsWithFieldsGrouped = forHbsWithFieldsGrouped;
|
||||
module.exports.fromPost = fromPost;
|
||||
module.exports.fromAPI = fromAPI;
|
||||
module.exports.fromImport = fromImport;
|
||||
|
|
|
|||
|
|
@ -50,11 +50,15 @@ async function listDTAjax(context, type, subType, entityId, params) {
|
|||
);
|
||||
}
|
||||
|
||||
async function list(context, type, subType, entityId) {
|
||||
async function listTx(tx, context, type, subType, entityId) {
|
||||
enforceTypePermitted(type, subType);
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'view'));
|
||||
return await tx(getFilesTable(type, subType)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
|
||||
}
|
||||
|
||||
async function list(context, type, subType, entityId) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'view'));
|
||||
return await tx(getFilesTable(type, subType)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
|
||||
return listTx(tx, context, type, subType, entityId);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -304,6 +308,7 @@ async function copyAllTx(tx, context, fromType, fromSubType, fromEntityId, toTyp
|
|||
|
||||
module.exports.filesDir = filesDir;
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.listTx = listTx;
|
||||
module.exports.list = list;
|
||||
module.exports.getFileById = getFileById;
|
||||
module.exports.getFileByFilename = getFileByFilename;
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ async function getDefaultCustomFormValues() {
|
|||
}
|
||||
|
||||
form.layout = await getContents('views/subscription/layout.mjml.hbs') || '';
|
||||
form.form_input_style = await getContents('public/subscription/form-input-style.css') || '@import url(/subscription/form-input-style.css);';
|
||||
form.form_input_style = await getContents('static/subscription/form-input-style.css') || '@import url(/subscription/form-input-style.css);';
|
||||
|
||||
return form;
|
||||
}
|
||||
|
|
|
|||
175
models/links.js
Normal file
175
models/links.js
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
'use strict';
|
||||
|
||||
const knex = require('../lib/knex');
|
||||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const shares = require('./shares');
|
||||
const tools = require('../lib/tools');
|
||||
const campaigns = require('./campaigns');
|
||||
const lists = require('./lists');
|
||||
const subscriptions = require('./subscriptions');
|
||||
const contextHelpers = require('../lib/context-helpers');
|
||||
const geoip = require('geoip-ultralight');
|
||||
const uaParser = require('device');
|
||||
const he = require('he');
|
||||
const { enforce } = require('../lib/helpers');
|
||||
const { getTrustedUrl } = require('../lib/urls');
|
||||
|
||||
const LinkId = {
|
||||
OPEN: -1,
|
||||
GENERAL_CLICK: 0
|
||||
};
|
||||
|
||||
async function resolve(linkCid) {
|
||||
return await knex('links').where('cid', linkCid).select(['id', 'url']).first();
|
||||
}
|
||||
|
||||
async function countLink(remoteIp, userAgent, campaignCid, listCid, subscriptionCid, linkId) {
|
||||
await knex.transaction(async tx => {
|
||||
const list = await lists.getByCidTx(tx, contextHelpers.getAdminContext(), listCid);
|
||||
const campaign = await campaigns.getByCidTx(tx, contextHelpers.getAdminContext(), campaignCid);
|
||||
const subscription = await subscriptions.getByCidTx(tx, contextHelpers.getAdminContext(), subscriptionCid);
|
||||
|
||||
const country = geoip.lookupCountry(remoteIp) || null;
|
||||
const device = uaParser(userAgent, { unknownUserAgentDeviceType: 'desktop', emptyUserAgentDeviceType: 'desktop' });
|
||||
const now = new Date();
|
||||
|
||||
const _countLink = async (clickLinkId, incrementOnDup) => {
|
||||
try {
|
||||
const campaignLinksQry = knex('campaign_links')
|
||||
.insert({
|
||||
campaign: campaign.id,
|
||||
list: list.id,
|
||||
subscription: subscription.id,
|
||||
link: linkId,
|
||||
ip: remoteIp,
|
||||
device_type: device.type,
|
||||
country
|
||||
}).toSQL();
|
||||
|
||||
const campaignLinksQryResult = await tx.raw(campaignLinksQry.sql + (incrementOnDup ? ' ON DUPLICATE KEY UPDATE `count`=`count`+1' : ''), campaignLinksQry.bindings);
|
||||
|
||||
if (campaignLinksQryResult.affectedRows > 1) { // When using DUPLICATE KEY UPDATE, this means that the entry was already there
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Update opened and click timestamps
|
||||
const latestUpdates = {};
|
||||
|
||||
if (!campaign.click_tracking_disabled && linkId > LinkId.GENERAL_CLICK) {
|
||||
latestUpdates.latest_click = now;
|
||||
}
|
||||
|
||||
if (!campaign.open_tracking_disabled) {
|
||||
latestUpdates.latest_open = now;
|
||||
}
|
||||
|
||||
if (latestUpdates.latest_click || latestUpdates.latest_open) {
|
||||
await tx(subscriptions.getSubscriptionTableName(list.id)).update(latestUpdates).where('id', subscription.id);
|
||||
}
|
||||
|
||||
|
||||
// Update clicks
|
||||
if (linkId > LinkId.GENERAL_CLICK && !campaign.click_tracking_disabled) {
|
||||
if (await _countLink(linkId, true)) {
|
||||
if (await _countLink(LinkId.GENERAL_CLICK, false)) {
|
||||
await tx('campaigns').increment('clicks').where('id', campaign.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update opens. We count a click as an open too.
|
||||
if (!campaign.open_tracking_disabled) {
|
||||
if (await _countLink(LinkId.OPEN, true)) {
|
||||
await tx('campaigns').increment('opened').where('id', campaign.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function addOrGet(campaignId, url) {
|
||||
return await knex.transaction(async tx => {
|
||||
const link = tx('links').select(['id', 'cid']).where({
|
||||
campaign: campaignId,
|
||||
url
|
||||
}).first();
|
||||
|
||||
if (!link) {
|
||||
let cid = shortid.generate();
|
||||
|
||||
const ids = tx('links').insert({
|
||||
campaign: campaignId,
|
||||
cid,
|
||||
url
|
||||
});
|
||||
|
||||
return {
|
||||
id: ids[0],
|
||||
cid
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function updateLinks(campaign, list, subscription, message) {
|
||||
if ((campaign.open_tracking_disabled && campaign.click_tracking_disabled) || !message || !message.trim()) {
|
||||
// tracking is disabled, do not modify the message
|
||||
return message;
|
||||
}
|
||||
|
||||
// insert tracking image
|
||||
if (!campaign.open_tracking_disabled) {
|
||||
let inserted = false;
|
||||
const imgUrl = getTrustedUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}`);
|
||||
const img = '<img src="' + imgUrl + '" width="1" height="1" alt="mt">';
|
||||
message = message.replace(/<\/body\b/i, match => {
|
||||
inserted = true;
|
||||
return img + match;
|
||||
});
|
||||
if (!inserted) {
|
||||
message = message + img;
|
||||
}
|
||||
}
|
||||
|
||||
if (!campaign.click_tracking_disabled) {
|
||||
const re = /(<a[^>]* href\s*=[\s"']*)(http[^"'>\s]+)/gi;
|
||||
|
||||
const urlsToBeReplaced = new Set();
|
||||
|
||||
message.replace(re, (match, prefix, encodedUrl) => {
|
||||
const url = he.decode(encodedUrl, {isAttributeValue: true});
|
||||
urlsToBeReplaced.add(url);
|
||||
});
|
||||
|
||||
const urls = new Map(); // url -> {id, cid} (as returned by add)
|
||||
for (const url of urlsToBeReplaced) {
|
||||
const link = await addOrGet(campaign.id, url);
|
||||
urls.set(url, link);
|
||||
}
|
||||
|
||||
message.replace(re, (match, prefix, encodedUrl) => {
|
||||
const url = he.decode(encodedUrl, {isAttributeValue: true});
|
||||
const link = urls.get(url);
|
||||
return getTrustedUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}/${link.cid}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports.resolve = resolve;
|
||||
module.exports.countLink = countLink;
|
||||
module.exports.addOrGet = addOrGet;
|
||||
module.exports.updateLinks = updateLinks;
|
||||
|
|
@ -14,7 +14,7 @@ const entitySettings = require('../lib/entity-settings');
|
|||
|
||||
const UnsubscriptionMode = require('../shared/lists').UnsubscriptionMode;
|
||||
|
||||
const allowedKeys = new Set(['name', 'description', 'default_form', 'public_subscribe', 'unsubscription_mode', 'contact_email', 'homepage', 'namespace']);
|
||||
const allowedKeys = new Set(['name', 'description', 'default_form', 'public_subscribe', 'unsubscription_mode', 'contact_email', 'homepage', 'namespace', 'to_name']);
|
||||
|
||||
function hash(entity) {
|
||||
return hasher.hash(filterObject(entity, allowedKeys));
|
||||
|
|
@ -61,7 +61,7 @@ async function listWithSegmentByCampaignDTAjax(context, campaignId, params) {
|
|||
);
|
||||
}
|
||||
|
||||
async function _getByIdTx(tx, context, id) {
|
||||
async function getByIdTx(tx, context, id) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', id, 'view');
|
||||
const entity = await tx('lists').where('id', id).first();
|
||||
return entity;
|
||||
|
|
@ -70,28 +70,32 @@ async function _getByIdTx(tx, context, id) {
|
|||
async function getById(context, id) {
|
||||
return await knex.transaction(async tx => {
|
||||
// note that permissions are not obtained here as this methods is used only with synthetic admin context
|
||||
return await _getByIdTx(tx, context, id);
|
||||
return await getByIdTx(tx, context, id);
|
||||
});
|
||||
}
|
||||
|
||||
async function getByIdWithListFields(context, id) {
|
||||
return await knex.transaction(async tx => {
|
||||
const entity = await _getByIdTx(tx, context, id);
|
||||
const entity = await getByIdTx(tx, context, id);
|
||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'list', id);
|
||||
entity.listFields = await fields.listByOrderListTx(tx, id);
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
async function getByCidTx(tx, context, cid) {
|
||||
const entity = await tx('lists').where('cid', cid).first();
|
||||
if (!entity) {
|
||||
shares.throwPermissionDenied();
|
||||
}
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'view');
|
||||
return entity;
|
||||
}
|
||||
|
||||
async function getByCid(context, cid) {
|
||||
return await knex.transaction(async tx => {
|
||||
const entity = await tx('lists').where('cid', cid).first();
|
||||
if (!entity) {
|
||||
shares.throwPermissionDenied();
|
||||
}
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'view');
|
||||
return entity;
|
||||
return getByCidTx(tx, context, cid);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -212,8 +216,10 @@ module.exports.UnsubscriptionMode = UnsubscriptionMode;
|
|||
module.exports.hash = hash;
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.listWithSegmentByCampaignDTAjax = listWithSegmentByCampaignDTAjax;
|
||||
module.exports.getByIdTx = getByIdTx;
|
||||
module.exports.getById = getById;
|
||||
module.exports.getByIdWithListFields = getByIdWithListFields;
|
||||
module.exports.getByCidTx = getByCidTx;
|
||||
module.exports.getByCid = getByCid;
|
||||
module.exports.create = create;
|
||||
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
|
||||
|
|
|
|||
|
|
@ -194,28 +194,30 @@ async function hashByList(listId, entity) {
|
|||
});
|
||||
}
|
||||
|
||||
async function _getByTx(tx, context, listId, key, value, grouped) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
||||
|
||||
const entity = await tx(getSubscriptionTableName(listId)).where(key, value).first();
|
||||
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
||||
}
|
||||
|
||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||
|
||||
if (grouped) {
|
||||
groupSubscription(groupedFieldsMap, entity);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
||||
}
|
||||
|
||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||
|
||||
if (grouped) {
|
||||
groupSubscription(groupedFieldsMap, entity);
|
||||
}
|
||||
|
||||
return entity;
|
||||
return _getByTx(tx, context, listId, key, value, grouped);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function getById(context, listId, id, grouped = true) {
|
||||
return await _getBy(context, listId, 'id', id, grouped);
|
||||
}
|
||||
|
|
@ -228,6 +230,10 @@ async function getByCid(context, listId, cid, grouped = true) {
|
|||
return await _getBy(context, listId, 'cid', cid, grouped);
|
||||
}
|
||||
|
||||
async function getByCidTx(tx, context, listId, cid, grouped = true) {
|
||||
return await _getByTx(tx, 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');
|
||||
|
|
@ -725,6 +731,7 @@ async function getListsWithEmail(context, email) {
|
|||
module.exports.getSubscriptionTableName = getSubscriptionTableName;
|
||||
module.exports.hashByList = hashByList;
|
||||
module.exports.getById = getById;
|
||||
module.exports.getByCidTx = getByCidTx;
|
||||
module.exports.getByCid = getByCid;
|
||||
module.exports.getByEmail = getByEmail;
|
||||
module.exports.list = list;
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ async function sendPasswordReset(usernameOrEmail) {
|
|||
const { adminEmail } = await settings.get(contextHelpers.getAdminContext(), ['adminEmail']);
|
||||
|
||||
const mailer = await mailers.getOrCreateMailer();
|
||||
await mailer.sendMail({
|
||||
await mailer.sendTransactionalMail({
|
||||
from: {
|
||||
address: adminEmail
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue