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();
}
console.log(err);
if (req.needsRESTJSONResponse) {
const resp = {
message: err.message,

View file

@ -628,10 +628,11 @@ export default class CUD extends Component {
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
const campaignsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') }
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{ 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.')}/>;
@ -688,6 +689,13 @@ export default class CUD extends Component {
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<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')}/>
{extraSettings}

View file

@ -136,7 +136,7 @@ export default class CustomContent extends Component {
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);
@ -175,7 +175,7 @@ export default class CustomContent extends Component {
{customTemplateTypeKey && getTypeForm(this, customTemplateTypeKey, true)}
{customTemplateTypeKey && getEditForm(this, customTemplateTypeKey)}
{customTemplateTypeKey && getEditForm(this, customTemplateTypeKey, 'data_sourceCustom_')}
<ButtonRow>
<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 = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{
data: 4,
data: 5,
title: t('Status'),
render: (data, display, rowData) => {
if (data === CampaignStatus.SCHEDULED) {
const scheduled = rowData[5];
const scheduled = rowData[6];
if (scheduled && new Date(scheduled) > new Date()) {
return t('Sending scheduled');
} 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('Namespace') },
{ data: 8, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 9, title: t('Namespace') },
{
actions: data => {
const actions = [];
const perms = data[9];
const campaignType = data[3];
const campaignSource = data[6];
const perms = data[10];
const campaignType = data[4];
const campaignSource = data[7];
if (perms.includes('viewStats')) {
actions.push({

View file

@ -83,7 +83,7 @@ class TestUser extends Component {
const testUsersColumns = [
{ 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: 6, title: t('Segment') },
{ data: 7, title: t('List namespace') }

View file

@ -61,7 +61,7 @@ function getMenus(t) {
title: t('Attachments'),
link: params => `/campaigns/${params.campaignId}/attachments`,
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: {
title: t('Triggers'),

View file

@ -185,10 +185,11 @@ export default class CUD extends Component {
const campaignsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') }
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Description') },
{ data: 4, title: t('Type'), render: data => this.campaignTypeLabels[data] },
{ 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(';');

View file

@ -181,7 +181,7 @@ export default class CUD extends Component {
<InputField id="name" label={t('Name')}/>
{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')}
</StaticField>
}

View file

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

View file

@ -404,7 +404,7 @@ export default class CUD extends Component {
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}>
<InputField id="name" label={t('Name')}/>

View file

@ -81,11 +81,11 @@ export default class List extends Component {
<div>
{mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports') &&
<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>
}
<Title>{t('Imports & Tasks')}</Title>
<Title>{t('Imports')}</Title>
<Table withHeader dataUrl={`rest/imports-table/${this.props.list.id}`} columns={columns} />
</div>

View file

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

View file

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

View file

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

View file

@ -34,7 +34,7 @@ export default class UserShares extends Component {
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) {
this.sharesTables[key].refresh();
}

View file

@ -165,7 +165,7 @@ export default class CUD extends Component {
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);

View file

@ -1,99 +1,2 @@
/* Space out content a bit */
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) {
this.listsById = Map(); // listId -> list
this.listsByCid = Map(); // listCid -> list
this.listsFieldsGrouped = Map(); // listId -> fieldsGrouped
this.listsById = new Map(); // listId -> list
this.listsByCid = new Map(); // listCid -> list
this.listsFieldsGrouped = new Map(); // listId -> fieldsGrouped
this.attachments = [];
await knex.transaction(async tx => {
@ -39,13 +39,15 @@ class CampaignSender {
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) {
const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), listSpec.list);
this.listsById.set(list.id) = list;
this.listsByCid.set(list.cid) = list;
this.listsFieldsGrouped.set(list.id) = await fields.listGroupedTx(tx, list.id);
this.listsById.set(list.id, list);
this.listsByCid.set(list.cid, list);
this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id));
}
if (campaign.source === CampaignSource.TEMPLATE) {
@ -63,7 +65,7 @@ class CampaignSender {
});
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) {
@ -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)
: htmlToText.fromString(html, {wordwrap: 130});
@ -136,7 +138,7 @@ class CampaignSender {
const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid);
const flds = this.listsFieldsGrouped.get(list.id);
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);
}
@ -150,7 +152,7 @@ class CampaignSender {
const subscriptionGrouped = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, email);
const flds = this.listsFieldsGrouped.get(listId);
const campaign = this.campaign;
const mergeTags = fields.forHbsWithFieldsGrouped(flds, subscriptionGrouped);
const mergeTags = fields.getMergeTags(flds, subscriptionGrouped);
const encryptionKeys = [];
for (const fld of flds) {

View file

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

View file

@ -64,7 +64,7 @@ async function listDTAjax(context, params) {
params,
builder => builder.from('campaigns')
.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')
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
.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')
.whereNot('campaigns.id', campaignId)
.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')
.groupBy('campaigns.id')
.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.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`)
@ -428,11 +428,13 @@ async function updateWithConsistencyCheck(context, entity, content) {
};
}
filteredEntity.data = JSON.stringify(filteredEntity.data);
await tx('campaigns').where('id', entity.id).update(filteredEntity);
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);
await tx('campaigns').where('id', entity.id).update(filteredEntity);
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 { getFieldColumn } = require('../shared/lists');
const { cleanupFromPost } = require('../lib/helpers');
const Handlebars = require('handlebars');
const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']);
@ -26,6 +27,11 @@ const Cardinality = {
MULTIPLE: 1
};
function render(template, options) {
const renderer = Handlebars.compile(template || '');
return renderer(options);
}
fieldTypes.text = {
validate: field => {},
addColumn: (table, name) => table.string(name),
@ -35,7 +41,8 @@ fieldTypes.text = {
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeText',
forHbs: (field, value) => value,
parsePostValue: (field, value) => value
parsePostValue: (field, value) => value,
render: (field, value) => value
};
fieldTypes.website = {
@ -47,7 +54,8 @@ fieldTypes.website = {
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeWebsite',
forHbs: (field, value) => value,
parsePostValue: (field, value) => value
parsePostValue: (field, value) => value,
render: (field, value) => value
};
fieldTypes.longtext = {
@ -59,7 +67,8 @@ fieldTypes.longtext = {
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeLongtext',
forHbs: (field, value) => value,
parsePostValue: (field, value) => value
parsePostValue: (field, value) => value,
render: (field, value) => value
};
fieldTypes.gpg = {
@ -71,7 +80,8 @@ fieldTypes.gpg = {
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeGpg',
forHbs: (field, value) => value,
parsePostValue: (field, value) => value
parsePostValue: (field, value) => value,
render: (field, value) => value
};
fieldTypes.json = {
@ -83,7 +93,20 @@ fieldTypes.json = {
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeJson',
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 = {
@ -95,7 +118,8 @@ fieldTypes.number = {
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeNumber',
forHbs: (field, value) => value,
parsePostValue: (field, value) => Number(value)
parsePostValue: (field, value) => Number(value),
render: (field, value) => value
};
fieldTypes['checkbox-grouped'] = {
@ -104,7 +128,18 @@ fieldTypes['checkbox-grouped'] = {
grouped: true,
enumerated: false,
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'] = {
@ -113,7 +148,8 @@ fieldTypes['radio-grouped'] = {
grouped: true,
enumerated: false,
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeRadioGrouped'
getHbsType: field => 'typeRadioGrouped',
render: (field, value) => field.groupedOptions[value].name
};
fieldTypes['dropdown-grouped'] = {
@ -122,7 +158,8 @@ fieldTypes['dropdown-grouped'] = {
grouped: true,
enumerated: false,
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeDropdownGrouped'
getHbsType: field => 'typeDropdownGrouped',
render: (field, value) => field.groupedOptions[value].name
};
fieldTypes['radio-enum'] = {
@ -135,7 +172,8 @@ fieldTypes['radio-enum'] = {
grouped: false,
enumerated: true,
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeRadioEnum'
getHbsType: field => 'typeRadioEnum',
render: (field, value) => field.groupedOptions[value].name
};
fieldTypes['dropdown-enum'] = {
@ -148,7 +186,8 @@ fieldTypes['dropdown-enum'] = {
grouped: false,
enumerated: true,
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeDropdownEnum'
getHbsType: field => 'typeDropdownEnum',
render: (field, value) => field.groupedOptions[value].name
};
fieldTypes.option = {
@ -172,7 +211,8 @@ fieldTypes['date'] = {
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeDate' + field.settings.dateFormat.charAt(0).toUpperCase() + field.settings.dateFormat.slice(1),
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'] = {
@ -186,7 +226,8 @@ fieldTypes['birthday'] = {
cardinality: Cardinality.SINGLE,
getHbsType: field => 'typeBirthday' + field.settings.dateFormat.charAt(0).toUpperCase() + field.settings.dateFormat.slice(1),
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);
@ -627,7 +668,6 @@ function forHbsWithFieldsGrouped(fieldsGrouped, subscription) { // assumes group
}
return customFields;
}
// 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);
}
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,
// or (3) from import.
// 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.fromAPI = fromAPI;
module.exports.fromImport = fromImport;
module.exports.getMergeTags = getMergeTags;

View file

@ -12,7 +12,7 @@ const geoip = require('geoip-ultralight');
const uaParser = require('device');
const he = require('he');
const { enforce } = require('../lib/helpers');
const { getTrustedUrl } = require('../lib/urls');
const { getPublicUrl } = require('../lib/urls');
const tools = require('../lib/tools');
const LinkId = {
@ -28,7 +28,7 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
await knex.transaction(async tx => {
const list = await lists.getByCidTx(tx, contextHelpers.getAdminContext(), listCid);
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 device = uaParser(userAgent, { unknownUserAgentDeviceType: 'desktop', emptyUserAgentDeviceType: 'desktop' });
@ -134,7 +134,7 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) {
// insert tracking image
if (!campaign.open_tracking_disabled) {
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">';
message = message.replace(/<\/body\b/i, match => {
inserted = true;
@ -150,7 +150,7 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) {
const urlsToBeReplaced = new Set();
message.replace(re, (match, prefix, encodedUrl) => {
message = message.replace(re, (match, prefix, encodedUrl) => {
const url = he.decode(encodedUrl, {isAttributeValue: true});
urlsToBeReplaced.add(url);
});
@ -163,12 +163,14 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) {
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 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;

View file

@ -8,6 +8,7 @@ const { enforce, filterObject } = require('../lib/helpers');
const hasher = require('node-object-hash')();
const moment = require('moment');
const fields = require('./fields');
const subscriptions = require('./subscriptions');
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(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 => {
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);
enforce(date.isValid(), 'Invalid date value');
},
addQuery: (query, rule) => {
addQuery: (subsTableName, query, rule) => {
const thisDay = moment.utc(rule.value).startOf('day');
const nextDay = moment(thisDay).add(1, 'days');
if (thisDaySqlOperator) {
query.where(rule.column, thisDaySqlOperator, thisDay.toDate())
query.where(subsTableName + '. ' + rule.column, thisDaySqlOperator, thisDay.toDate())
}
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 => {
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 tomorrowWithOffset = moment(todayWithOffset).add(1, 'days');
if (todaySqlOperator) {
query.where(rule.column, todaySqlOperator, todayWithOffset.toDate())
query.where(subsTableName + '. ' + rule.column, todaySqlOperator, todayWithOffset.toDate())
}
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) {
return {
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 settings = JSON.parse(entity.settings);
const subsTableName = subscriptions.getSubscriptionTableName(listId);
function processRule(query, rule) {
if (rule.type in compositeRuleTypes) {
compositeRuleTypes[rule.type].addQuery(query, rule.rules, (subQuery, childRule) => {
@ -400,7 +403,7 @@ async function getQueryGeneratorTx(tx, listId, id) {
});
} else {
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() {
addSegmentQuery(this);
});
return query;
},
columns,

View file

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

View file

@ -14,9 +14,9 @@ router.getAsync('/:campaign/:list/:subscription', async (req, res) => {
'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);
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);
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)
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 {
log.error('Redirect', 'Unresolved URL: <%s>', req.url);
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();
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({
html: req.body.html,
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>