Bugfixing.

This commit is contained in:
Tomas Bures 2018-09-27 12:34:54 +02:00
parent 86efa11994
commit 5670d21e76
31 changed files with 241 additions and 216 deletions

View file

@ -316,7 +316,6 @@ function createApp(appType) {
return next(); return next();
} }
console.log(err);
if (req.needsRESTJSONResponse) { if (req.needsRESTJSONResponse) {
const resp = { const resp = {
message: err.message, message: err.message,

View file

@ -628,10 +628,11 @@ export default class CUD extends Component {
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) { } else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
const campaignsColumns = [ const campaignsColumns = [
{ data: 1, title: t('Name') }, { data: 1, title: t('Name') },
{ data: 2, title: t('Description') }, { data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Type'), render: data => this.campaignTypeLabels[data] }, { data: 3, title: t('Description') },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() }, { data: 4, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{ data: 5, title: t('Namespace') } { data: 5, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('Namespace') }
]; ];
templateEdit = <TableSelect key="campaignSelect" id="data_sourceCampaign" label={t('Campaign')} withHeader dropdown dataUrl='rest/campaigns-with-content-table' columns={campaignsColumns} selectionLabelIndex={1} help={t('Content of the selected campaign will be copied into this campaign.')}/>; templateEdit = <TableSelect key="campaignSelect" id="data_sourceCampaign" label={t('Campaign')} withHeader dropdown dataUrl='rest/campaigns-with-content-table' columns={campaignsColumns} selectionLabelIndex={1} help={t('Content of the selected campaign will be copied into this campaign.')}/>;
@ -688,6 +689,13 @@ export default class CUD extends Component {
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}> <Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/> <InputField id="name" label={t('Name')}/>
{isEdit &&
<StaticField id="cid" className={styles.formDisabled} label={t('ID')} help={t('This is the campaign ID displayed to the subscribers')}>
{this.getFormValue('cid')}
</StaticField>
}
<TextArea id="description" label={t('Description')}/> <TextArea id="description" label={t('Description')}/>
{extraSettings} {extraSettings}

View file

@ -136,7 +136,7 @@ export default class CustomContent extends Component {
this.disableForm(); this.disableForm();
const response = await axios.post(getUrl('rest/html-to-text', { html })); const response = await axios.post(getUrl('rest/html-to-text'), { html });
this.updateFormValue('data_sourceCustom_text', response.data.text); this.updateFormValue('data_sourceCustom_text', response.data.text);
@ -175,7 +175,7 @@ export default class CustomContent extends Component {
{customTemplateTypeKey && getTypeForm(this, customTemplateTypeKey, true)} {customTemplateTypeKey && getTypeForm(this, customTemplateTypeKey, true)}
{customTemplateTypeKey && getEditForm(this, customTemplateTypeKey)} {customTemplateTypeKey && getEditForm(this, customTemplateTypeKey, 'data_sourceCustom_')}
<ButtonRow> <ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/> <Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>

View file

@ -68,14 +68,15 @@ export default class List extends Component {
const columns = [ const columns = [
{ data: 1, title: t('Name') }, { data: 1, title: t('Name') },
{ data: 2, title: t('Description') }, { data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Type'), render: data => this.campaignTypeLabels[data] }, { data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{ {
data: 4, data: 5,
title: t('Status'), title: t('Status'),
render: (data, display, rowData) => { render: (data, display, rowData) => {
if (data === CampaignStatus.SCHEDULED) { if (data === CampaignStatus.SCHEDULED) {
const scheduled = rowData[5]; const scheduled = rowData[6];
if (scheduled && new Date(scheduled) > new Date()) { if (scheduled && new Date(scheduled) > new Date()) {
return t('Sending scheduled'); return t('Sending scheduled');
} else { } else {
@ -86,14 +87,14 @@ export default class List extends Component {
} }
} }
}, },
{ data: 7, title: t('Created'), render: data => moment(data).fromNow() }, { data: 8, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 8, title: t('Namespace') }, { data: 9, title: t('Namespace') },
{ {
actions: data => { actions: data => {
const actions = []; const actions = [];
const perms = data[9]; const perms = data[10];
const campaignType = data[3]; const campaignType = data[4];
const campaignSource = data[6]; const campaignSource = data[7];
if (perms.includes('viewStats')) { if (perms.includes('viewStats')) {
actions.push({ actions.push({

View file

@ -83,7 +83,7 @@ class TestUser extends Component {
const testUsersColumns = [ const testUsersColumns = [
{ data: 1, title: t('Email') }, { data: 1, title: t('Email') },
{ data: 4, title: t('List ID') }, { data: 4, title: t('List ID'), render: data => <code>{data}</code> },
{ data: 5, title: t('List') }, { data: 5, title: t('List') },
{ data: 6, title: t('Segment') }, { data: 6, title: t('Segment') },
{ data: 7, title: t('List namespace') } { data: 7, title: t('List namespace') }

View file

@ -61,7 +61,7 @@ function getMenus(t) {
title: t('Attachments'), title: t('Attachments'),
link: params => `/campaigns/${params.campaignId}/attachments`, link: params => `/campaigns/${params.campaignId}/attachments`,
visible: resolved => resolved.campaign.permissions.includes('viewAttachments'), visible: resolved => resolved.campaign.permissions.includes('viewAttachments'),
panelRender: props => <Files title={t('Attachments')} help={t('These files will be attached to the campaign emails as proper attachments. This means they count towards to resulting eventual size of the email.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/> panelRender: props => <Files title={t('Attachments')} help={t('These files will be attached to the campaign emails as proper attachments. This means they count towards to the eventual size of the email.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/>
}, },
triggers: { triggers: {
title: t('Triggers'), title: t('Triggers'),

View file

@ -185,10 +185,11 @@ export default class CUD extends Component {
const campaignsColumns = [ const campaignsColumns = [
{ data: 1, title: t('Name') }, { data: 1, title: t('Name') },
{ data: 2, title: t('Description') }, { data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Type'), render: data => this.campaignTypeLabels[data] }, { data: 3, title: t('Description') },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() }, { data: 4, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{ data: 5, title: t('Namespace') } { data: 5, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('Namespace') }
]; ];
const campaignLists = this.props.campaign.lists.map(x => x.list).join(';'); const campaignLists = this.props.campaign.lists.map(x => x.list).join(';');

View file

@ -181,7 +181,7 @@ export default class CUD extends Component {
<InputField id="name" label={t('Name')}/> <InputField id="name" label={t('Name')}/>
{isEdit && {isEdit &&
<StaticField id="cid" className={styles.formDisabled} label={t('List ID')} help={t('This is the list ID displayed to the subscribers')}> <StaticField id="cid" className={styles.formDisabled} label={t('ID')} help={t('This is the list ID displayed to the subscribers')}>
{this.getFormValue('cid')} {this.getFormValue('cid')}
</StaticField> </StaticField>
} }

View file

@ -96,7 +96,7 @@ export default class List extends Component {
if (perms.includes('viewImports')) { if (perms.includes('viewImports')) {
actions.push({ actions.push({
label: <Icon icon="sort" title={t('Imports & Tasks')}/>, label: <Icon icon="sort" title={t('Imports')}/>,
link: `/lists/${data[0]}/imports` link: `/lists/${data[0]}/imports`
}); });
} }

View file

@ -404,7 +404,7 @@ export default class CUD extends Component {
deletedMsg={t('Field deleted')}/> deletedMsg={t('Field deleted')}/>
} }
<Title>{isEdit ? t('Edit Import/Task') : t('Create Import/Task')}</Title> <Title>{isEdit ? t('Edit Import') : t('Create Import')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}> <Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/> <InputField id="name" label={t('Name')}/>

View file

@ -81,11 +81,11 @@ export default class List extends Component {
<div> <div>
{mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports') && {mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports') &&
<Toolbar> <Toolbar>
<NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import/Task')}/> <NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import')}/>
</Toolbar> </Toolbar>
} }
<Title>{t('Imports & Tasks')}</Title> <Title>{t('Imports')}</Title>
<Table withHeader dataUrl={`rest/imports-table/${this.props.list.id}`} columns={columns} /> <Table withHeader dataUrl={`rest/imports-table/${this.props.list.id}`} columns={columns} />
</div> </div>

View file

@ -132,7 +132,7 @@ function getMenus(t) {
} }
}, },
imports: { imports: {
title: t('Imports & Tasks'), title: t('Imports'),
link: params => `/lists/${params.listId}/imports/`, link: params => `/lists/${params.listId}/imports/`,
visible: resolved => resolved.list.permissions.includes('viewImports'), visible: resolved => resolved.list.permissions.includes('viewImports'),
panelRender: props => <ImportsList list={props.resolved.list} />, panelRender: props => <ImportsList list={props.resolved.list} />,

View file

@ -20,6 +20,7 @@ import {
Form, Form,
FormSendMethod, FormSendMethod,
InputField, InputField,
StaticField,
TextArea, TextArea,
withForm withForm
} from '../lib/form'; } from '../lib/form';
@ -37,6 +38,8 @@ import {
MailerType MailerType
} from "../../../shared/send-configurations"; } from "../../../shared/send-configurations";
import styles from "../lib/styles.scss";
import mailtrainConfig from 'mailtrainConfig'; import mailtrainConfig from 'mailtrainConfig';
@ -189,6 +192,13 @@ export default class CUD extends Component {
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}> <Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/> <InputField id="name" label={t('Name')}/>
{isEdit &&
<StaticField id="cid" className={styles.formDisabled} label={t('ID')}>
{this.getFormValue('cid')}
</StaticField>
}
<TextArea id="description" label={t('Description')}/> <TextArea id="description" label={t('Description')}/>
<NamespaceSelect/> <NamespaceSelect/>
@ -221,6 +231,8 @@ export default class CUD extends Component {
} }
</Fieldset> </Fieldset>
<hr/>
<ButtonRow> <ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/> <Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{canDelete && {canDelete &&

View file

@ -89,6 +89,7 @@ export default class Update extends Component {
<TextArea id="pgpPrivateKey" label={t('GPG private key')} placeholder={t('Begins with \'-----BEGIN PGP PRIVATE KEY BLOCK-----\'')} help={t('This value is optional. If you do not provide a private key GPG encrypted messages are sent without signing.')}/> <TextArea id="pgpPrivateKey" label={t('GPG private key')} placeholder={t('Begins with \'-----BEGIN PGP PRIVATE KEY BLOCK-----\'')} help={t('This value is optional. If you do not provide a private key GPG encrypted messages are sent without signing.')}/>
</Fieldset> </Fieldset>
<hr/>
<ButtonRow> <ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/> <Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
</ButtonRow> </ButtonRow>

View file

@ -39,7 +39,7 @@ export default class Share extends Component {
userId userId
}; };
await axios.put(getUrl('rest/shares', data)); await axios.put(getUrl('rest/shares'), data);
this.sharesTable.refresh(); this.sharesTable.refresh();
this.usersTableSelect.refresh(); this.usersTableSelect.refresh();
} }

View file

@ -34,7 +34,7 @@ export default class UserShares extends Component {
userId: this.props.user.id userId: this.props.user.id
}; };
await axios.put(getUrl('rest/shares', data)); await axios.put(getUrl('rest/shares'), data);
for (const key in this.sharesTables) { for (const key in this.sharesTables) {
this.sharesTables[key].refresh(); this.sharesTables[key].refresh();
} }

View file

@ -165,7 +165,7 @@ export default class CUD extends Component {
this.disableForm(); this.disableForm();
const response = await axios.post(getUrl('rest/html-to-text', { html })); const response = await axios.post(getUrl('rest/html-to-text'), { html });
this.updateFormValue('text', response.data.text); this.updateFormValue('text', response.data.text);

View file

@ -1,99 +1,2 @@
/* Space out content a bit */
body { body {
padding-top: 20px;
padding-bottom: 20px;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header, .marketing, .footer {
padding-right: 15px;
padding-left: 15px;
}
/* Custom page header */
.header {
padding-bottom: 20px;
border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 40px;
}
/* Custom page footer */
.footer {
padding-top: 19px;
color: #777;
border-top: 1px solid #e5e5e5;
}
/* Customize container */
@media (min-width: 768px) {
.container {
max-width: 730px;
}
}
.container-narrow > hr {
margin: 30px 0;
}
/* Main marketing message and sign up button */
.jumbotron {
text-align: center;
border-bottom: 1px solid #e5e5e5;
}
.jumbotron .btn {
padding: 14px 24px;
font-size: 21px;
}
/* Supporting marketing content */
.marketing {
margin: 40px 0;
}
.marketing p + h4 {
margin-top: 28px;
}
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
/* Remove the padding we set earlier */
.header, .marketing, .footer {
padding-right: 0;
padding-left: 0;
}
/* Space out the masthead */
.header {
margin-bottom: 30px;
}
/* Remove the bottom border on the jumbotron for visual effect */
.jumbotron {
border-bottom: 0;
}
}
.gpg-text {
font-family: monospace;
} }

View file

@ -27,9 +27,9 @@ class CampaignSender {
} }
async init(settings) { async init(settings) {
this.listsById = Map(); // listId -> list this.listsById = new Map(); // listId -> list
this.listsByCid = Map(); // listCid -> list this.listsByCid = new Map(); // listCid -> list
this.listsFieldsGrouped = Map(); // listId -> fieldsGrouped this.listsFieldsGrouped = new Map(); // listId -> fieldsGrouped
this.attachments = []; this.attachments = [];
await knex.transaction(async tx => { await knex.transaction(async tx => {
@ -39,13 +39,15 @@ class CampaignSender {
this.campaign = await campaigns.rawGetByTx(tx, 'id', settings.campaignId); this.campaign = await campaigns.rawGetByTx(tx, 'id', settings.campaignId);
} }
this.sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), campaign.send_configuration); const campaign = this.campaign;
this.sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), campaign.send_configuration, false, true);
for (const listSpec of campaign.lists) { for (const listSpec of campaign.lists) {
const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), listSpec.list); const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), listSpec.list);
this.listsById.set(list.id) = list; this.listsById.set(list.id, list);
this.listsByCid.set(list.cid) = list; this.listsByCid.set(list.cid, list);
this.listsFieldsGrouped.set(list.id) = await fields.listGroupedTx(tx, list.id); this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id));
} }
if (campaign.source === CampaignSource.TEMPLATE) { if (campaign.source === CampaignSource.TEMPLATE) {
@ -63,7 +65,7 @@ class CampaignSender {
}); });
this.useVerp = config.verp.enabled && sendConfiguration.verp_hostname; this.useVerp = config.verp.enabled && sendConfiguration.verp_hostname;
this.useVerpSenderHeader = useVerp && config.verp.disablesenderheader !== true; this.useVerpSenderHeader = this.useVerp && config.verp.disablesenderheader !== true;
} }
async _getMessage(campaign, list, subscriptionGrouped, mergeTags, replaceDataImgs) { async _getMessage(campaign, list, subscriptionGrouped, mergeTags, replaceDataImgs) {
@ -118,9 +120,9 @@ class CampaignSender {
}); });
} }
const html = renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, html, false, true) : html; html = renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, html, false, true) : html;
const text = (text || '').trim() text = (text || '').trim()
? (renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text) : text) ? (renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text) : text)
: htmlToText.fromString(html, {wordwrap: 130}); : htmlToText.fromString(html, {wordwrap: 130});
@ -136,7 +138,7 @@ class CampaignSender {
const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid); const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid);
const flds = this.listsFieldsGrouped.get(list.id); const flds = this.listsFieldsGrouped.get(list.id);
const campaign = this.campaign; const campaign = this.campaign;
const mergeTags = fields.forHbsWithFieldsGrouped(flds, subscriptionGrouped); const mergeTags = fields.getMergeTags(flds, subscriptionGrouped);
return await this._getMessage(campaign, list, subscriptionGrouped, mergeTags, false); return await this._getMessage(campaign, list, subscriptionGrouped, mergeTags, false);
} }
@ -150,7 +152,7 @@ class CampaignSender {
const subscriptionGrouped = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, email); const subscriptionGrouped = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, email);
const flds = this.listsFieldsGrouped.get(listId); const flds = this.listsFieldsGrouped.get(listId);
const campaign = this.campaign; const campaign = this.campaign;
const mergeTags = fields.forHbsWithFieldsGrouped(flds, subscriptionGrouped); const mergeTags = fields.getMergeTags(flds, subscriptionGrouped);
const encryptionKeys = []; const encryptionKeys = [];
for (const fld of flds) { for (const fld of flds) {

View file

@ -108,16 +108,16 @@ function validateEmailGetMessage(result, address) {
function formatMessage(campaign, list, subscription, mergeTags, message, filter, isHTML) { function formatMessage(campaign, list, subscription, mergeTags, message, filter, isHTML) {
filter = typeof filter === 'function' ? filter : (str => str); filter = typeof filter === 'function' ? filter : (str => str);
let links = getMessageLinks(campaign, list, subscription); const links = getMessageLinks(campaign, list, subscription);
let getValue = key => { const getValue = key => {
key = (key || '').toString().toUpperCase().trim(); key = (key || '').toString().toUpperCase().trim();
if (links.hasOwnProperty(key)) { if (links.hasOwnProperty(key)) {
return links[key]; return links[key];
} }
if (mergeTags.hasOwnProperty(key)) { if (mergeTags.hasOwnProperty(key)) {
let value = (mergeTags[key] || '').toString(); const value = (mergeTags[key] || '').toString();
let containsHTML = /<[a-z][\s\S]*>/.test(value); const containsHTML = /<[a-z][\s\S]*>/.test(value);
return isHTML ? he.encode((containsHTML ? value : value.replace(/(?:\r\n|\r|\n)/g, '<br/>')), { return isHTML ? he.encode((containsHTML ? value : value.replace(/(?:\r\n|\r|\n)/g, '<br/>')), {
useNamedReferences: true, useNamedReferences: true,
allowUnsafeSymbols: true allowUnsafeSymbols: true
@ -127,7 +127,6 @@ function formatMessage(campaign, list, subscription, mergeTags, message, filter,
}; };
return message.replace(/\[([a-z0-9_]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => { return message.replace(/\[([a-z0-9_]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => {
identifier = identifier.toUpperCase();
let value = getValue(identifier); let value = getValue(identifier);
if (value === false) { if (value === false) {
return match; return match;
@ -189,6 +188,7 @@ module.exports = {
mergeTemplateIntoLayout, mergeTemplateIntoLayout,
getTemplate, getTemplate,
prepareHtml, prepareHtml,
getMessageLinks getMessageLinks,
formatMessage
}; };

View file

@ -64,7 +64,7 @@ async function listDTAjax(context, params) {
params, params,
builder => builder.from('campaigns') builder => builder.from('campaigns')
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace'), .innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace'),
['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name'] ['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
); );
} }
@ -76,7 +76,7 @@ async function listWithContentDTAjax(context, params) {
builder => builder.from('campaigns') builder => builder.from('campaigns')
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace') .innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
.whereIn('campaigns.source', [CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_TEMPLATE, CampaignSource.CUSTOM_FROM_CAMPAIGN]), .whereIn('campaigns.source', [CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_TEMPLATE, CampaignSource.CUSTOM_FROM_CAMPAIGN]),
['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.type', 'campaigns.created', 'namespaces.name'] ['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.created', 'namespaces.name']
); );
} }
@ -89,7 +89,7 @@ async function listOthersWhoseListsAreIncludedDTAjax(context, campaignId, listId
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace') .innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
.whereNot('campaigns.id', campaignId) .whereNot('campaigns.id', campaignId)
.whereNotExists(qry => qry.from('campaign_lists').whereRaw('campaign_lists.campaign = campaigns.id').whereNotIn('campaign_lists.list', listIds)), .whereNotExists(qry => qry.from('campaign_lists').whereRaw('campaign_lists.campaign = campaigns.id').whereNotIn('campaign_lists.list', listIds)),
['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.type', 'campaigns.created', 'namespaces.name'] ['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.created', 'namespaces.name']
); );
} }
@ -191,7 +191,7 @@ async function rawGetByTx(tx, key, id) {
.leftJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign') .leftJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
.groupBy('campaigns.id') .groupBy('campaigns.id')
.select([ .select([
'campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source', 'campaigns.id', 'campaigns.cid', 'campaigns.name', 'campaigns.description', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source',
'campaigns.send_configuration', 'campaigns.from_name_override', 'campaigns.from_email_override', 'campaigns.reply_to_override', 'campaigns.subject_override', 'campaigns.send_configuration', 'campaigns.from_name_override', 'campaigns.from_email_override', 'campaigns.reply_to_override', 'campaigns.subject_override',
'campaigns.data', 'campaigns.click_tracking_disabled', 'campaigns.open_tracking_disabled', 'campaigns.unsubscribe_url', 'campaigns.data', 'campaigns.click_tracking_disabled', 'campaigns.open_tracking_disabled', 'campaigns.unsubscribe_url',
knex.raw(`GROUP_CONCAT(CONCAT_WS(\':\', campaign_lists.list, campaign_lists.segment) ORDER BY campaign_lists.id SEPARATOR \';\') as lists`) knex.raw(`GROUP_CONCAT(CONCAT_WS(\':\', campaign_lists.list, campaign_lists.segment) ORDER BY campaign_lists.id SEPARATOR \';\') as lists`)
@ -428,12 +428,14 @@ async function updateWithConsistencyCheck(context, entity, content) {
}; };
} }
if (content === Content.ALL || content === Content.WITHOUT_SOURCE_CUSTOM) {
await tx('campaign_lists').where('campaign', entity.id).del();
await tx('campaign_lists').insert(entity.lists.map(x => ({campaign: entity.id, ...x})));
}
filteredEntity.data = JSON.stringify(filteredEntity.data); filteredEntity.data = JSON.stringify(filteredEntity.data);
await tx('campaigns').where('id', entity.id).update(filteredEntity); await tx('campaigns').where('id', entity.id).update(filteredEntity);
await tx('campaign_lists').where('campaign', entity.id).del();
await tx('campaign_lists').insert(entity.lists.map(x => ({campaign: entity.id, ...x})));
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: entity.id }); await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: entity.id });
}); });
} }

View file

@ -13,6 +13,7 @@ const segments = require('./segments');
const { formatDate, formatBirthday, parseDate, parseBirthday } = require('../shared/date'); const { formatDate, formatBirthday, parseDate, parseBirthday } = require('../shared/date');
const { getFieldColumn } = require('../shared/lists'); const { getFieldColumn } = require('../shared/lists');
const { cleanupFromPost } = require('../lib/helpers'); const { cleanupFromPost } = require('../lib/helpers');
const Handlebars = require('handlebars');
const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']); const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']);
@ -26,6 +27,11 @@ const Cardinality = {
MULTIPLE: 1 MULTIPLE: 1
}; };
function render(template, options) {
const renderer = Handlebars.compile(template || '');
return renderer(options);
}
fieldTypes.text = { fieldTypes.text = {
validate: field => {}, validate: field => {},
addColumn: (table, name) => table.string(name), addColumn: (table, name) => table.string(name),
@ -35,7 +41,8 @@ fieldTypes.text = {
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeText', getHbsType: field => 'typeText',
forHbs: (field, value) => value, forHbs: (field, value) => value,
parsePostValue: (field, value) => value parsePostValue: (field, value) => value,
render: (field, value) => value
}; };
fieldTypes.website = { fieldTypes.website = {
@ -47,7 +54,8 @@ fieldTypes.website = {
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeWebsite', getHbsType: field => 'typeWebsite',
forHbs: (field, value) => value, forHbs: (field, value) => value,
parsePostValue: (field, value) => value parsePostValue: (field, value) => value,
render: (field, value) => value
}; };
fieldTypes.longtext = { fieldTypes.longtext = {
@ -59,7 +67,8 @@ fieldTypes.longtext = {
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeLongtext', getHbsType: field => 'typeLongtext',
forHbs: (field, value) => value, forHbs: (field, value) => value,
parsePostValue: (field, value) => value parsePostValue: (field, value) => value,
render: (field, value) => value
}; };
fieldTypes.gpg = { fieldTypes.gpg = {
@ -71,7 +80,8 @@ fieldTypes.gpg = {
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeGpg', getHbsType: field => 'typeGpg',
forHbs: (field, value) => value, forHbs: (field, value) => value,
parsePostValue: (field, value) => value parsePostValue: (field, value) => value,
render: (field, value) => value
}; };
fieldTypes.json = { fieldTypes.json = {
@ -83,7 +93,20 @@ fieldTypes.json = {
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeJson', getHbsType: field => 'typeJson',
forHbs: (field, value) => value, forHbs: (field, value) => value,
parsePostValue: (field, value) => value parsePostValue: (field, value) => value,
render: (field, value) => {
try {
let parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
parsed = {
values: parsed
};
}
return render(field.settings.renderTemplate, parsed);
} catch (err) {
return err.message;
}
}
}; };
fieldTypes.number = { fieldTypes.number = {
@ -95,7 +118,8 @@ fieldTypes.number = {
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeNumber', getHbsType: field => 'typeNumber',
forHbs: (field, value) => value, forHbs: (field, value) => value,
parsePostValue: (field, value) => Number(value) parsePostValue: (field, value) => Number(value),
render: (field, value) => value
}; };
fieldTypes['checkbox-grouped'] = { fieldTypes['checkbox-grouped'] = {
@ -104,7 +128,18 @@ fieldTypes['checkbox-grouped'] = {
grouped: true, grouped: true,
enumerated: false, enumerated: false,
cardinality: Cardinality.MULTIPLE, cardinality: Cardinality.MULTIPLE,
getHbsType: field => 'typeCheckboxGrouped' getHbsType: field => 'typeCheckboxGrouped',
render: (field, value) => {
const subItems = value.map(col => field.groupedOptions[col].name);
if (field.settings.groupTemplate) {
return render(field.settings.groupTemplate, {
values: subItems
});
} else {
return subItems.join(', ');
}
}
}; };
fieldTypes['radio-grouped'] = { fieldTypes['radio-grouped'] = {
@ -113,7 +148,8 @@ fieldTypes['radio-grouped'] = {
grouped: true, grouped: true,
enumerated: false, enumerated: false,
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeRadioGrouped' getHbsType: field => 'typeRadioGrouped',
render: (field, value) => field.groupedOptions[value].name
}; };
fieldTypes['dropdown-grouped'] = { fieldTypes['dropdown-grouped'] = {
@ -122,7 +158,8 @@ fieldTypes['dropdown-grouped'] = {
grouped: true, grouped: true,
enumerated: false, enumerated: false,
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeDropdownGrouped' getHbsType: field => 'typeDropdownGrouped',
render: (field, value) => field.groupedOptions[value].name
}; };
fieldTypes['radio-enum'] = { fieldTypes['radio-enum'] = {
@ -135,7 +172,8 @@ fieldTypes['radio-enum'] = {
grouped: false, grouped: false,
enumerated: true, enumerated: true,
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeRadioEnum' getHbsType: field => 'typeRadioEnum',
render: (field, value) => field.groupedOptions[value].name
}; };
fieldTypes['dropdown-enum'] = { fieldTypes['dropdown-enum'] = {
@ -148,7 +186,8 @@ fieldTypes['dropdown-enum'] = {
grouped: false, grouped: false,
enumerated: true, enumerated: true,
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeDropdownEnum' getHbsType: field => 'typeDropdownEnum',
render: (field, value) => field.groupedOptions[value].name
}; };
fieldTypes.option = { fieldTypes.option = {
@ -172,7 +211,8 @@ fieldTypes['date'] = {
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeDate' + field.settings.dateFormat.charAt(0).toUpperCase() + field.settings.dateFormat.slice(1), getHbsType: field => 'typeDate' + field.settings.dateFormat.charAt(0).toUpperCase() + field.settings.dateFormat.slice(1),
forHbs: (field, value) => formatDate(field.settings.dateFormat, value), forHbs: (field, value) => formatDate(field.settings.dateFormat, value),
parsePostValue: (field, value) => parseDate(field.settings.dateFormat, value) parsePostValue: (field, value) => parseDate(field.settings.dateFormat, value),
render: (field, value) => formatDate(field.settings.dateFormat, value)
}; };
fieldTypes['birthday'] = { fieldTypes['birthday'] = {
@ -186,7 +226,8 @@ fieldTypes['birthday'] = {
cardinality: Cardinality.SINGLE, cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeBirthday' + field.settings.dateFormat.charAt(0).toUpperCase() + field.settings.dateFormat.slice(1), getHbsType: field => 'typeBirthday' + field.settings.dateFormat.charAt(0).toUpperCase() + field.settings.dateFormat.slice(1),
forHbs: (field, value) => formatBirthday(field.settings.dateFormat, value), forHbs: (field, value) => formatBirthday(field.settings.dateFormat, value),
parsePostValue: (field, value) => parseBirthday(field.settings.dateFormat, value) parsePostValue: (field, value) => parseBirthday(field.settings.dateFormat, value),
render: (field, value) => formatBirthday(field.settings.dateFormat, value)
}; };
const groupedTypes = Object.keys(fieldTypes).filter(key => fieldTypes[key].grouped); const groupedTypes = Object.keys(fieldTypes).filter(key => fieldTypes[key].grouped);
@ -627,7 +668,6 @@ function forHbsWithFieldsGrouped(fieldsGrouped, subscription) { // assumes group
} }
return customFields; return customFields;
} }
// Returns an array that can be used for rendering by Handlebars // Returns an array that can be used for rendering by Handlebars
@ -636,6 +676,22 @@ async function forHbs(context, listId, subscription) { // assumes grouped subscr
return forHbsWithFieldsGrouped(flds, subscription); return forHbsWithFieldsGrouped(flds, subscription);
} }
function getMergeTags(fieldsGrouped, subscription) { // assumes grouped subscription
const mergeTags = {
'EMAIL': subscription.email
};
for (const fld of fieldsGrouped) {
const type = fieldTypes[fld.type];
const fldCol = getFieldColumn(fld);
mergeTags[fld.key] = type.render(fld, subscription[fldCol]);
}
return mergeTags;
}
// 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, // 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,
// or (3) from import. // or (3) from import.
// If a field is not specified in the POST data, it is also omitted in the returned subscription // If a field is not specified in the POST data, it is also omitted in the returned subscription
@ -749,3 +805,4 @@ module.exports.forHbsWithFieldsGrouped = forHbsWithFieldsGrouped;
module.exports.fromPost = fromPost; module.exports.fromPost = fromPost;
module.exports.fromAPI = fromAPI; module.exports.fromAPI = fromAPI;
module.exports.fromImport = fromImport; module.exports.fromImport = fromImport;
module.exports.getMergeTags = getMergeTags;

View file

@ -12,7 +12,7 @@ const geoip = require('geoip-ultralight');
const uaParser = require('device'); const uaParser = require('device');
const he = require('he'); const he = require('he');
const { enforce } = require('../lib/helpers'); const { enforce } = require('../lib/helpers');
const { getTrustedUrl } = require('../lib/urls'); const { getPublicUrl } = require('../lib/urls');
const tools = require('../lib/tools'); const tools = require('../lib/tools');
const LinkId = { const LinkId = {
@ -28,7 +28,7 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
await knex.transaction(async tx => { await knex.transaction(async tx => {
const list = await lists.getByCidTx(tx, contextHelpers.getAdminContext(), listCid); const list = await lists.getByCidTx(tx, contextHelpers.getAdminContext(), listCid);
const campaign = await campaigns.getTrackingSettingsByCidTx(tx, campaignCid); const campaign = await campaigns.getTrackingSettingsByCidTx(tx, campaignCid);
const subscription = await subscriptions.getByCidTx(tx, contextHelpers.getAdminContext(), subscriptionCid); const subscription = await subscriptions.getByCidTx(tx, contextHelpers.getAdminContext(), list.id, subscriptionCid);
const country = geoip.lookupCountry(remoteIp) || null; const country = geoip.lookupCountry(remoteIp) || null;
const device = uaParser(userAgent, { unknownUserAgentDeviceType: 'desktop', emptyUserAgentDeviceType: 'desktop' }); const device = uaParser(userAgent, { unknownUserAgentDeviceType: 'desktop', emptyUserAgentDeviceType: 'desktop' });
@ -134,7 +134,7 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) {
// insert tracking image // insert tracking image
if (!campaign.open_tracking_disabled) { if (!campaign.open_tracking_disabled) {
let inserted = false; let inserted = false;
const imgUrl = getTrustedUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}`); const imgUrl = getPublicUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}`);
const img = '<img src="' + imgUrl + '" width="1" height="1" alt="mt">'; const img = '<img src="' + imgUrl + '" width="1" height="1" alt="mt">';
message = message.replace(/<\/body\b/i, match => { message = message.replace(/<\/body\b/i, match => {
inserted = true; inserted = true;
@ -150,7 +150,7 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) {
const urlsToBeReplaced = new Set(); const urlsToBeReplaced = new Set();
message.replace(re, (match, prefix, encodedUrl) => { message = message.replace(re, (match, prefix, encodedUrl) => {
const url = he.decode(encodedUrl, {isAttributeValue: true}); const url = he.decode(encodedUrl, {isAttributeValue: true});
urlsToBeReplaced.add(url); urlsToBeReplaced.add(url);
}); });
@ -163,12 +163,14 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) {
urls.set(url, link); urls.set(url, link);
} }
message.replace(re, (match, prefix, encodedUrl) => { message = message.replace(re, (match, prefix, encodedUrl) => {
const url = he.decode(encodedUrl, {isAttributeValue: true}); const url = he.decode(encodedUrl, {isAttributeValue: true});
const link = urls.get(url); const link = urls.get(url);
return getTrustedUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}/${link.cid}`); return getPublicUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}/${link.cid}`);
}); });
} }
return message;
} }
module.exports.LinkId = LinkId; module.exports.LinkId = LinkId;

View file

@ -8,6 +8,7 @@ const { enforce, filterObject } = require('../lib/helpers');
const hasher = require('node-object-hash')(); const hasher = require('node-object-hash')();
const moment = require('moment'); const moment = require('moment');
const fields = require('./fields'); const fields = require('./fields');
const subscriptions = require('./subscriptions');
const { parseDate, parseBirthday, DateFormat } = require('../shared/date'); const { parseDate, parseBirthday, DateFormat } = require('../shared/date');
@ -89,7 +90,7 @@ function stringValueSettings(sqlOperator, allowEmpty) {
enforce(typeof rule.value === 'string', 'Invalid value type in rule'); enforce(typeof rule.value === 'string', 'Invalid value type in rule');
enforce(allowEmpty || rule.value, 'Value in rule must not be empty'); enforce(allowEmpty || rule.value, 'Value in rule must not be empty');
}, },
addQuery: (query, rule) => query.where(rule.column, sqlOperator, rule.value) addQuery: (subsTableName, query, rule) => query.where(subsTableName + '. ' + rule.column, sqlOperator, rule.value)
}; };
} }
@ -98,7 +99,7 @@ function numberValueSettings(sqlOperator) {
validate: rule => { validate: rule => {
enforce(typeof rule.value === 'number', 'Invalid value type in rule'); enforce(typeof rule.value === 'number', 'Invalid value type in rule');
}, },
addQuery: (query, rule) => query.where(rule.column, sqlOperator, rule.value) addQuery: (subsTableName, query, rule) => query.where(subsTableName + '. ' + rule.column, sqlOperator, rule.value)
}; };
} }
@ -108,16 +109,16 @@ function dateValueSettings(thisDaySqlOperator, nextDaySqlOperator) {
const date = moment.utc(rule.value); const date = moment.utc(rule.value);
enforce(date.isValid(), 'Invalid date value'); enforce(date.isValid(), 'Invalid date value');
}, },
addQuery: (query, rule) => { addQuery: (subsTableName, query, rule) => {
const thisDay = moment.utc(rule.value).startOf('day'); const thisDay = moment.utc(rule.value).startOf('day');
const nextDay = moment(thisDay).add(1, 'days'); const nextDay = moment(thisDay).add(1, 'days');
if (thisDaySqlOperator) { if (thisDaySqlOperator) {
query.where(rule.column, thisDaySqlOperator, thisDay.toDate()) query.where(subsTableName + '. ' + rule.column, thisDaySqlOperator, thisDay.toDate())
} }
if (nextDaySqlOperator) { if (nextDaySqlOperator) {
query.where(rule.column, nextDaySqlOperator, nextDay.toDate()); query.where(subsTableName + '. ' + rule.column, nextDaySqlOperator, nextDay.toDate());
} }
} }
}; };
@ -128,16 +129,16 @@ function dateRelativeValueSettings(todaySqlOperator, tomorrowSqlOperator) {
validate: rule => { validate: rule => {
enforce(typeof rule.value === 'number', 'Invalid value type in rule'); enforce(typeof rule.value === 'number', 'Invalid value type in rule');
}, },
addQuery: (query, rule) => { addQuery: (subsTableName, query, rule) => {
const todayWithOffset = moment.utc().startOf('day').add(rule.value, 'days'); const todayWithOffset = moment.utc().startOf('day').add(rule.value, 'days');
const tomorrowWithOffset = moment(todayWithOffset).add(1, 'days'); const tomorrowWithOffset = moment(todayWithOffset).add(1, 'days');
if (todaySqlOperator) { if (todaySqlOperator) {
query.where(rule.column, todaySqlOperator, todayWithOffset.toDate()) query.where(subsTableName + '. ' + rule.column, todaySqlOperator, todayWithOffset.toDate())
} }
if (tomorrowSqlOperator) { if (tomorrowSqlOperator) {
query.where(rule.column, tomorrowSqlOperator, tomorrowWithOffset.toDate()); query.where(subsTableName + '. ' + rule.column, tomorrowSqlOperator, tomorrowWithOffset.toDate());
} }
} }
}; };
@ -146,7 +147,7 @@ function dateRelativeValueSettings(todaySqlOperator, tomorrowSqlOperator) {
function optionValueSettings(value) { function optionValueSettings(value) {
return { return {
validate: rule => {}, validate: rule => {},
addQuery: (query, rule) => query.where(rule.column, value) addQuery: (subsTableName, query, rule) => query.where(subsTableName + '. ' + rule.column, value)
}; };
} }
@ -393,6 +394,8 @@ async function getQueryGeneratorTx(tx, listId, id) {
const entity = await tx('segments').where({id, list: listId}).first(); const entity = await tx('segments').where({id, list: listId}).first();
const settings = JSON.parse(entity.settings); const settings = JSON.parse(entity.settings);
const subsTableName = subscriptions.getSubscriptionTableName(listId);
function processRule(query, rule) { function processRule(query, rule) {
if (rule.type in compositeRuleTypes) { if (rule.type in compositeRuleTypes) {
compositeRuleTypes[rule.type].addQuery(query, rule.rules, (subQuery, childRule) => { compositeRuleTypes[rule.type].addQuery(query, rule.rules, (subQuery, childRule) => {
@ -400,7 +403,7 @@ async function getQueryGeneratorTx(tx, listId, id) {
}); });
} else { } else {
const colType = fieldsByColumn[rule.column].type; const colType = fieldsByColumn[rule.column].type;
primitiveRuleTypes[colType][rule.type].addQuery(query, rule); primitiveRuleTypes[colType][rule.type].addQuery(subsTableName, query, rule);
} }
} }

View file

@ -311,6 +311,7 @@ async function listDTAjax(context, listId, segmentId, params) {
query.where(function() { query.where(function() {
addSegmentQuery(this); addSegmentQuery(this);
}); });
return query; return query;
}, },
columns, columns,

View file

@ -11,19 +11,30 @@ router.get('/:campaign/:list/:subscription', (req, res, next) => {
.then(result => { .then(result => {
const {html} = result; const {html} = result;
res.render('partials/tracking-scripts', { if (html.match(/<\/body\b/i)) {
layout: 'archive/layout-raw' res.render('partials/tracking-scripts', {
}, (err, scripts) => { layout: 'archive/layout-raw'
if (err) { }, (err, scripts) => {
return next(err); console.log(scripts);
} console.log(err);
html = scripts ? html.replace(/<\/body\b/i, match => scripts + match) : html; if (err) {
return next(err);
}
html = scripts ? html.replace(/<\/body\b/i, match => scripts + match) : html;
res.render('archive/view-raw', { res.render('archive/view', {
layout: 'archive/layout-raw', layout: 'archive/layout-raw',
message: html
});
});
} else {
res.render('archive/view', {
layout: 'archive/layout-wrapped',
message: html message: html
}); });
}); }
}) })
.catch(err => next(err)); .catch(err => next(err));
}); });

View file

@ -14,9 +14,9 @@ router.getAsync('/:campaign/:list/:subscription', async (req, res) => {
'Content-Length': trackImg.length 'Content-Length': trackImg.length
}); });
await links.countLink(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, links.LinkId.OPEN);
res.end(trackImg); res.end(trackImg);
await links.countLink(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, links.LinkId.OPEN);
}); });
@ -24,10 +24,10 @@ router.getAsync('/:campaign/:list/:subscription/:link', async (req, res) => {
const link = await links.resolve(req.params.link); const link = await links.resolve(req.params.link);
if (link) { if (link) {
await links.countLink(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, link.id);
// In Mailtrain v1 we would do the URL expansion here based on merge tags. We don't do it here anymore. Instead, the URLs are expanded when message is sent out (in links.updateLinks) // In Mailtrain v1 we would do the URL expansion here based on merge tags. We don't do it here anymore. Instead, the URLs are expanded when message is sent out (in links.updateLinks)
return res.redirect(url); res.redirect(url);
await links.countLink(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, link.id);
} else { } else {
log.error('Redirect', 'Unresolved URL: <%s>', req.url); log.error('Redirect', 'Unresolved URL: <%s>', req.url);
throw new interoperableErrors.NotFoundError('Oops, we couldn\'t find a link for the URL you clicked'); throw new interoperableErrors.NotFoundError('Oops, we couldn\'t find a link for the URL you clicked');

View file

@ -9,6 +9,10 @@ const premailerPrepareAsync = bluebird.promisify(premailerApi.prepare);
const router = require('../../lib/router-async').create(); const router = require('../../lib/router-async').create();
router.postAsync('/html-to-text', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.postAsync('/html-to-text', passport.loggedIn, passport.csrfProtection, async (req, res) => {
if (!req.body.html) {
return res.json({text: ''}); // Premailer crashes very hard when html is empty
}
const email = await premailerPrepareAsync({ const email = await premailerPrepareAsync({
html: req.body.html, html: req.body.html,
fetchHTML: false fetchHTML: false

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="icon" href="/static/favicon.ico">
<title>Mailtrain</title>
<link rel="stylesheet" href="/static/css/narrow.css">
</head>
<body>
{{{body}}}
{{> tracking_scripts}}
</body>
</html>

View file

@ -1,18 +1,18 @@
{{#if uaCode}} {{#if uaCode}}
<script> <script>
(function(i, s, o, g, r, a, m) { (function(i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r; i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function() { i[r] = i[r] || function() {
(i[r].q = i[r].q || []).push(arguments) (i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date(); }, i[r].l = 1 * new Date();
a = s.createElement(o), a = s.createElement(o),
m = s.getElementsByTagName(o)[0]; m = s.getElementsByTagName(o)[0];
a.async = 1; a.async = 1;
a.src = g; a.src = g;
m.parentNode.insertBefore(a, m) m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
ga('create', '{{uaCode}}', 'auto'); ga('create', '{{uaCode}}', 'auto');
ga('send', 'pageview'); ga('send', 'pageview');
</script> </script>
{{/if}} {{/if}}