fixed conflicts
This commit is contained in:
commit
6b87a9711f
74 changed files with 3540 additions and 34844 deletions
|
@ -1,5 +1,12 @@
|
|||
# Changelog
|
||||
|
||||
## 1.23.0 2017-03-19
|
||||
|
||||
* Fixed security issue where description tags were able to include script tags. Reported by Andreas Lindh. Fixed with [ae6affda](https://github.com/andris9/mailtrain/commit/ae6affda8193f034e06f7e095ee23821a83d5190)
|
||||
* Fixed security issue where templates that looked like file paths loaded content from arbitrary files. Reported by Andreas Lindh. Fixed with [0879fa41](https://github.com/andris9/mailtrain/commit/0879fa412a2d4a417aeca5cd5092a8f86531e7ef)
|
||||
* Fixed security issue where users were able to use html tags in subscription values. Reported by Andreas Lindh. Fixed with [9d5fb816](https://github.com/andris9/mailtrain/commit/9d5fb816c937114966d4f589e1ad4e164ff3a187)
|
||||
* Support for multiple HTML editors (Mosaico, GrapeJS, Summernote, HTML code)
|
||||
|
||||
## 1.22.0 2017-03-02
|
||||
|
||||
* Reverted license back to GPL-v3 to support Mosaico
|
7
app.js
7
app.js
|
@ -29,6 +29,7 @@ let templates = require('./routes/templates');
|
|||
let campaigns = require('./routes/campaigns');
|
||||
let links = require('./routes/links');
|
||||
let fields = require('./routes/fields');
|
||||
let forms = require('./routes/forms');
|
||||
let segments = require('./routes/segments');
|
||||
let triggers = require('./routes/triggers');
|
||||
let webhooks = require('./routes/webhooks');
|
||||
|
@ -54,6 +55,7 @@ if (config.www.proxy) {
|
|||
app.disable('x-powered-by');
|
||||
|
||||
hbs.registerPartials(__dirname + '/views/partials');
|
||||
hbs.registerPartials(__dirname + '/views/subscription/partials/');
|
||||
|
||||
/**
|
||||
* We need this helper to make sure that we consume flash messages only
|
||||
|
@ -80,7 +82,9 @@ hbs.registerHelper('flash_messages', function () { // eslint-disable-line prefer
|
|||
let rows = [];
|
||||
|
||||
messages[key].forEach(message => {
|
||||
rows.push(hbs.handlebars.escapeExpression(message));
|
||||
message = hbs.handlebars.escapeExpression(message);
|
||||
message = message.replace(/(\r\n|\n|\r)/gm, '<br>');
|
||||
rows.push(message);
|
||||
});
|
||||
|
||||
if (rows.length > 1) {
|
||||
|
@ -205,6 +209,7 @@ app.use('/campaigns', campaigns);
|
|||
app.use('/settings', settings);
|
||||
app.use('/links', links);
|
||||
app.use('/fields', fields);
|
||||
app.use('/forms', forms);
|
||||
app.use('/segments', segments);
|
||||
app.use('/triggers', triggers);
|
||||
app.use('/webhooks', webhooks);
|
||||
|
|
|
@ -38,6 +38,9 @@ language="en"
|
|||
# Inject custom scripts in layout.hbs
|
||||
# customscripts=["/custom/hello-world.js"]
|
||||
|
||||
# Inject custom scripts in subscription/layout.mjml.hbs
|
||||
# customsubscriptionscripts=["/custom/hello-world.js"]
|
||||
|
||||
# If you start out as a root user (eg. if you want to use ports lower than 1000)
|
||||
# then you can downgrade the user once all services are up and running
|
||||
#user="nobody"
|
||||
|
@ -112,6 +115,8 @@ host="localhost"
|
|||
port=3002
|
||||
baseDN="ou=users,dc=company"
|
||||
filter="(|(username={{username}})(mail={{username}}))"
|
||||
#Username field in LDAP (uid/cn/username)
|
||||
uidTag="username"
|
||||
passwordresetlink=""
|
||||
|
||||
[postfixbounce]
|
||||
|
|
File diff suppressed because it is too large
Load diff
187
lib/helpers.js
187
lib/helpers.js
|
@ -1,12 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
let config = require('config');
|
||||
let path = require('path');
|
||||
let fs = require('fs');
|
||||
let tools = require('./tools');
|
||||
let settings = require('./models/settings');
|
||||
let lists = require('./models/lists');
|
||||
let fields = require('./models/fields');
|
||||
let forms = require('./models/forms');
|
||||
let _ = require('./translate')._;
|
||||
let objectHash = require('object-hash');
|
||||
let mjml = require('mjml');
|
||||
let mjmlTemplates = new Map();
|
||||
let hbs = require('hbs');
|
||||
|
||||
module.exports = {
|
||||
getDefaultMergeTags,
|
||||
getListMergeTags
|
||||
getListMergeTags,
|
||||
captureFlashMessages,
|
||||
injectCustomFormData,
|
||||
injectCustomFormTemplates,
|
||||
filterCustomFields,
|
||||
getMjmlTemplate
|
||||
};
|
||||
|
||||
function getDefaultMergeTags(callback) {
|
||||
|
@ -73,3 +88,173 @@ function getListMergeTags(listId, callback) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
function filterCustomFields(customFieldsIn = [], fieldIds = [], method = 'include') {
|
||||
let customFields = customFieldsIn.slice();
|
||||
fieldIds = typeof fieldIds === 'string' ? fieldIds.split(',') : fieldIds;
|
||||
|
||||
customFields.unshift({
|
||||
id: 'email',
|
||||
name: 'Email Address',
|
||||
type: 'Email',
|
||||
typeSubsciptionEmail: true
|
||||
}, {
|
||||
id: 'firstname',
|
||||
name: 'First Name',
|
||||
type: 'Text',
|
||||
typeFirstName: true
|
||||
}, {
|
||||
id: 'lastname',
|
||||
name: 'Last Name',
|
||||
type: 'Text',
|
||||
typeLastName: true
|
||||
});
|
||||
|
||||
let filtered = [];
|
||||
|
||||
if (method === 'include') {
|
||||
fieldIds.forEach(id => {
|
||||
let field = customFields.find(f => f.id.toString() === id);
|
||||
field && filtered.push(field);
|
||||
});
|
||||
} else {
|
||||
customFields.forEach(field => {
|
||||
!fieldIds.includes(field.id.toString()) && filtered.push(field);
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function injectCustomFormData(customFormId, viewPath, data, callback) {
|
||||
|
||||
let injectDefaultData = data => {
|
||||
data.customFields = filterCustomFields(data.customFields, [], 'exclude');
|
||||
data.formInputStyle = '@import url(/subscription/form-input-style.css);';
|
||||
return data;
|
||||
};
|
||||
|
||||
if (Number(customFormId) < 1) {
|
||||
return callback(null, injectDefaultData(data));
|
||||
}
|
||||
|
||||
forms.get(customFormId, (err, form) => {
|
||||
if (err) {
|
||||
return callback(null, injectDefaultData(data));
|
||||
}
|
||||
|
||||
let view = viewPath.split('/')[1];
|
||||
|
||||
if (view === 'web-subscribe') {
|
||||
data.customFields = form.fieldsShownOnSubscribe
|
||||
? filterCustomFields(data.customFields, form.fieldsShownOnSubscribe)
|
||||
: filterCustomFields(data.customFields, [], 'exclude');
|
||||
} else if (view === 'web-manage') {
|
||||
data.customFields = form.fieldsShownOnManage
|
||||
? filterCustomFields(data.customFields, form.fieldsShownOnManage)
|
||||
: filterCustomFields(data.customFields, [], 'exclude');
|
||||
}
|
||||
|
||||
let key = tools.fromDbKey(view);
|
||||
data.template.template = form[key] || data.template.template;
|
||||
data.template.layout = form.layout || data.template.layout;
|
||||
data.formInputStyle = form.formInputStyle || '@import url(/subscription/form-input-style.css);';
|
||||
|
||||
settings.list(['ua_code'], (err, configItems) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
data.uaCode = configItems.uaCode;
|
||||
data.customSubscriptionScripts = config.customsubscriptionscripts || [];
|
||||
callback(null, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function injectCustomFormTemplates(customFormId, templates, callback) {
|
||||
if (Number(customFormId) < 1) {
|
||||
return callback(null, templates);
|
||||
}
|
||||
|
||||
forms.get(customFormId, (err, form) => {
|
||||
if (err) {
|
||||
return callback(null, templates);
|
||||
}
|
||||
|
||||
let lookUp = name => {
|
||||
let key = tools.fromDbKey(
|
||||
/subscription\/([^.]*)/.exec(name)[1]
|
||||
);
|
||||
return form[key] || name;
|
||||
};
|
||||
|
||||
Object.keys(templates).forEach(key => {
|
||||
let value = templates[key];
|
||||
|
||||
if (typeof value === 'string') {
|
||||
templates[key] = lookUp(value);
|
||||
}
|
||||
if (typeof value === 'object' && value.template) {
|
||||
templates[key].template = lookUp(value.template);
|
||||
}
|
||||
if (typeof value === 'object' && value.layout) {
|
||||
templates[key].layout = lookUp(value.layout);
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, templates);
|
||||
});
|
||||
}
|
||||
|
||||
function getMjmlTemplate(template, callback) {
|
||||
if (!template) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let key = (typeof template === 'object') ? objectHash(template) : template;
|
||||
|
||||
if (mjmlTemplates.has(key)) {
|
||||
return callback(null, mjmlTemplates.get(key));
|
||||
}
|
||||
|
||||
let done = source => {
|
||||
let compiled;
|
||||
try {
|
||||
compiled = mjml.mjml2html(source);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (compiled.errors.length) {
|
||||
return callback(compiled.errors[0].message || compiled.errors[0]);
|
||||
}
|
||||
let renderer = hbs.handlebars.compile(compiled.html);
|
||||
mjmlTemplates.set(key, renderer);
|
||||
callback(null, renderer);
|
||||
};
|
||||
|
||||
if (typeof template === 'object') {
|
||||
tools.mergeTemplateIntoLayout(template.template, template.layout, (err, source) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
done(source);
|
||||
});
|
||||
} else {
|
||||
fs.readFile(path.join(__dirname, '..', 'views', template), 'utf-8', (err, source) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
done(source);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function captureFlashMessages(req, res, callback) {
|
||||
res.render('subscription/capture-flash-messages', { layout: null }, (err, flash) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, flash);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ let path = require('path');
|
|||
let templates = new Map();
|
||||
let htmlToText = require('html-to-text');
|
||||
let aws = require('aws-sdk');
|
||||
let objectHash = require('object-hash');
|
||||
let mjml = require('mjml');
|
||||
|
||||
let _ = require('./translate')._;
|
||||
let util = require('util');
|
||||
|
@ -124,18 +126,46 @@ function getTemplate(template, callback) {
|
|||
return callback(null, false);
|
||||
}
|
||||
|
||||
if (templates.has(template)) {
|
||||
return callback(null, templates.get(template));
|
||||
let key = (typeof template === 'object') ? objectHash(template) : template;
|
||||
|
||||
if (templates.has(key)) {
|
||||
return callback(null, templates.get(key));
|
||||
}
|
||||
|
||||
fs.readFile(path.join(__dirname, '..', 'views', template), 'utf-8', (err, source) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
let done = (source, isMjml = false) => {
|
||||
if (isMjml) {
|
||||
let compiled;
|
||||
try {
|
||||
compiled = mjml.mjml2html(source);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (compiled.errors.length) {
|
||||
return callback(compiled.errors[0].message || compiled.errors[0]);
|
||||
}
|
||||
source = compiled.html;
|
||||
}
|
||||
let renderer = Handlebars.compile(source);
|
||||
templates.set(template, renderer);
|
||||
return callback(null, renderer);
|
||||
});
|
||||
templates.set(key, renderer);
|
||||
callback(null, renderer);
|
||||
};
|
||||
|
||||
if (typeof template === 'object') {
|
||||
tools.mergeTemplateIntoLayout(template.template, template.layout, (err, source) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let isMjml = template.type === 'mjml';
|
||||
done(source, isMjml);
|
||||
});
|
||||
} else {
|
||||
fs.readFile(path.join(__dirname, '..', 'views', template), 'utf-8', (err, source) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
done(source);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createMailer(callback) {
|
||||
|
|
|
@ -619,6 +619,9 @@ module.exports.create = (campaign, opts, callback) => {
|
|||
Object.keys(campaign).forEach(key => {
|
||||
let value = typeof campaign[key] === 'number' ? campaign[key] : (campaign[key] || '').toString().trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0 && keys.indexOf(key) < 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
|
@ -791,6 +794,9 @@ module.exports.update = (id, updates, callback) => {
|
|||
Object.keys(campaign).forEach(key => {
|
||||
let value = typeof campaign[key] === 'number' ? campaign[key] : (campaign[key] || '').toString().trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0 && keys.indexOf(key) < 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
|
|
|
@ -405,6 +405,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
case 'longtext':
|
||||
{
|
||||
let item = {
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
column: field.column,
|
||||
|
@ -434,6 +435,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
}
|
||||
|
||||
let item = {
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
column: field.column,
|
||||
|
@ -449,6 +451,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
case 'number':
|
||||
{
|
||||
let item = {
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
column: field.column,
|
||||
|
@ -466,6 +469,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
case 'checkbox':
|
||||
{
|
||||
let item = {
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
visible: !!field.visible,
|
||||
|
@ -556,6 +560,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
}
|
||||
|
||||
let item = {
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
column: field.column,
|
||||
|
|
409
lib/models/forms.js
Normal file
409
lib/models/forms.js
Normal file
|
@ -0,0 +1,409 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let fs = require('fs');
|
||||
let path = require('path');
|
||||
let tools = require('../tools');
|
||||
let mjml = require('mjml');
|
||||
let _ = require('../translate')._;
|
||||
|
||||
let allowedKeys = [
|
||||
'name',
|
||||
'description',
|
||||
'fields_shown_on_subscribe',
|
||||
'fields_shown_on_manage',
|
||||
'layout',
|
||||
'form_input_style',
|
||||
'mail_confirm_html',
|
||||
'mail_confirm_text',
|
||||
'mail_subscription_confirmed_html',
|
||||
'mail_subscription_confirmed_text',
|
||||
'mail_unsubscribe_confirmed_html',
|
||||
'mail_unsubscribe_confirmed_text',
|
||||
'web_confirm_notice',
|
||||
'web_manage_address',
|
||||
'web_manage',
|
||||
'web_subscribe',
|
||||
'web_subscribed',
|
||||
'web_unsubscribe_notice',
|
||||
'web_unsubscribe',
|
||||
'web_updated_notice'
|
||||
];
|
||||
|
||||
module.exports.list = (listId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM custom_forms WHERE list=? ORDER BY id', [listId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let formList = rows && rows.map(row => tools.convertKeys(row)) || [];
|
||||
return callback(null, formList);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Form ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM custom_forms WHERE id=? LIMIT 1', [id], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let form = rows && rows[0] && tools.convertKeys(rows[0]) || false;
|
||||
|
||||
if (!form) {
|
||||
connection.release();
|
||||
return callback(new Error('Selected form not found'));
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM custom_forms_data WHERE form=?', [id], (err, data_rows = []) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
data_rows.forEach(data_row => {
|
||||
let modelKey = tools.fromDbKey(data_row.data_key);
|
||||
form[modelKey] = data_row.data_value;
|
||||
});
|
||||
|
||||
return callback(null, form);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports.create = (listId, form, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing Form ID')));
|
||||
}
|
||||
|
||||
form = tools.convertKeys(form);
|
||||
form = setDefaultValues(form);
|
||||
form.name = (form.name || '').toString().trim();
|
||||
|
||||
if (!form.name) {
|
||||
return callback(new Error(_('Form Name must be set')));
|
||||
}
|
||||
|
||||
let keys = ['list'];
|
||||
let values = [listId];
|
||||
|
||||
Object.keys(form).forEach(key => {
|
||||
let value = form[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let filtered = filterKeysAndValues(keys, values, 'exclude', ['mail_', 'web_']);
|
||||
let query = 'INSERT INTO custom_forms (' + filtered.keys.join(', ') + ') VALUES (' + filtered.values.map(() => '?').join(',') + ')';
|
||||
|
||||
connection.query(query, filtered.values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let formId = result && result.insertId;
|
||||
|
||||
if (!formId) {
|
||||
return callback(new Error('Invalid custom_forms insertId'));
|
||||
}
|
||||
|
||||
let jobs = 1;
|
||||
let error = null;
|
||||
|
||||
let done = err => {
|
||||
jobs--;
|
||||
error = err ? err : error; // One's enough
|
||||
jobs === 0 && callback(error, formId);
|
||||
};
|
||||
|
||||
filtered = filterKeysAndValues(keys, values, 'include', ['mail_', 'web_']);
|
||||
|
||||
filtered.keys.forEach((key, index) => {
|
||||
jobs++;
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
connection.query('INSERT INTO custom_forms_data (form, data_key, data_value) VALUES (?, ?, ?)', [formId, key, filtered.values[index]], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
return done(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
done(null);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
updates = tools.convertKeys(updates);
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Form ID')));
|
||||
}
|
||||
|
||||
if (!(updates.name || '').toString().trim()) {
|
||||
return callback(new Error(_('Form Name must be set')));
|
||||
}
|
||||
|
||||
let keys = [];
|
||||
let values = [];
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
let value = typeof updates[key] === 'string' ? updates[key].trim() : updates[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let filtered = filterKeysAndValues(keys, values, 'exclude', ['mail_', 'web_']);
|
||||
let query = 'UPDATE custom_forms SET ' + filtered.keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1';
|
||||
|
||||
connection.query(query, filtered.values.concat(id), (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let affectedRows = result && result.affectedRows;
|
||||
|
||||
let jobs = 1;
|
||||
let error = null;
|
||||
|
||||
let done = err => {
|
||||
jobs--;
|
||||
error = err ? err : error; // One's enough
|
||||
|
||||
if (jobs === 0) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// Save then validate, as otherwise their work get's lost ...
|
||||
err = testForMjmlErrors(keys, values);
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, affectedRows);
|
||||
}
|
||||
};
|
||||
|
||||
filtered = filterKeysAndValues(keys, values, 'include', ['mail_', 'web_']);
|
||||
|
||||
filtered.keys.forEach((key, index) => {
|
||||
jobs++;
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
connection.query('UPDATE custom_forms_data SET data_value=? WHERE data_key=? AND form=?', [filtered.values[index], key, id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
return done(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
done(null);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (formId, callback) => {
|
||||
formId = Number(formId) || 0;
|
||||
|
||||
if (formId < 1) {
|
||||
return callback(new Error(_('Missing Form ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM custom_forms WHERE id=? LIMIT 1', [formId], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
connection.release();
|
||||
return callback(new Error(_('Custom form not found')));
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM custom_forms WHERE id=? LIMIT 1', [formId], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function setDefaultValues(form) {
|
||||
let getContents = fileName => {
|
||||
try {
|
||||
let basePath = path.join(__dirname, '..', '..');
|
||||
let template = fs.readFileSync(path.join(basePath, fileName), 'utf8');
|
||||
return template.replace(/\{\{#translate\}\}(.*?)\{\{\/translate\}\}/g, (m, s) => _(s));
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
allowedKeys.forEach(key => {
|
||||
let modelKey = tools.fromDbKey(key);
|
||||
let base = 'views/subscription/' + key.replace(/_/g, '-');
|
||||
|
||||
if (key.startsWith('mail') || key.startsWith('web')) {
|
||||
form[modelKey] = getContents(base + '.mjml.hbs') || getContents(base + '.hbs') || '';
|
||||
}
|
||||
});
|
||||
|
||||
form.layout = getContents('views/subscription/layout.mjml.hbs') || '';
|
||||
form.formInputStyle = getContents('public/subscription/form-input-style.css') || '@import url(/subscription/form-input-style.css);';
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
function filterKeysAndValues(keysIn, valuesIn, method = 'include', prefixes = []) {
|
||||
let values = [];
|
||||
|
||||
let prefixMatch = key => (
|
||||
prefixes.some(prefix => key.startsWith(prefix))
|
||||
);
|
||||
|
||||
let keys = keysIn.filter((key, index) => {
|
||||
if ((method === 'include' && prefixMatch(key)) || (method === 'exclude' && !prefixMatch(key))) {
|
||||
values.push(valuesIn[index]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
keys,
|
||||
values
|
||||
};
|
||||
}
|
||||
|
||||
function testForMjmlErrors(keys, values) {
|
||||
|
||||
let errors = [];
|
||||
let testLayout = '<mjml><mj-body><mj-container>{{{body}}}</mj-container></mj-body></mjml>';
|
||||
|
||||
let hasMjmlError = (template, layout = testLayout) => {
|
||||
let source = layout.replace(/\{\{\{body\}\}\}/g, template);
|
||||
let compiled;
|
||||
|
||||
try {
|
||||
compiled = mjml.mjml2html(source);
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (compiled.errors.length) {
|
||||
return compiled.errors[0].message || compiled.errors[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
if (key.startsWith('mail_') || key.startsWith('web_')) {
|
||||
|
||||
let template = values[index];
|
||||
let err = hasMjmlError(template);
|
||||
|
||||
err && errors.push(key + ': ' + (err.message || err));
|
||||
key === 'mail_confirm_html' && !template.includes('{{confirmUrl}}') && errors.push(key + ': Missing {{confirmUrl}}');
|
||||
|
||||
} else if (key === 'layout') {
|
||||
|
||||
let layout = values[index];
|
||||
let err = hasMjmlError('', layout);
|
||||
|
||||
err && errors.push('layout: ' + (err.message || err));
|
||||
!layout.includes('{{{body}}}') && errors.push('layout: {{{body}}} not found');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (errors.length) {
|
||||
errors.forEach((err, index) => {
|
||||
errors[index] = (index + 1) + ') ' + err;
|
||||
});
|
||||
return 'Please fix these MJML errors:\n\n' + errors.join('\n');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
|
@ -6,7 +6,7 @@ let shortid = require('shortid');
|
|||
let segments = require('./segments');
|
||||
let _ = require('../translate')._;
|
||||
|
||||
let allowedKeys = ['description'];
|
||||
let allowedKeys = ['description', 'default_form'];
|
||||
|
||||
module.exports.list = (start, limit, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
|
@ -123,6 +123,9 @@ module.exports.create = (list, callback) => {
|
|||
Object.keys(list).forEach(key => {
|
||||
let value = list[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
|
@ -182,6 +185,9 @@ module.exports.update = (id, updates, callback) => {
|
|||
Object.keys(updates).forEach(key => {
|
||||
let value = updates[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
let db = require('../db');
|
||||
let shortid = require('shortid');
|
||||
let tools = require('../tools');
|
||||
let helpers = require('../helpers');
|
||||
let fields = require('./fields');
|
||||
let geoip = require('geoip-ultralight');
|
||||
let segments = require('./segments');
|
||||
|
@ -210,7 +211,7 @@ module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
|
|||
}
|
||||
});
|
||||
|
||||
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'serviceUrl'], (err, configItems) => {
|
||||
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl'], (err, configItems) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
@ -221,35 +222,55 @@ module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
|
|||
return;
|
||||
}
|
||||
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: [].concat(data.firstName || []).concat(data.lastName || []).join(' '),
|
||||
address: email
|
||||
},
|
||||
subject: util.format(_('%s: Please Confirm Subscription'), list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html: 'emails/confirm-html.hbs',
|
||||
text: 'emails/confirm-text.hbs',
|
||||
data: {
|
||||
title: list.name,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
confirmUrl: urllib.resolve(configItems.serviceUrl, '/subscription/subscribe/' + cid)
|
||||
}
|
||||
}, err => {
|
||||
let sendMail = (html, text) => {
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: [].concat(data.firstName || []).concat(data.lastName || []).join(' '),
|
||||
address: email
|
||||
},
|
||||
subject: util.format(_('%s: Please Confirm Subscription'), list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
data: {
|
||||
title: list.name,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
confirmUrl: urllib.resolve(configItems.serviceUrl, '/subscription/subscribe/' + cid)
|
||||
}
|
||||
}, err => {
|
||||
if (err) {
|
||||
log.error('Subscription', err.stack);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let text = {
|
||||
template: 'subscription/mail-confirm-text.hbs'
|
||||
};
|
||||
|
||||
let html = {
|
||||
template: 'subscription/mail-confirm-html.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs',
|
||||
type: 'mjml'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormTemplates(list.defaultForm, { text, html }, (err, tmpl) => {
|
||||
if (err) {
|
||||
log.error('Subscription', err.stack);
|
||||
return sendMail(html, text);
|
||||
}
|
||||
|
||||
sendMail(tmpl.html, tmpl.text);
|
||||
});
|
||||
});
|
||||
return callback(null, cid);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -88,6 +88,9 @@ module.exports.create = (template, callback) => {
|
|||
Object.keys(template).forEach(key => {
|
||||
let value = template[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
|
@ -133,6 +136,9 @@ module.exports.update = (id, updates, callback) => {
|
|||
Object.keys(updates).forEach(key => {
|
||||
let value = updates[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
|
|
|
@ -16,7 +16,9 @@ let LdapStrategy;
|
|||
try {
|
||||
LdapStrategy = require('passport-ldapjs').Strategy; // eslint-disable-line global-require
|
||||
} catch (E) {
|
||||
// ignore
|
||||
if (config.ldap.enabled) {
|
||||
log.info('LDAP', 'Module "passport-ldapjs" not installed. LDAP auth will fail.');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.csrfProtection = csrf({
|
||||
|
@ -80,27 +82,28 @@ if (config.ldap.enabled && LdapStrategy) {
|
|||
base: config.ldap.baseDN,
|
||||
search: {
|
||||
filter: config.ldap.filter,
|
||||
attributes: ['username', 'mail'],
|
||||
attributes: [config.ldap.uidTag, 'mail'],
|
||||
scope: 'sub'
|
||||
}
|
||||
},
|
||||
uidTag: config.ldap.uidTag
|
||||
};
|
||||
|
||||
passport.use(new LdapStrategy(opts, (profile, done) => {
|
||||
users.findByUsername(profile.username, (err, user) => {
|
||||
users.findByUsername(profile[config.ldap.uidTag], (err, user) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
// password is empty for ldap
|
||||
users.add(profile.username, '', profile.mail, (err, id) => {
|
||||
users.add(profile[config.ldap.uidTag], '', profile.mail, (err, id) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
return done(null, {
|
||||
id,
|
||||
username: profile.username
|
||||
username: profile[config.ldap.uidTag]
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
|
80
lib/tools.js
80
lib/tools.js
|
@ -1,13 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
let fs = require('fs');
|
||||
let path = require('path');
|
||||
let db = require('./db');
|
||||
let slugify = require('slugify');
|
||||
let Isemail = require('isemail');
|
||||
let urllib = require('url');
|
||||
let juice = require('juice');
|
||||
let jsdom = require('jsdom');
|
||||
let he = require('he');
|
||||
let _ = require('./translate')._;
|
||||
let util = require('util');
|
||||
let createDOMPurify = require('dompurify');
|
||||
|
||||
let blockedUsers = ['abuse', 'admin', 'billing', 'compliance', 'devnull', 'dns', 'ftp', 'hostmaster', 'inoc', 'ispfeedback', 'ispsupport', 'listrequest', 'list', 'maildaemon', 'noc', 'noreply', 'noreply', 'null', 'phish', 'phishing', 'postmaster', 'privacy', 'registrar', 'root', 'security', 'spam', 'support', 'sysadmin', 'tech', 'undisclosedrecipients', 'unsubscribe', 'usenet', 'uucp', 'webmaster', 'www'];
|
||||
|
||||
|
@ -22,6 +26,8 @@ module.exports = {
|
|||
formatMessage,
|
||||
getMessageLinks,
|
||||
prepareHtml,
|
||||
purifyHTML,
|
||||
mergeTemplateIntoLayout,
|
||||
workers: new Set()
|
||||
};
|
||||
|
||||
|
@ -169,7 +175,7 @@ function getMessageLinks(serviceUrl, campaign, list, subscription) {
|
|||
};
|
||||
}
|
||||
|
||||
function formatMessage(serviceUrl, campaign, list, subscription, message, filter) {
|
||||
function formatMessage(serviceUrl, campaign, list, subscription, message, filter, isHTML) {
|
||||
filter = typeof filter === 'function' ? filter : (str => str);
|
||||
|
||||
let links = getMessageLinks(serviceUrl, campaign, list, subscription);
|
||||
|
@ -180,7 +186,9 @@ function formatMessage(serviceUrl, campaign, list, subscription, message, filter
|
|||
return links[key];
|
||||
}
|
||||
if (subscription.mergeTags.hasOwnProperty(key)) {
|
||||
return subscription.mergeTags[key];
|
||||
return isHTML ? he.encode(subscription.mergeTags[key], {
|
||||
useNamedReferences: true
|
||||
}) : subscription.mergeTags[key];
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
@ -196,8 +204,13 @@ function prepareHtml(html, callback) {
|
|||
if (!(html || '').toString().trim()) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
jsdom.env(html, (err, win) => {
|
||||
jsdom.env(false, false, {
|
||||
html,
|
||||
features: {
|
||||
FetchExternalResources: false, // disables resource loading over HTTP / filesystem
|
||||
ProcessExternalResources: false // do not execute JS within script blocks
|
||||
}
|
||||
}, (err, win) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
@ -224,3 +237,62 @@ function prepareHtml(html, callback) {
|
|||
return callback(null, juice(preparedHtml));
|
||||
});
|
||||
}
|
||||
|
||||
function purifyHTML(html) {
|
||||
let win = jsdom.jsdom('', {
|
||||
features: {
|
||||
FetchExternalResources: false, // disables resource loading over HTTP / filesystem
|
||||
ProcessExternalResources: false // do not execute JS within script blocks
|
||||
}
|
||||
}).defaultView;
|
||||
let DOMPurify = createDOMPurify(win);
|
||||
return DOMPurify.sanitize(html);
|
||||
}
|
||||
|
||||
// TODO Simplify!
|
||||
function mergeTemplateIntoLayout(template, layout, callback) {
|
||||
|
||||
layout = layout || '{{{body}}}';
|
||||
|
||||
let readFile = (relPath, callback) => {
|
||||
fs.readFile(path.join(__dirname, '..', 'views', relPath), 'utf-8', (err, source) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, source);
|
||||
});
|
||||
};
|
||||
|
||||
let done = (template, layout) => {
|
||||
let source = layout.replace(/\{\{\{body\}\}\}/g, template);
|
||||
return callback(null, source);
|
||||
};
|
||||
|
||||
if (layout.endsWith('.hbs')) {
|
||||
readFile(layout, (err, layout) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
// Please dont end your custom messages with .hbs ...
|
||||
if (template.endsWith('.hbs')) {
|
||||
readFile(template, (err, template) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return done(template, layout);
|
||||
});
|
||||
} else {
|
||||
return done(template, layout);
|
||||
}
|
||||
});
|
||||
} else if (template.endsWith('.hbs')) {
|
||||
readFile(template, (err, template) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return done(template, layout);
|
||||
});
|
||||
} else {
|
||||
return done(template, layout);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"schemaVersion": 21
|
||||
"schemaVersion": 22
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "mailtrain",
|
||||
"private": true,
|
||||
"version": "1.22.0",
|
||||
"version": "1.23.0",
|
||||
"description": "Self hosted email newsletter app",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -47,6 +47,7 @@
|
|||
"csurf": "^1.9.0",
|
||||
"csv-generate": "^1.0.0",
|
||||
"csv-parse": "^1.2.0",
|
||||
"dompurify": "^0.8.5",
|
||||
"escape-html": "^1.0.3",
|
||||
"express": "^4.15.2",
|
||||
"express-session": "^1.15.1",
|
||||
|
@ -69,6 +70,7 @@
|
|||
"libmime": "^3.1.0",
|
||||
"marked": "^0.3.6",
|
||||
"memory-cache": "^0.1.6",
|
||||
"mjml": "^3.3.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"moment-timezone": "^0.5.11",
|
||||
"morgan": "^1.8.1",
|
||||
|
@ -80,6 +82,7 @@
|
|||
"nodemailer": "^3.1.8",
|
||||
"nodemailer-openpgp": "^1.0.2",
|
||||
"npmlog": "^4.0.2",
|
||||
"object-hash": "^1.1.7",
|
||||
"openpgp": "^2.5.1",
|
||||
"passport": "^0.3.2",
|
||||
"passport-local": "^1.0.0",
|
||||
|
|
1
public/ace/mode-css.js
Normal file
1
public/ace/mode-css.js
Normal file
File diff suppressed because one or more lines are too long
1
public/ace/mode-plain_text.js
Normal file
1
public/ace/mode-plain_text.js
Normal file
|
@ -0,0 +1 @@
|
|||
ace.define("ace/mode/plain_text",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/text_highlight_rules","ace/mode/behaviour"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text").Mode,s=e("./text_highlight_rules").TextHighlightRules,o=e("./behaviour").Behaviour,u=function(){this.HighlightRules=s,this.$behaviour=new o};r.inherits(u,i),function(){this.type="text",this.getNextLineIndent=function(e,t,n){return""},this.$id="ace/mode/plain_text"}.call(u.prototype),t.Mode=u})
|
1
public/ace/worker-css.js
Normal file
1
public/ace/worker-css.js
Normal file
File diff suppressed because one or more lines are too long
4
public/bootstrap/themes/flatly.min.css
vendored
4
public/bootstrap/themes/flatly.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -1,6 +0,0 @@
|
|||
/* eslint-env browser */
|
||||
|
||||
'use strict';
|
||||
|
||||
document.getElementById('unsubscribe-button').click();
|
||||
document.getElementById('unsubscribe-form').submit();
|
156
public/javascript/cookie.2.1.3.js
Normal file
156
public/javascript/cookie.2.1.3.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
/*!
|
||||
* JavaScript Cookie v2.1.3
|
||||
* https://github.com/js-cookie/js-cookie
|
||||
*
|
||||
* Copyright 2006, 2015 Klaus Hartl & Fagner Brack
|
||||
* Released under the MIT license
|
||||
*/
|
||||
;(function (factory) {
|
||||
var registeredInModuleLoader = false;
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define(factory);
|
||||
registeredInModuleLoader = true;
|
||||
}
|
||||
if (typeof exports === 'object') {
|
||||
module.exports = factory();
|
||||
registeredInModuleLoader = true;
|
||||
}
|
||||
if (!registeredInModuleLoader) {
|
||||
var OldCookies = window.Cookies;
|
||||
var api = window.Cookies = factory();
|
||||
api.noConflict = function () {
|
||||
window.Cookies = OldCookies;
|
||||
return api;
|
||||
};
|
||||
}
|
||||
}(function () {
|
||||
function extend () {
|
||||
var i = 0;
|
||||
var result = {};
|
||||
for (; i < arguments.length; i++) {
|
||||
var attributes = arguments[ i ];
|
||||
for (var key in attributes) {
|
||||
result[key] = attributes[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function init (converter) {
|
||||
function api (key, value, attributes) {
|
||||
var result;
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Write
|
||||
|
||||
if (arguments.length > 1) {
|
||||
attributes = extend({
|
||||
path: '/'
|
||||
}, api.defaults, attributes);
|
||||
|
||||
if (typeof attributes.expires === 'number') {
|
||||
var expires = new Date();
|
||||
expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5);
|
||||
attributes.expires = expires;
|
||||
}
|
||||
|
||||
try {
|
||||
result = JSON.stringify(value);
|
||||
if (/^[\{\[]/.test(result)) {
|
||||
value = result;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
if (!converter.write) {
|
||||
value = encodeURIComponent(String(value))
|
||||
.replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
|
||||
} else {
|
||||
value = converter.write(value, key);
|
||||
}
|
||||
|
||||
key = encodeURIComponent(String(key));
|
||||
key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
|
||||
key = key.replace(/[\(\)]/g, escape);
|
||||
|
||||
return (document.cookie = [
|
||||
key, '=', value,
|
||||
attributes.expires ? '; expires=' + attributes.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
|
||||
attributes.path ? '; path=' + attributes.path : '',
|
||||
attributes.domain ? '; domain=' + attributes.domain : '',
|
||||
attributes.secure ? '; secure' : ''
|
||||
].join(''));
|
||||
}
|
||||
|
||||
// Read
|
||||
|
||||
if (!key) {
|
||||
result = {};
|
||||
}
|
||||
|
||||
// To prevent the for loop in the first place assign an empty array
|
||||
// in case there are no cookies at all. Also prevents odd result when
|
||||
// calling "get()"
|
||||
var cookies = document.cookie ? document.cookie.split('; ') : [];
|
||||
var rdecode = /(%[0-9A-Z]{2})+/g;
|
||||
var i = 0;
|
||||
|
||||
for (; i < cookies.length; i++) {
|
||||
var parts = cookies[i].split('=');
|
||||
var cookie = parts.slice(1).join('=');
|
||||
|
||||
if (cookie.charAt(0) === '"') {
|
||||
cookie = cookie.slice(1, -1);
|
||||
}
|
||||
|
||||
try {
|
||||
var name = parts[0].replace(rdecode, decodeURIComponent);
|
||||
cookie = converter.read ?
|
||||
converter.read(cookie, name) : converter(cookie, name) ||
|
||||
cookie.replace(rdecode, decodeURIComponent);
|
||||
|
||||
if (this.json) {
|
||||
try {
|
||||
cookie = JSON.parse(cookie);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (key === name) {
|
||||
result = cookie;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
result[name] = cookie;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
api.set = api;
|
||||
api.get = function (key) {
|
||||
return api.call(api, key);
|
||||
};
|
||||
api.getJSON = function () {
|
||||
return api.apply({
|
||||
json: true
|
||||
}, [].slice.call(arguments));
|
||||
};
|
||||
api.defaults = {};
|
||||
|
||||
api.remove = function (key, attributes) {
|
||||
api(key, '', extend(attributes, {
|
||||
expires: -1
|
||||
}));
|
||||
};
|
||||
|
||||
api.withConverter = init;
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
return init(function () {});
|
||||
}));
|
|
@ -9,6 +9,9 @@ $('.summernote').summernote({
|
|||
tabsize: 2
|
||||
});
|
||||
|
||||
// https://ace.c9.io/#nav=higlighter
|
||||
// https://github.com/ajaxorg/ace-builds/tree/v1.2.3/src-min-noconflict
|
||||
|
||||
$('div.code-editor').each(function () {
|
||||
var editor = ace.edit(this);
|
||||
var textarea = document.querySelector('input[name=html]');
|
||||
|
@ -17,8 +20,34 @@ $('div.code-editor').each(function () {
|
|||
editor.getSession().setMode('ace/mode/html');
|
||||
editor.getSession().setUseWrapMode(true);
|
||||
editor.getSession().setUseSoftTabs(true);
|
||||
editor.setShowPrintMargin(false);
|
||||
editor.getSession().on('change', function () {
|
||||
textarea.value = editor.getSession().getValue();
|
||||
});
|
||||
textarea.value = editor.getSession().getValue();
|
||||
});
|
||||
|
||||
$('div[class*="code-editor-"]').each(function () {
|
||||
var input = $(this).siblings('input')[0];
|
||||
var mode = 'html';
|
||||
var editor = ace.edit(this);
|
||||
|
||||
if ($(this).hasClass('code-editor-text')) {
|
||||
mode = 'plain_text';
|
||||
} else if ($(this).hasClass('code-editor-mjml')) {
|
||||
mode = 'html';
|
||||
editor.getSession().setUseWorker(false);
|
||||
} else if ($(this).hasClass('code-editor-css')) {
|
||||
mode = 'css';
|
||||
}
|
||||
|
||||
editor.setTheme('ace/theme/chrome');
|
||||
editor.setShowPrintMargin(false);
|
||||
editor.getSession().setMode('ace/mode/' + mode);
|
||||
editor.getSession().setUseWrapMode(true);
|
||||
editor.getSession().setUseSoftTabs(true);
|
||||
editor.getSession().setValue(input.value);
|
||||
editor.getSession().on('change', function () {
|
||||
input.value = editor.getSession().getValue();
|
||||
});
|
||||
});
|
||||
|
|
13
public/javascript/jquery-ui-1.12.1.min.js
vendored
Normal file
13
public/javascript/jquery-ui-1.12.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
215
public/subscription/form-input-style.css
Normal file
215
public/subscription/form-input-style.css
Normal file
|
@ -0,0 +1,215 @@
|
|||
/* --- Colors ----------
|
||||
|
||||
Input Border: #DCE4EC
|
||||
Input Group: #FAFAFA
|
||||
Muted: #999999
|
||||
Anchor: #1F68D5
|
||||
Alerts: ...
|
||||
|
||||
*/
|
||||
|
||||
/* --- General -------- */
|
||||
|
||||
form {
|
||||
margin: .5em 0 1em;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.2em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
margin-bottom: .3em;
|
||||
}
|
||||
|
||||
.label-checkbox,
|
||||
.label-radio {
|
||||
font-weight: normal;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.label-inline {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
margin-left: .3em;
|
||||
}
|
||||
|
||||
|
||||
/* --- Inputs ------------- */
|
||||
|
||||
input[type='email'],
|
||||
input[type='number'],
|
||||
input[type='password'],
|
||||
input[type='search'],
|
||||
input[type='tel'],
|
||||
input[type='text'],
|
||||
input[type='url'],
|
||||
textarea,
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 2px solid #DCE4EC;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
height: 2.8em;
|
||||
padding: .3em .7em;
|
||||
width: 100%;
|
||||
font-size: 1.2em;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
input[type='email']:focus,
|
||||
input[type='number']:focus,
|
||||
input[type='password']:focus,
|
||||
input[type='search']:focus,
|
||||
input[type='tel']:focus,
|
||||
input[type='text']:focus,
|
||||
input[type='url']:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border-color: #2D3E4F;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input[readonly] {
|
||||
color: #999999;
|
||||
}
|
||||
input[readonly]:focus {
|
||||
border-color: #DCE4EC;
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
margin-bottom: 0;
|
||||
margin-right: .2em;
|
||||
}
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
select {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#2D3E4F" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 8em;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* --- Input Group --------- */
|
||||
|
||||
.input-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-group-addon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background: #FAFAFA;
|
||||
height: 100%;
|
||||
padding: 0 .75em;
|
||||
border: 2px solid #DCE4EC;
|
||||
box-sizing: border-box;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.input-group-addon > * {
|
||||
line-height: 2.8em;
|
||||
}
|
||||
|
||||
|
||||
/* --- Alerts ------------- */
|
||||
|
||||
.alert {
|
||||
margin: 1.25em auto 0;
|
||||
padding: 1em;
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
font-size: 1em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.alert-dismissible .close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert-success { color: #397740; background-color: #DEF0D9; border-color: #CFEAC8; }
|
||||
.alert-info { color: #33708E; background-color: #D9EDF6; border-color: #BCDFF0; }
|
||||
.alert-warning { color: #8A6D3F; background-color: #FCF8E4; border-color: #F9F2CE; }
|
||||
.alert-danger { color: #AA4144; background-color: #F2DEDE; border-color: #EBCCCC; }
|
||||
|
||||
|
||||
/* --- GPG Key ------------- */
|
||||
|
||||
.form-group.gpg > label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-download-pubkey,
|
||||
.btn-download-pubkey:focus,
|
||||
.btn-download-pubkey:active {
|
||||
background: none;
|
||||
border: none;
|
||||
display: block;
|
||||
font: inherit;
|
||||
font-size: .8em;
|
||||
margin: .3em 0 0;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
outline-offset: 0;
|
||||
color: #1F68D5;
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
height: auto;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.btn-download-pubkey:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.gpg-text {
|
||||
font-family: monospace;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
|
||||
/* --- Other ------------- */
|
||||
|
||||
.help-block {
|
||||
display: block;
|
||||
font-size: .9em;
|
||||
line-height: 1;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
form a {
|
||||
color: #1F68D5;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
form a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
|
@ -67,7 +67,7 @@ router.get('/:campaign/:list/:subscription', passport.csrfProtection, (req, res,
|
|||
let render = (view, layout) => {
|
||||
res.render(view, {
|
||||
layout,
|
||||
message: renderTags ? tools.formatMessage(serviceUrl, campaign, list, subscription, html) : html,
|
||||
message: renderTags ? tools.formatMessage(serviceUrl, campaign, list, subscription, html, false, true) : html,
|
||||
campaign,
|
||||
list,
|
||||
subscription,
|
||||
|
@ -80,6 +80,9 @@ router.get('/:campaign/:list/:subscription', passport.csrfProtection, (req, res,
|
|||
res.render('partials/tracking-scripts', {
|
||||
layout: 'archive/layout-raw'
|
||||
}, (err, scripts) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
html = scripts ? html.replace(/<\/body\b/i, match => scripts + match) : html;
|
||||
render('archive/view-raw', 'archive/layout-raw');
|
||||
});
|
||||
|
|
296
routes/forms.js
Normal file
296
routes/forms.js
Normal file
|
@ -0,0 +1,296 @@
|
|||
'use strict';
|
||||
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let passport = require('../lib/passport');
|
||||
let tools = require('../lib/tools');
|
||||
let helpers = require('../lib/helpers');
|
||||
let _ = require('../lib/translate')._;
|
||||
let lists = require('../lib/models/lists');
|
||||
let fields = require('../lib/models/fields');
|
||||
let forms = require('../lib/models/forms');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
res.setSelectedMenu('lists');
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/:list', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
forms.list(list.id, (err, rows) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
res.render('lists/forms/forms', {
|
||||
customForms: rows.map(row => {
|
||||
row.index = ++index;
|
||||
row.isDefaultForm = list.defaultForm === row.id;
|
||||
return row;
|
||||
}),
|
||||
list,
|
||||
csrfToken: req.csrfToken()
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/create', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
let data = {};
|
||||
data.csrfToken = req.csrfToken();
|
||||
data.list = list;
|
||||
|
||||
res.render('lists/forms/create', data);
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/create', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
forms.create(req.params.list, req.body, (err, id) => {
|
||||
if (err || !id) {
|
||||
req.flash('danger', err && err.message || err || _('Could not create custom form'));
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list) + '/create?' + tools.queryParams(req.body));
|
||||
}
|
||||
req.flash('success', 'Custom form created');
|
||||
res.redirect('/forms/' + encodeURIComponent(req.params.list) + '/edit/' + id);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:list/edit/:form', passport.csrfProtection, (req, res) => {
|
||||
lists.get(req.params.list, (err, list) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!list) {
|
||||
req.flash('danger', _('Selected list ID not found'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
forms.get(req.params.form, (err, form) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
if (!form) {
|
||||
req.flash('danger', _('Selected form not found'));
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, rows) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
|
||||
let customFields = rows.map(row => {
|
||||
row.type = fields.types[row.type];
|
||||
return row;
|
||||
});
|
||||
|
||||
let allFields = helpers.filterCustomFields(customFields, [], 'exclude');
|
||||
let fieldsShownOnSubscribe = allFields;
|
||||
let fieldsHiddenOnSubscribe = [];
|
||||
let fieldsShownOnManage = allFields;
|
||||
let fieldsHiddenOnManage = [];
|
||||
|
||||
if (form.fieldsShownOnSubscribe) {
|
||||
fieldsShownOnSubscribe = helpers.filterCustomFields(customFields, form.fieldsShownOnSubscribe, 'include');
|
||||
fieldsHiddenOnSubscribe = helpers.filterCustomFields(customFields, form.fieldsShownOnSubscribe, 'exclude');
|
||||
}
|
||||
|
||||
if (form.fieldsShownOnManage) {
|
||||
fieldsShownOnManage = helpers.filterCustomFields(customFields, form.fieldsShownOnManage, 'include');
|
||||
fieldsHiddenOnManage = helpers.filterCustomFields(customFields, form.fieldsShownOnManage, 'exclude');
|
||||
}
|
||||
|
||||
let helpEmailText = _('The plaintext version for this email');
|
||||
let helpMjmlBase = _('Custom forms use MJML for formatting');
|
||||
let helpMjmlDocLink = _('See the MJML documentation <a class="mjml-documentation">here</a>');
|
||||
let helpMjmlGeneral = helpMjmlBase + ' ' + helpMjmlDocLink;
|
||||
|
||||
let templateOptgroups = [
|
||||
{
|
||||
label: _('General'),
|
||||
opts: [{
|
||||
name: 'layout',
|
||||
label: _('Layout'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral,
|
||||
isLayout: true
|
||||
}, {
|
||||
name: 'form_input_style',
|
||||
label: _('Form Input Style'),
|
||||
type: 'css',
|
||||
help: _('This CSS stylesheet defines the appearance of form input elements and alerts')
|
||||
}]
|
||||
}, {
|
||||
label: _('Subscribe'),
|
||||
opts: [{
|
||||
name: 'web_subscribe',
|
||||
label: _('Web - Subscribe'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'web_confirm_notice',
|
||||
label: _('Web - Confirm Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_confirm_html',
|
||||
label: _('Mail - Confirm Subscription (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_confirm_text',
|
||||
label: _('Mail - Confirm Subscription (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'web_subscribed',
|
||||
label: _('Web - Subscribed Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_subscription_confirmed_html',
|
||||
label: _('Mail - Subscription Confirmed (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_subscription_confirmed_text',
|
||||
label: _('Mail - Subscription Confirmed (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}]
|
||||
}, {
|
||||
label: _('Manage'),
|
||||
opts: [{
|
||||
name: 'web_manage',
|
||||
label: _('Web - Manage Preferences'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'web_manage_address',
|
||||
label: _('Web - Manage Address'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'web_updated_notice',
|
||||
label: _('Web - Updated Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}]
|
||||
}, {
|
||||
label: _('Unsubscribe'),
|
||||
opts: [{
|
||||
name: 'web_unsubscribe',
|
||||
label: _('Web - Unsubscribe'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'web_unsubscribe_notice',
|
||||
label: _('Web - Unsubscribe Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_unsubscribe_confirmed_html',
|
||||
label: _('Mail - Unsubscribe Confirmed (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_unsubscribe_confirmed_text',
|
||||
label: _('Mail - Unsubscribe Confirmed (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}]
|
||||
}
|
||||
];
|
||||
|
||||
templateOptgroups.forEach(group => {
|
||||
group.opts.forEach(opt => {
|
||||
let key = tools.fromDbKey(opt.name);
|
||||
opt.value = form[key];
|
||||
});
|
||||
});
|
||||
|
||||
subscriptions.listTestUsers(list.id, (err, testUsers) => {
|
||||
res.render('lists/forms/edit', {
|
||||
csrfToken: req.csrfToken(),
|
||||
list,
|
||||
form,
|
||||
templateOptgroups,
|
||||
fieldsShownOnSubscribe,
|
||||
fieldsHiddenOnSubscribe,
|
||||
fieldsShownOnManage,
|
||||
fieldsHiddenOnManage,
|
||||
testUsers,
|
||||
useEditor: true
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
forms.update(req.body.id, req.body, (err, updated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
} else if (updated) {
|
||||
req.flash('success', _('Form settings updated'));
|
||||
} else {
|
||||
req.flash('info', _('Form settings not updated'));
|
||||
}
|
||||
|
||||
if (req.body.id) {
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list) + '/edit/' + encodeURIComponent(req.body.id));
|
||||
} else {
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/:list/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
forms.delete(req.body.id, (err, deleted) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (deleted) {
|
||||
req.flash('success', _('Custom form deleted'));
|
||||
} else {
|
||||
req.flash('info', _('Could not delete specified form'));
|
||||
}
|
||||
|
||||
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -8,6 +8,7 @@ let router = new express.Router();
|
|||
let lists = require('../lib/models/lists');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
let fields = require('../lib/models/fields');
|
||||
let forms = require('../lib/models/forms');
|
||||
let tools = require('../lib/tools');
|
||||
let striptags = require('striptags');
|
||||
let htmlescape = require('escape-html');
|
||||
|
@ -101,8 +102,21 @@ router.get('/edit/:id', passport.csrfProtection, (req, res) => {
|
|||
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
list.csrfToken = req.csrfToken();
|
||||
res.render('lists/edit', list);
|
||||
|
||||
forms.list(list.id, (err, customForms) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/lists');
|
||||
}
|
||||
|
||||
list.customForms = customForms.map(row => {
|
||||
row.selected = list.defaultForm === row.id;
|
||||
return row;
|
||||
});
|
||||
|
||||
list.csrfToken = req.csrfToken();
|
||||
res.render('lists/edit', list);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -117,7 +131,9 @@ router.post('/edit', passport.parseForm, passport.csrfProtection, (req, res) =>
|
|||
req.flash('info', _('List settings not updated'));
|
||||
}
|
||||
|
||||
if (req.body.id) {
|
||||
if (req.query.next) {
|
||||
return res.redirect(req.query.next);
|
||||
} else if (req.body.id) {
|
||||
return res.redirect('/lists/edit/' + encodeURIComponent(req.body.id));
|
||||
} else {
|
||||
return res.redirect('/lists');
|
||||
|
@ -219,7 +235,7 @@ router.post('/ajax/:id', (req, res) => {
|
|||
} else {
|
||||
return htmlescape(cRow.value || '');
|
||||
}
|
||||
})).concat(statuses[row.status]).concat(row.created && row.created.toISOString ? '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>' : 'N/A').concat('<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/subscription/' + list.id + '/edit/' + row.cid + '">' + _('Edit') + '</a>'))
|
||||
})).concat(statuses[row.status]).concat(row.created && row.created.toISOString ? '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>' : 'N/A').concat('<a href="/lists/subscription/' + list.id + '/edit/' + row.cid + '">' + _('Edit') + '</a>'))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
let log = require('npmlog');
|
||||
|
||||
let tools = require('../lib/tools');
|
||||
let helpers = require('../lib/helpers');
|
||||
let mailer = require('../lib/mailer');
|
||||
let passport = require('../lib/passport');
|
||||
let express = require('express');
|
||||
|
@ -10,11 +11,13 @@ let urllib = require('url');
|
|||
let router = new express.Router();
|
||||
let lists = require('../lib/models/lists');
|
||||
let fields = require('../lib/models/fields');
|
||||
let forms = require('../lib/models/forms');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
let settings = require('../lib/models/settings');
|
||||
let openpgp = require('openpgp');
|
||||
let _ = require('../lib/translate')._;
|
||||
let util = require('util');
|
||||
let hbs = require('hbs');
|
||||
|
||||
router.get('/subscribe/:cid', (req, res, next) => {
|
||||
subscriptions.subscribe(req.params.cid, req.ip, (err, subscription) => {
|
||||
|
@ -37,17 +40,44 @@ router.get('/subscribe/:cid', (req, res, next) => {
|
|||
return next(err);
|
||||
}
|
||||
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'pgpPrivateKey', 'defaultAddress', 'defaultFrom', 'disableConfirmations'], (err, configItems) => {
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'pgpPrivateKey', 'defaultAddress', 'defaultPostaddress', 'defaultFrom', 'disableConfirmations'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.render('subscription/subscribed', {
|
||||
let data = {
|
||||
title: list.name,
|
||||
layout: 'subscription/layout',
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
preferences: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
||||
hasPubkey: !!configItems.pgpPrivateKey
|
||||
hasPubkey: !!configItems.pgpPrivateKey,
|
||||
defaultAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
template: {
|
||||
template: 'subscription/web-subscribed.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribed', data, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (configItems.disableConfirmations) {
|
||||
|
@ -66,30 +96,52 @@ router.get('/subscribe/:cid', (req, res, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: [].concat(subscription.firstName || []).concat(subscription.lastName || []).join(' '),
|
||||
address: subscription.email
|
||||
},
|
||||
subject: util.format(_('%s: Subscription Confirmed'), list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html: 'emails/subscription-confirmed-html.hbs',
|
||||
text: 'emails/subscription-confirmed-text.hbs',
|
||||
data: {
|
||||
title: list.name,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
preferencesUrl: urllib.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/manage/' + subscription.cid),
|
||||
unsubscribeUrl: urllib.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid)
|
||||
}
|
||||
}, err => {
|
||||
let sendMail = (html, text) => {
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: [].concat(subscription.firstName || []).concat(subscription.lastName || []).join(' '),
|
||||
address: subscription.email
|
||||
},
|
||||
subject: util.format(_('%s: Subscription Confirmed'), list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
data: {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
preferencesUrl: urllib.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/manage/' + subscription.cid),
|
||||
unsubscribeUrl: urllib.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid),
|
||||
}
|
||||
}, err => {
|
||||
if (err) {
|
||||
log.error('Subscription', err.stack);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let text = {
|
||||
template: 'subscription/mail-subscription-confirmed-text.hbs'
|
||||
};
|
||||
|
||||
let html = {
|
||||
template: 'subscription/mail-subscription-confirmed-html.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs',
|
||||
type: 'mjml'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormTemplates(req.query.fid || list.defaultForm, { text, html }, (err, tmpl) => {
|
||||
if (err) {
|
||||
log.error('Subscription', err.stack);
|
||||
return sendMail(html, text);
|
||||
}
|
||||
|
||||
sendMail(tmpl.html, tmpl.text);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -124,12 +176,41 @@ router.get('/:cid', passport.csrfProtection, (req, res, next) => {
|
|||
data.customFields = fields.getRow(fieldList, data);
|
||||
data.useEditor = true;
|
||||
|
||||
settings.list(['pgpPrivateKey'], (err, configItems) => {
|
||||
settings.list(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
data.hasPubkey = !!configItems.pgpPrivateKey;
|
||||
res.render('subscription/subscribe', data);
|
||||
data.defaultAddress = configItems.defaultAddress;
|
||||
data.defaultPostaddress = configItems.defaultPostaddress;
|
||||
|
||||
data.template = {
|
||||
template: 'subscription/web-subscribe.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribe', data, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.needsJsWarning = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -146,15 +227,43 @@ router.get('/:cid/confirm-notice', (req, res, next) => {
|
|||
return next(err);
|
||||
}
|
||||
|
||||
settings.list(['defaultHomepage', 'serviceUrl'], (err, configItems) => {
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.render('subscription/confirm-notice', {
|
||||
let data = {
|
||||
title: list.name,
|
||||
layout: 'subscription/layout',
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
defaultAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
template: {
|
||||
template: 'subscription/web-confirm-notice.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-confirm-notice', data, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.isConfirmNotice = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -171,15 +280,42 @@ router.get('/:cid/updated-notice', (req, res, next) => {
|
|||
return next(err);
|
||||
}
|
||||
|
||||
settings.list(['defaultHomepage', 'serviceUrl'], (err, configItems) => {
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.render('subscription/updated-notice', {
|
||||
let data = {
|
||||
title: list.name,
|
||||
layout: 'subscription/layout',
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
defaultAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
template: {
|
||||
template: 'subscription/web-updated-notice.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-updated-notice', data, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -196,15 +332,43 @@ router.get('/:cid/unsubscribe-notice', (req, res, next) => {
|
|||
return next(err);
|
||||
}
|
||||
|
||||
settings.list(['defaultHomepage', 'serviceUrl'], (err, configItems) => {
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.render('subscription/unsubscribe-notice', {
|
||||
let data = {
|
||||
title: list.name,
|
||||
layout: 'subscription/layout',
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
defaultAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
template: {
|
||||
template: 'subscription/web-unsubscribe-notice.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-unsubscribe-notice', data, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -298,13 +462,42 @@ router.get('/:lcid/manage/:ucid', passport.csrfProtection, (req, res, next) => {
|
|||
|
||||
subscription.useEditor = true;
|
||||
|
||||
settings.list(['pgpPrivateKey'], (err, configItems) => {
|
||||
settings.list(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
subscription.hasPubkey = !!configItems.pgpPrivateKey;
|
||||
subscription.defaultAddress = configItems.defaultAddress;
|
||||
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
||||
|
||||
res.render('subscription/manage', subscription);
|
||||
subscription.template = {
|
||||
template: 'subscription/web-manage.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-manage', subscription, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.needsJsWarning = true;
|
||||
data.isManagePreferences = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -344,24 +537,55 @@ router.get('/:lcid/manage-address/:ucid', passport.csrfProtection, (req, res, ne
|
|||
return next(err);
|
||||
}
|
||||
|
||||
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
|
||||
if (!err && !subscription) {
|
||||
err = new Error(_('Subscription not found from this list'));
|
||||
err.status = 404;
|
||||
settings.list(['defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
subscription.lcid = req.params.lcid;
|
||||
subscription.title = list.name;
|
||||
subscription.csrfToken = req.csrfToken();
|
||||
subscription.layout = 'subscription/layout';
|
||||
subscription.useEditor = true;
|
||||
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
|
||||
if (!err && !subscription) {
|
||||
err = new Error(_('Subscription not found from this list'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
res.render('subscription/manage-address', subscription);
|
||||
subscription.lcid = req.params.lcid;
|
||||
subscription.title = list.name;
|
||||
subscription.csrfToken = req.csrfToken();
|
||||
subscription.defaultAddress = configItems.defaultAddress;
|
||||
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
||||
|
||||
subscription.template = {
|
||||
template: 'subscription/web-manage-address.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-manage-address', subscription, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.needsJsWarning = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.post('/:lcid/manage-address', passport.parseForm, passport.csrfProtection, (req, res, next) => {
|
||||
lists.getByCid(req.params.lcid, (err, list) => {
|
||||
if (!err && !list) {
|
||||
|
@ -397,23 +621,56 @@ router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next)
|
|||
return next(err);
|
||||
}
|
||||
|
||||
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
|
||||
if (!err && !subscription) {
|
||||
err = new Error(_('Subscription not found from this list'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
settings.list(['defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
subscription.lcid = req.params.lcid;
|
||||
subscription.title = list.name;
|
||||
subscription.csrfToken = req.csrfToken();
|
||||
subscription.layout = 'subscription/layout';
|
||||
subscription.autosubmit = !!req.query.auto;
|
||||
subscription.campaign = req.query.c;
|
||||
res.render('subscription/unsubscribe', subscription);
|
||||
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
|
||||
if (!err && !subscription) {
|
||||
err = new Error(_('Subscription not found from this list'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
subscription.lcid = req.params.lcid;
|
||||
subscription.title = list.name;
|
||||
subscription.csrfToken = req.csrfToken();
|
||||
subscription.autosubmit = !!req.query.auto;
|
||||
subscription.campaign = req.query.c;
|
||||
subscription.defaultAddress = configItems.defaultAddress;
|
||||
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
||||
|
||||
subscription.template = {
|
||||
template: 'subscription/web-unsubscribe.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-unsubscribe', subscription, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -451,7 +708,7 @@ router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (
|
|||
}
|
||||
});
|
||||
|
||||
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'serviceUrl', 'disableConfirmations'], (err, configItems) => {
|
||||
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl', 'disableConfirmations'], (err, configItems) => {
|
||||
if (err) {
|
||||
return log.error('Settings', err);
|
||||
}
|
||||
|
@ -460,29 +717,50 @@ router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (
|
|||
return;
|
||||
}
|
||||
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: [].concat(subscription.firstName || []).concat(subscription.lastName || []).join(' '),
|
||||
address: subscription.email
|
||||
},
|
||||
subject: util.format(_('%s: Subscription Confirmed'), list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html: 'emails/unsubscribe-confirmed-html.hbs',
|
||||
text: 'emails/unsubscribe-confirmed-text.hbs',
|
||||
data: {
|
||||
title: list.name,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
subscribeUrl: urllib.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '?cid=' + subscription.cid)
|
||||
}
|
||||
}, err => {
|
||||
let sendMail = (html, text) => {
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: [].concat(subscription.firstName || []).concat(subscription.lastName || []).join(' '),
|
||||
address: subscription.email
|
||||
},
|
||||
subject: util.format(_('%s: Unsubscribe Confirmed'), list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
data: {
|
||||
title: list.name,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
subscribeUrl: urllib.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '?cid=' + subscription.cid),
|
||||
}
|
||||
}, err => {
|
||||
if (err) {
|
||||
log.error('Subscription', err.stack);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let text = {
|
||||
template: 'subscription/mail-unsubscribe-confirmed-text.hbs'
|
||||
};
|
||||
|
||||
let html = {
|
||||
template: 'subscription/mail-unsubscribe-confirmed-html.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs',
|
||||
type: 'mjml'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormTemplates(req.query.fid || list.defaultForm, { text, html }, (err, tmpl) => {
|
||||
if (err) {
|
||||
log.error('Subscription', err.stack);
|
||||
return sendMail(html, text);
|
||||
}
|
||||
|
||||
sendMail(tmpl.html, tmpl.text);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -371,7 +371,7 @@ function formatMessage(message, callback) {
|
|||
|
||||
let campaignAddress = [campaign.cid, list.cid, message.subscription.cid].join('.');
|
||||
|
||||
let renderedHtml = renderTags ? tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, html) : html;
|
||||
let renderedHtml = renderTags ? tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, html, false, true) : html;
|
||||
|
||||
let renderedText = (text || '').trim() ? (renderTags ? tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, text) : text) : htmlToText.fromString(renderedHtml, {
|
||||
wordwrap: 130
|
||||
|
|
|
@ -113,6 +113,29 @@ CREATE TABLE `custom_fields` (
|
|||
KEY `list_2` (`list`),
|
||||
CONSTRAINT `custom_fields_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE `custom_forms` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`list` int(11) unsigned NOT NULL,
|
||||
`name` varchar(255) DEFAULT '',
|
||||
`description` text,
|
||||
`fields_shown_on_subscribe` varchar(255) DEFAULT '',
|
||||
`fields_shown_on_manage` varchar(255) DEFAULT '',
|
||||
`layout` longtext,
|
||||
`form_input_style` longtext,
|
||||
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `list` (`list`),
|
||||
CONSTRAINT `custom_forms_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE `custom_forms_data` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`form` int(11) unsigned NOT NULL,
|
||||
`data_key` varchar(255) DEFAULT '',
|
||||
`data_value` longtext,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `form` (`form`),
|
||||
CONSTRAINT `custom_forms_data_ibfk_1` FOREIGN KEY (`form`) REFERENCES `custom_forms` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE `import_failed` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`import` int(11) unsigned NOT NULL,
|
||||
|
@ -157,6 +180,7 @@ CREATE TABLE `links` (
|
|||
CREATE TABLE `lists` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||
`default_form` int(11) unsigned DEFAULT NULL,
|
||||
`name` varchar(255) NOT NULL DEFAULT '',
|
||||
`description` text,
|
||||
`subscribers` int(11) unsigned DEFAULT '0',
|
||||
|
@ -212,7 +236,7 @@ CREATE TABLE `settings` (
|
|||
`value` text NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `key` (`key`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4;
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','smtp-pulse.com');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','465');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','TLS');
|
||||
|
@ -229,7 +253,7 @@ INSERT INTO `settings` (`id`, `key`, `value`) VALUES (13,'default_from','My Awes
|
|||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','http://localhost:3000/');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','21');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','22');
|
||||
CREATE TABLE `subscription` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||
|
|
38
setup/sql/upgrade-00022.sql
Normal file
38
setup/sql/upgrade-00022.sql
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Header section
|
||||
# Define incrementing schema version number
|
||||
SET @schema_version = '22';
|
||||
|
||||
# Create table to store custom forms
|
||||
CREATE TABLE `custom_forms` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`list` int(11) unsigned NOT NULL,
|
||||
`name` varchar(255) DEFAULT '',
|
||||
`description` text,
|
||||
`fields_shown_on_subscribe` varchar(255) DEFAULT '',
|
||||
`fields_shown_on_manage` varchar(255) DEFAULT '',
|
||||
`layout` longtext,
|
||||
`form_input_style` longtext,
|
||||
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `list` (`list`),
|
||||
CONSTRAINT `custom_forms_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
# Create table to store custom form data
|
||||
CREATE TABLE `custom_forms_data` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`form` int(11) unsigned NOT NULL,
|
||||
`data_key` varchar(255) DEFAULT '',
|
||||
`data_value` longtext,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `form` (`form`),
|
||||
CONSTRAINT `custom_forms_data_ibfk_1` FOREIGN KEY (`form`) REFERENCES `custom_forms` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
# Add default_form to lists
|
||||
ALTER TABLE `lists` ADD COLUMN `default_form` int(11) unsigned DEFAULT NULL AFTER `cid`;
|
||||
|
||||
# Footer section
|
||||
LOCK TABLES `settings` WRITE;
|
||||
INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
|
||||
UNLOCK TABLES;
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -17,12 +17,14 @@
|
|||
<form class="form-horizontal" method="post" action="/lists/edit">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="id" value="{{id}}" />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name" class="col-sm-2 control-label">{{#translate}}Name{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control input-lg" name="name" id="name" value="{{name}}" placeholder="{{#translate}}List Name{{/translate}}" autofocus required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name" class="col-sm-2 control-label">{{#translate}}List ID{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -30,6 +32,7 @@
|
|||
<span class="help-block">{{#translate}}This is the list ID displayed to the subscribers{{/translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description" class="col-sm-2 control-label">{{#translate}}Description{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -38,6 +41,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="default_form" class="col-sm-2 control-label">{{#translate}}Custom Form{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="default_form" name="default_form">
|
||||
<option value="0">{{#translate}}Default Mailtrain Form{{/translate}}</option>
|
||||
{{#each customForms}}
|
||||
<option value="{{id}}" {{#if selected}}selected{{/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<span class="help-block">{{#translate}}The custom form used for this list. You can create a form <a href="/forms/{{id}}/create">here</a>.{{/translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
|
|
27
views/lists/forms/create.hbs
Normal file
27
views/lists/forms/create.hbs
Normal file
|
@ -0,0 +1,27 @@
|
|||
<ol class="breadcrumb">
|
||||
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
|
||||
<li><a href="/lists/">{{#translate}}Lists{{/translate}}</a></li>
|
||||
<li><a href="/lists/view/{{list.id}}">{{list.name}}</a></li>
|
||||
<li><a href="/forms/{{list.id}}">{{#translate}}Custom Forms{{/translate}}</a></li>
|
||||
<li class="active">{{#translate}}Create Form{{/translate}}</li>
|
||||
</ol>
|
||||
|
||||
<h2>{{list.name}} <small>{{#translate}}Create Custom Form{{/translate}}</small></h2>
|
||||
|
||||
<hr>
|
||||
|
||||
<form class="form-horizontal" method="post" action="/forms/{{list.id}}/create">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<div class="form-group">
|
||||
<label for="name" class="col-sm-2 control-label">{{#translate}}Form Name{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control input-lg" name="name" id="name" value="{{name}}" placeholder="{{#translate}}Form Name{{/translate}}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-plus"></i> {{#translate}}Add Form{{/translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
283
views/lists/forms/edit.hbs
Normal file
283
views/lists/forms/edit.hbs
Normal file
|
@ -0,0 +1,283 @@
|
|||
<ol class="breadcrumb">
|
||||
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
|
||||
<li><a href="/lists/">{{#translate}}Lists{{/translate}}</a></li>
|
||||
<li><a href="/lists/view/{{list.id}}">{{list.name}}</a></li>
|
||||
<li><a href="/forms/{{list.id}}">{{#translate}}Custom Forms{{/translate}}</a></li>
|
||||
<li class="active">{{#translate}}Edit Form{{/translate}}</li>
|
||||
</ol>
|
||||
|
||||
<h2>{{list.name}} <small>{{#translate}}Edit Custom Form{{/translate}}</small> <a class="btn btn-default btn-xs" href="/forms/{{list.id}}" role="button"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> {{#translate}}Back to forms{{/translate}}</a></h2>
|
||||
|
||||
<hr>
|
||||
|
||||
<form method="post" class="delete-form" id="forms-delete" action="/forms/{{list.id}}/delete">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="id" value="{{form.id}}">
|
||||
</form>
|
||||
|
||||
<form class="form-horizontal" method="post" id="forms-update" action="/forms/{{list.id}}/edit">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="id" value="{{form.id}}">
|
||||
<input type="hidden" name="fields_shown_on_subscribe" value="{{form.fieldsShownOnSubscribe}}">
|
||||
<input type="hidden" name="fields_shown_on_manage" value="{{form.fieldsShownOnManage}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name" class="col-sm-2 control-label">{{#translate}}Form Name{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control input-lg" name="name" id="name" value="{{form.name}}" placeholder="{{#translate}}Form Name{{/translate}}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-description" class="col-sm-2 control-label">{{#translate}}Description{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="form-description" name="description" rows="3" placeholder="{{#translate}}Optional comments about this form{{/translate}}">{{form.description}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="previewLinks" class="col-sm-2 control-label">{{#translate}}Form Preview{{/translate}}</label>
|
||||
<div class="col-sm-10" id="previewLinks">
|
||||
<div class="help-block">
|
||||
<small>
|
||||
{{#translate}}Note: These links are solely for a quick preview. If you submit a preview form you'll get redirected to the list's default form.{{/translate}}
|
||||
</small>
|
||||
</div>
|
||||
<p>
|
||||
<a href="/subscription/{{list.cid}}?fid={{form.id}}" target="_blank">{{#translate}}Subscribe{{/translate}}</a>
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/confirm-notice?fid={{form.id}}" target="_blank">{{#translate}}Confirm Notice{{/translate}}</a>
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/updated-notice?fid={{form.id}}" target="_blank">{{#translate}}Updated Notice{{/translate}}</a>
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/unsubscribe-notice?fid={{form.id}}" target="_blank">{{#translate}}Unsubscribed Notice{{/translate}}</a>
|
||||
{{#if testUsers}}
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/manage/{{testUsers.0.cid}}?fid={{form.id}}" target="_blank">{{#translate}}Manage{{/translate}}</a>
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/manage-address/{{testUsers.0.cid}}?fid={{form.id}}" target="_blank">{{#translate}}Manage Address{{/translate}}</a>
|
||||
{{else}}
|
||||
|
|
||||
<small class="text-muted">{{#translate}}Create a test user for additional options{{/translate}}</small>
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<p><br></p>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#templates" aria-controls="templates" role="tab" data-toggle="tab">{{#translate}}Templates{{/translate}}</a></li>
|
||||
<li role="presentation"><a href="#fields" aria-controls="fields" role="tab" data-toggle="tab">{{#translate}}Fields{{/translate}}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
<div role="tabpanel" class="tab-pane active" id="templates">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="form-header" class="col-sm-2 control-label">{{#translate}}Edit{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="templateSelect">
|
||||
{{#each templateOptgroups}}
|
||||
<optgroup label="{{label}}">
|
||||
{{#each opts}}
|
||||
<option value="{{name}}">{{label}}</option>
|
||||
{{/each}}
|
||||
</optgroup>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var forceAceRender = function(name) {
|
||||
var div = $('.form-group.template.' + name).find('.ace_editor')[0];
|
||||
div.env && div.env.editor && div.env.editor.resize(true);
|
||||
};
|
||||
$('#templateSelect').on('change', function() {
|
||||
$('.form-group.template').hide();
|
||||
var name = $(this).val();
|
||||
$('.form-group.template.' + name).show();
|
||||
forceAceRender(name);
|
||||
});
|
||||
$('ul.nav-tabs > li > a').on('shown.bs.tab', function(e) {
|
||||
var id = $(e.target).attr('href').substr(1);
|
||||
id === 'templates' && forceAceRender($('#templateSelect').val());
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{{#each templateOptgroups}}
|
||||
{{#each opts}}
|
||||
<div class="form-group template {{name}}" {{#unless isLayout}}style="display: none;"{{/unless}}>
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="help-block" style="margin-top: -8px;">
|
||||
<small>{{#if help}}{{{help}}}{{else}} {{/if}}</small>
|
||||
</div>
|
||||
<div class="code-editor-{{type}}" style="height: 700px; border: 1px solid #ccc;"></div>
|
||||
<input type="hidden" name="{{name}}" value="{{value}}">
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Fields -->
|
||||
|
||||
<style>
|
||||
ul.fields {
|
||||
min-height: 54px;
|
||||
border: 1px dashed #ccc;
|
||||
margin: 0;
|
||||
padding: 11px 10px 10px;
|
||||
background: #efefef;
|
||||
}
|
||||
ul.fields li {
|
||||
list-style: none;
|
||||
margin: -1px 0 0;
|
||||
padding: 0 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
line-height: 30px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
cursor: move;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
jQuery.get('/javascript/jquery-ui-1.12.1.min.js', undefined, function() {
|
||||
|
||||
$('.fieldsHiddenOnSubscribe, .fieldsShownOnSubscribe').sortable({
|
||||
connectWith: '.connectedSortableSubscribe'
|
||||
}).disableSelection();
|
||||
|
||||
$('.fieldsHiddenOnManage, .fieldsShownOnManage').sortable({
|
||||
connectWith: '.connectedSortableManage'
|
||||
}).disableSelection();
|
||||
|
||||
$('#forms-update').on('submit', function(e) {
|
||||
var s = [];
|
||||
var m = [];
|
||||
$('.fieldsShownOnSubscribe > li').each(function() {
|
||||
s.push($(this).data('field-id'));
|
||||
});
|
||||
$('.fieldsShownOnManage > li').each(function() {
|
||||
m.push($(this).data('field-id'));
|
||||
});
|
||||
$('input[name=fields_shown_on_subscribe]').val(s.join(','));
|
||||
$('input[name=fields_shown_on_manage]').val(m.join(','));
|
||||
});
|
||||
|
||||
}, 'script');
|
||||
});
|
||||
</script>
|
||||
|
||||
<div role="tabpanel" class="tab-pane" id="fields">
|
||||
<div class="form-group">
|
||||
<label for="form-fields" class="col-sm-2 control-label">{{#translate}}Form Fields{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-6">
|
||||
<h6>{{#translate}}Fields hidden on subscription page:{{/translate}}</h6>
|
||||
<ul class="fields fieldsHiddenOnSubscribe connectedSortableSubscribe">
|
||||
{{#each fieldsHiddenOnSubscribe}}
|
||||
<li class="ui-state-default" data-field-id="{{id}}">
|
||||
{{name}}
|
||||
<span style="font-size: 10px; color: #aaa; float: right;">{{type}}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<h6>{{#translate}}Fields shown on subscription page:{{/translate}}</h6>
|
||||
<ul class="fields fieldsShownOnSubscribe connectedSortableSubscribe">
|
||||
{{#each fieldsShownOnSubscribe}}
|
||||
<li class="ui-state-default" data-field-id="{{id}}">
|
||||
{{name}}
|
||||
<span style="font-size: 10px; color: #aaa; float: right;">{{type}}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<h6>{{#translate}}Fields hidden on preferences page:{{/translate}}</h6>
|
||||
<ul class="fields fieldsHiddenOnManage connectedSortableManage">
|
||||
{{#each fieldsHiddenOnManage}}
|
||||
<li class="ui-state-default" data-field-id="{{id}}">
|
||||
{{name}}
|
||||
<span style="font-size: 10px; color: #aaa; float: right;">{{type}}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<h6>{{#translate}}Fields shown on preferences page:{{/translate}}</h6>
|
||||
<ul class="fields fieldsShownOnManage connectedSortableManage">
|
||||
{{#each fieldsShownOnManage}}
|
||||
<li class="ui-state-default" data-field-id="{{id}}">
|
||||
{{name}}
|
||||
<span style="font-size: 10px; color: #aaa; float: right;">{{type}}</span>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end .tab-content -->
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="pull-right">
|
||||
<button type="submit" form="forms-delete" class="btn btn-danger"><i class="glyphicon glyphicon-remove"></i> {{#translate}}Delete Form{{/translate}}</button>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-ok"></i> {{#translate}}Update{{/translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<script src="/javascript/cookie.2.1.3.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Remember Tab
|
||||
var tab = Cookies.get('tab');
|
||||
tab && $('ul.nav-tabs > li > a[href="#' + tab + '"]').click();
|
||||
$('ul.nav-tabs > li > a').on('shown.bs.tab', function(e) {
|
||||
var id = $(e.target).attr('href').substr(1);
|
||||
Cookies.set('tab', id, { expires: 7, path: '' });
|
||||
});
|
||||
|
||||
// Remember Template
|
||||
var tmpl = Cookies.get('tmpl');
|
||||
tmpl && $('#templateSelect').val(tmpl).trigger('change');
|
||||
$('#templateSelect').on('change', function() {
|
||||
Cookies.set('tmpl', $(this).val(), { expires: 7, path: '' });
|
||||
});
|
||||
|
||||
$('a.mjml-documentation')
|
||||
.attr('href', 'https://mjml.io/documentation/')
|
||||
.attr('target', '_blank')
|
||||
.attr('rel', 'noreferrer')
|
||||
|
||||
});
|
||||
</script>
|
87
views/lists/forms/forms.hbs
Normal file
87
views/lists/forms/forms.hbs
Normal file
|
@ -0,0 +1,87 @@
|
|||
<ol class="breadcrumb">
|
||||
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
|
||||
<li><a href="/lists/">{{#translate}}Lists{{/translate}}</a></li>
|
||||
<li><a href="/lists/view/{{list.id}}">{{list.name}}</a></li>
|
||||
<li class="active">{{#translate}}Custom Forms{{/translate}}</li>
|
||||
</ol>
|
||||
|
||||
<div class="pull-right">
|
||||
<a class="btn btn-primary" href="/forms/{{list.id}}/create" role="button"><i class="glyphicon glyphicon-plus"></i> {{#translate}}Create Custom Form{{/translate}}</a>
|
||||
</div>
|
||||
|
||||
<h2>{{list.name}} <small>{{#translate}}Custom Forms{{/translate}}</small></h2>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<th style="width: auto;" class="text-right">
|
||||
#
|
||||
</th>
|
||||
<th style="width: 30%;">
|
||||
{{#translate}}Name{{/translate}}
|
||||
</th>
|
||||
<th style="width: 60%;">
|
||||
{{#translate}}Description{{/translate}}
|
||||
</th>
|
||||
<th>
|
||||
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each customForms}}
|
||||
<tr>
|
||||
<th scope="row" class="text-right">
|
||||
{{index}}
|
||||
</th>
|
||||
<td>
|
||||
{{#if isDefaultForm}}
|
||||
<span class="glyphicon glyphicon-star" style="color: #DE4320; font-size: .8em; font-weight: bold; padding-right: 2px;"></span>
|
||||
{{else}}
|
||||
<span class="glyphicon glyphicon-star-empty" style="color: #ccc; font-size: .8em; padding-right: 2px;"></span>
|
||||
{{/if}}
|
||||
{{name}}
|
||||
</td>
|
||||
<td class="text-muted">
|
||||
{{description}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="/forms/{{../list.id}}/edit/{{id}}">
|
||||
{{#translate}}Edit{{/translate}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{#unless customForms}}
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
{{#translate}}No data available in table{{/translate}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/unless}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<form class="form-inline" method="post" action="/lists/edit?next=%2Fforms%2F{{list.id}}">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="id" value="{{list.id}}" />
|
||||
<input type="hidden" name="name" value="{{list.name}}" />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="default_form" class="control-label" style="color: #666; font-weight: normal;">{{#translate}}The default form for this list is:{{/translate}}</label>
|
||||
|
||||
<select class="form-control input-sm" id="default_form" name="default_form">
|
||||
<option value="0">{{#translate}}Default Mailtrain Form{{/translate}}</option>
|
||||
{{#each customForms}}
|
||||
<option value="{{id}}" {{#if isDefaultForm}}selected{{/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-default btn-sm">{{#translate}}Update{{/translate}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<p><br></p>
|
|
@ -14,34 +14,33 @@
|
|||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover data-table display nowrap" width="100%" data-row-sort="0,1,1,1,0,0">
|
||||
<thead>
|
||||
<th class="col-md-1">
|
||||
<th>
|
||||
#
|
||||
</th>
|
||||
<th>
|
||||
{{#translate}}Name{{/translate}}
|
||||
</th>
|
||||
<th class="col-md-2">
|
||||
<th>
|
||||
{{#translate}}ID{{/translate}}
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
<th>
|
||||
{{#translate}}Subscribers{{/translate}}
|
||||
</th>
|
||||
<th>
|
||||
{{#translate}}Description{{/translate}}
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
<th>
|
||||
|
||||
</th>
|
||||
</thead>
|
||||
{{#if rows}}
|
||||
<tbody>
|
||||
|
||||
{{#each rows}}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{{index}}
|
||||
</th>
|
||||
<td>
|
||||
<td style="width: 30%;">
|
||||
<span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span>
|
||||
<a href="/lists/view/{{id}}">
|
||||
{{name}}
|
||||
|
@ -53,11 +52,10 @@
|
|||
<td class="text-center">
|
||||
{{subscribers}}
|
||||
</td>
|
||||
<td class="text-muted">
|
||||
<td class="text-muted" style="width: 70%;">
|
||||
{{description}}
|
||||
</td>
|
||||
<td>
|
||||
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
|
||||
<a href="/lists/edit/{{id}}">
|
||||
{{#translate}}Edit{{/translate}}
|
||||
</a>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
{{#translate}}List Actions{{/translate}} <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/forms/{{id}}" role="button"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> {{#translate}}Custom Forms{{/translate}}</a></li>
|
||||
<li><a href="/fields/{{id}}" role="button"><span class="glyphicon glyphicon-tasks" aria-hidden="true"></span> {{#translate}}Custom Fields{{/translate}}</a></li>
|
||||
<li><a href="/segments/{{id}}" role="button"><span class="glyphicon glyphicon-filter" aria-hidden="true"></span> {{#translate}}Segments{{/translate}}</a></li>
|
||||
<li><a href="/lists/edit/{{id}}" role="button"><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> {{#translate}}Edit List{{/translate}}</a></li>
|
||||
|
@ -41,7 +42,8 @@
|
|||
<form class="form-inline" method="get">
|
||||
<div class="form-group">
|
||||
<label for="exampleInputName2">{{#translate}}Segment{{/translate}}</label>
|
||||
<select name="segment" class="form-control">
|
||||
|
||||
<select name="segment" class="form-control input-sm">
|
||||
<option value="0">All Subscriptions</option>
|
||||
{{#if segments}}
|
||||
<optgroup label="{{#translate}}Segments{{/translate}}">
|
||||
|
@ -55,7 +57,7 @@
|
|||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-filter" aria-hidden="true"></span> {{#translate}}Filter{{/translate}}</button>
|
||||
<button type="submit" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-filter" aria-hidden="true"></span> {{#translate}}Filter{{/translate}}</button>
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
@ -75,7 +77,7 @@
|
|||
<table data-topic-url="/lists" data-topic-id="{{id}}" data-sort-column="1" data-sort-order="asc" {{#if useSegment}} data-topic-args="segment={{useSegment}}" {{/if}} class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1{{customSort}},1,1,0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-1">
|
||||
<th>
|
||||
#
|
||||
</th>
|
||||
<th>
|
||||
|
|
1
views/subscription/capture-flash-messages.hbs
Normal file
1
views/subscription/capture-flash-messages.hbs
Normal file
|
@ -0,0 +1 @@
|
|||
{{flash_messages}}
|
|
@ -1,17 +0,0 @@
|
|||
<div class="alert alert-warning alert-dismissible" role="alert" id="js-warning">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<strong>{{#translate}}Warning!{{/translate}}</strong> {{#translate}}If JavaScript was not enabled then no confirmation message was sent{{/translate}}
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('js-warning').style.display = 'none';
|
||||
</script>
|
||||
|
||||
<h2>{{#translate}}Almost finished.{{/translate}}</h2>
|
||||
|
||||
<p>{{#translate}}We need to confirm your email address. To complete the subscription process, please click the link in the email we just sent you.{{/translate}}</p>
|
||||
|
||||
<p>
|
||||
<a class="btn btn-primary" href="{{homepage}}" role="button">
|
||||
<span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> {{#translate}}return to our website{{/translate}}
|
||||
</a>
|
||||
</p>
|
|
@ -1,79 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
|
||||
<meta name="description" content="{{#translate}}Self hosted email newsletter app{{/translate}}">
|
||||
<meta name="author" content="Andris Reinman">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
|
||||
<title>Mailtrain</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<!--
|
||||
<link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css">
|
||||
-->
|
||||
|
||||
<!-- -->
|
||||
<link rel="stylesheet" href="/bootstrap/themes/flatly.min.css">
|
||||
<!-- -->
|
||||
|
||||
<link rel="stylesheet" href="/css/narrow.css">
|
||||
|
||||
<link rel="stylesheet" href="/datepicker/css/bootstrap-datepicker3.css">
|
||||
{{#if useEditor}}
|
||||
<link rel="stylesheet" href="/summernote/summernote.css">
|
||||
{{/if}}
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="header clearfix">
|
||||
<h1 class="text-muted">{{title}}</h1>
|
||||
</div>
|
||||
|
||||
{{flash_messages}} {{{body}}}
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/javascript/jquery-2.2.1.min.js"></script>
|
||||
<script src="/bootstrap/js/bootstrap.min.js"></script>
|
||||
|
||||
<script src="/datepicker/js/bootstrap-datepicker.min.js"></script>
|
||||
<script src="/datatables/datatables.min.js"></script>
|
||||
<script src="/moment/moment.min.js"></script>
|
||||
<script src="/moment/moment-timezone-with-data.min.js"></script>
|
||||
<script src="/javascript/tables.js"></script>
|
||||
|
||||
{{#if useEditor}}
|
||||
<script src="/summernote/summernote.min.js"></script>
|
||||
<script src="/javascript/editor.js"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if uaCode}}
|
||||
<script>
|
||||
(function(i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
i[r] = i[r] || function() {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date();
|
||||
a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0];
|
||||
a.async = 1;
|
||||
a.src = g;
|
||||
m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', '{{uaCode}}', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
{{/if}}
|
||||
</body>
|
||||
|
||||
</html>
|
62
views/subscription/layout.mjml.hbs
Normal file
62
views/subscription/layout.mjml.hbs
Normal file
|
@ -0,0 +1,62 @@
|
|||
<mjml>
|
||||
<mj-head>
|
||||
<mj-title>{{title}}</mj-title>
|
||||
<mj-font name="Lato" href="https://fonts.googleapis.com/css?family=Lato:400,700,400italic" />
|
||||
<mj-attributes>
|
||||
<mj-all font-size="15px" color="#2D3E4F" font-family="Lato, Helvetica, Arial, sans-serif" />
|
||||
<mj-class name="h1" font-size="42px" line-height="68px" color="#b4bcc2" />
|
||||
<mj-class name="h3" font-size="24px" line-height="32px" />
|
||||
<mj-class name="p" font-size="15px" line-height="24px" />
|
||||
<mj-class name="small" font-size="12px" line-height="16px" color="#999999" />
|
||||
<mj-class name="hr" border-width="1px" border-style="solid" border-color="#e4e5e6" />
|
||||
<mj-class name="button" font-size="16px" background-color="#2D3E4F" color="white" align="left" inner-padding="16px 24px" border-radius="6px" />
|
||||
<mj-class name="footer-text" font-size="12px" line-height="18px" color="#999999" />
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-container width="560" background-color="#ffffff">
|
||||
|
||||
<mj-raw>
|
||||
{{#if isWeb}}
|
||||
<style>
|
||||
{{{formInputStyle}}}
|
||||
.alert { max-width: 520px; font-family: Lato, Helvetica, Arial, sans-serif; }
|
||||
</style>
|
||||
{{/if}}
|
||||
</mj-raw>
|
||||
|
||||
<mj-section padding-bottom="0">
|
||||
<mj-column>
|
||||
<mj-text mj-class="h1">
|
||||
{{title}}
|
||||
</mj-text>
|
||||
<mj-divider mj-class="hr"/>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-raw>
|
||||
{{#if isWeb}}
|
||||
{{> subscription_flash_messages}}
|
||||
{{/if}}
|
||||
</mj-raw>
|
||||
|
||||
{{{body}}}
|
||||
|
||||
<mj-section padding-top="0">
|
||||
<mj-column>
|
||||
<mj-text mj-class="small" font-style="italic">
|
||||
<!-- {{defaultPostaddress}} -->
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-raw>
|
||||
{{#if isWeb}}
|
||||
<!-- fixes https://github.com/mjmlio/mjml/issues/359 -->
|
||||
{{> subscription_footer_scripts btnBgColor='#2D3E4F' btnBgColorHover='#1A242F'}}
|
||||
{{/if}}
|
||||
</mj-raw>
|
||||
|
||||
</mj-container>
|
||||
</mj-body>
|
||||
</mjml>
|
17
views/subscription/mail-confirm-html.mjml.hbs
Normal file
17
views/subscription/mail-confirm-html.mjml.hbs
Normal file
|
@ -0,0 +1,17 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Please Confirm Subscription{{/translate}}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{confirmUrl}}">
|
||||
{{#translate}}Yes, subscribe me to this list{{/translate}}
|
||||
</mj-button>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}If you received this email by mistake, simply delete it. You won't be subscribed if you don't click the confirmation link above.{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}For questions about this list, please contact:{{/translate}}
|
||||
<br/><a href="mailto:{{contactAddress}}">{{contactAddress}}</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
18
views/subscription/mail-subscription-confirmed-html.mjml.hbs
Normal file
18
views/subscription/mail-subscription-confirmed-html.mjml.hbs
Normal file
|
@ -0,0 +1,18 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Subscription Confirmed{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}Your subscription to our list has been confirmed{{/translate}}. {{#translate}}If you want to modify your subscription then you can {{/translate}}
|
||||
<a href="{{preferencesUrl}}">{{#translate}}manage your preferences{{/translate}}</a> {{#translate}}or{{/translate}} <a href="{{unsubscribeUrl}}">{{#translate}}unsubscribe here{{/translate}}</a>.
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{homepage}}">
|
||||
{{#translate}}Return to our website{{/translate}}
|
||||
</mj-button>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}For questions about this list, please contact:{{/translate}}
|
||||
<br/><a href="mailto:{{contactAddress}}">{{contactAddress}}</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
17
views/subscription/mail-unsubscribe-confirmed-html.mjml.hbs
Normal file
17
views/subscription/mail-unsubscribe-confirmed-html.mjml.hbs
Normal file
|
@ -0,0 +1,17 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}You Are Now Unsubscribed{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}We have removed your email address from our list{{/translate}}. {{#translate}}If you unsubscribed by mistake, you can re-subscribe at:{{/translate}}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{subscribeUrl}}">
|
||||
{{#translate}}Subscribe{{/translate}}
|
||||
</mj-button>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}For questions about this list, please contact:{{/translate}}
|
||||
<br/><a href="mailto:{{contactAddress}}">{{contactAddress}}</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
|
@ -1,5 +1,5 @@
|
|||
{{{title}}}
|
||||
{{#translate}}You are now unsubscribed{{/translate}}
|
||||
{{#translate}}You Are Now Unsubscribed{{/translate}}
|
||||
========================
|
||||
|
||||
{{#translate}}We have removed your email address from our list.{{/translate}}
|
|
@ -1,25 +0,0 @@
|
|||
<h2>{{#translate}}Update your Email Address{{/translate}}</h2>
|
||||
|
||||
<form method="post" action="/subscription/{{lcid}}/manage-address">
|
||||
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">{{#translate}}Existing Email Address{{/translate}}</label>
|
||||
<input type="email" class="form-control" name="email" id="email" placeholder="" value="{{email}}" readonly>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email-new">{{#translate}}New Email Address{{/translate}}</label>
|
||||
<input type="email" class="form-control" name="email-new" id="email-new" placeholder="{{#translate}}Your new email address{{/translate}}" value="{{email}}">
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<small>{{#translate}}You will receive a confirmation request to your new email address that you need to accept before your email is actually changed{{/translate}}</small>
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-ok"></i> {{#translate}}Update Email Address{{/translate}}</button>
|
||||
</div>
|
||||
</form>
|
|
@ -1,129 +0,0 @@
|
|||
<h2>{{#translate}}Update your preferences{{/translate}}</h2>
|
||||
|
||||
{{#if hasPubkey}}
|
||||
<form method="post" id="download-pubkey" action="/subscription/publickey">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
</form>
|
||||
{{/if}}
|
||||
|
||||
<form method="post" action="/subscription/{{lcid}}/manage">
|
||||
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
<input type="hidden" class="tz-detect" name="tz" id="tz" value="{{tz}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">{{#translate}}Email Address{{/translate}}</label>
|
||||
<div class="input-group">
|
||||
<input type="email" class="form-control" name="email" id="email" placeholder="" value="{{email}}" readonly>
|
||||
<div class="input-group-addon"><a href="/subscription/{{lcid}}/manage-address/{{cid}}">{{#translate}}want to change it?{{/translate}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="first-name">{{#translate}}First Name{{/translate}}</label>
|
||||
<input type="text" class="form-control" name="first-name" id="first-name" placeholder="" value="{{firstName}}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="last-name">{{#translate}}Last Name{{/translate}}</label>
|
||||
<input type="text" class="form-control" name="last-name" id="last-name" placeholder="" value="{{lastName}}">
|
||||
</div>
|
||||
|
||||
{{#each customFields}}
|
||||
<div class="form-group">
|
||||
<label>{{name}}</label>
|
||||
|
||||
{{#if typeText}}
|
||||
<input type="text" class="form-control" name="{{column}}" value="{{value}}">
|
||||
{{/if}}
|
||||
|
||||
{{#if typeNumber}}
|
||||
<input type="number" class="form-control" name="{{column}}" value="{{value}}">
|
||||
{{/if}}
|
||||
|
||||
{{#if typeWebsite}}
|
||||
<input type="url" class="form-control" name="{{column}}" value="{{value}}">
|
||||
{{/if}}
|
||||
|
||||
{{#if typeLongtext}}
|
||||
<textarea class="form-control" rows="3" name="{{column}}">{{value}}</textarea>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeJson}}
|
||||
<textarea class="form-control gpg-text" rows="3" name="{{column}}" placeholder="{"data":"value"}">{{value}}</textarea>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeGpg}}
|
||||
{{#if ../hasPubkey}}
|
||||
<div class="pull-right">
|
||||
<button type="submit" class="btn btn-link btn-xs" form="download-pubkey"><span class="glyphicon glyphicon-cloud-download" aria-hidden="true"></span> {{#translate}}Download signature verification key{{/translate}}</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
<textarea class="form-control gpg-text" rows="3" name="{{column}}" placeholder="{{#translate}}Begins with{{/translate}} '-----BEGIN PGP PUBLIC KEY BLOCK-----'">{{value}}</textarea>
|
||||
<span class="help-block">{{#translate}}Insert your GPG public key here to encrypt messages sent to your address{{/translate}} <em>({{#translate}}optional{{/translate}})</em></span>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeDateUs}}
|
||||
<div class="input-group date fm-date-us">
|
||||
<input type="text" class="form-control" name="{{column}}" placeholder="MM/DD/YYYY" value="{{value}}"><span class="input-group-addon"><i class="glyphicon glyphicon-th"></i></span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeDateEur}}
|
||||
<div class="input-group date fm-date-eur">
|
||||
<input type="text" class="form-control" name="{{column}}" placeholder="DD/MM/YYYY" value="{{value}}"><span class="input-group-addon"><i class="glyphicon glyphicon-th"></i></span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeBirthdayUs}}
|
||||
<div class="input-group date fm-birthday-us">
|
||||
<input type="text" class="form-control" name="{{column}}" placeholder="MM/DD" value="{{value}}"><span class="input-group-addon"><i class="glyphicon glyphicon-th"></i></span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeBirthdayEur}}
|
||||
<div class="input-group date fm-birthday-eur">
|
||||
<input type="text" class="form-control" name="{{column}}" placeholder="DD/MM" value="{{value}}"><span class="input-group-addon"><i class="glyphicon glyphicon-th"></i></span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeDropdown}}
|
||||
<select name="{{key}}" class="form-control">
|
||||
<option value="">
|
||||
–– {{#translate}}Select{{/translate}} ––
|
||||
</option>
|
||||
{{#each options}}
|
||||
<option value="{{column}}" {{#if value}} selected {{/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeRadio}}
|
||||
{{#each options}}
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="{{../key}}" value="{{column}}" {{#if value}} checked {{/if}}> {{name}}
|
||||
</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if typeCheckbox}}
|
||||
{{#each options}}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="{{column}}" value="1" {{#if value}} checked {{/if}}> {{name}}
|
||||
</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-ok"></i> {{#translate}}Update Profile{{/translate}}</button> or <a href="/subscription/{{lcid}}/unsubscribe/{{cid}}">{{#translate}}Unsubscribe{{/translate}}</a>
|
||||
</div>
|
||||
</form>
|
143
views/subscription/partials/subscription-custom-fields.hbs
Normal file
143
views/subscription/partials/subscription-custom-fields.hbs
Normal file
|
@ -0,0 +1,143 @@
|
|||
{{#each customFields}}
|
||||
|
||||
{{#if typeSubsciptionEmail}}
|
||||
<div class="form-group email">
|
||||
<label for="email">{{#translate}}Email Address{{/translate}}</label>
|
||||
{{#if ../isManagePreferences}}
|
||||
<div class="input-group">
|
||||
<input type="email" name="email" id="email" placeholder="" value="{{../email}}" readonly>
|
||||
<div class="input-group-addon"><a href="/subscription/{{../lcid}}/manage-address/{{../cid}}">{{#translate}}want to change it?{{/translate}}</a></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input type="email" name="email" id="email" placeholder="" value="{{../email}}" required>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeFirstName}}
|
||||
<div class="form-group first-name">
|
||||
<label for="first-name">{{#translate}}First Name{{/translate}}</label>
|
||||
<input type="text" name="first-name" id="first-name" placeholder="" value="{{../firstName}}">
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeLastName}}
|
||||
<div class="form-group last-name">
|
||||
<label for="last-name">{{#translate}}Last Name{{/translate}}</label>
|
||||
<input type="text" name="last-name" id="last-name" placeholder="" value="{{../lastName}}">
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeText}}
|
||||
<div class="form-group text {{column}}">
|
||||
<label for="{{column}}">{{name}}</label>
|
||||
<input type="text" name="{{column}}" value="{{value}}">
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeNumber}}
|
||||
<div class="form-group number {{column}}">
|
||||
<label for="{{column}}">{{name}}</label>
|
||||
<input type="number" name="{{column}}" value="{{value}}">
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeWebsite}}
|
||||
<div class="form-group url {{column}}">
|
||||
<label for="{{column}}">{{name}}</label>
|
||||
<input type="url" name="{{column}}" value="{{value}}">
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeLongtext}}
|
||||
<div class="form-group longtext {{column}}">
|
||||
<label for="{{column}}">{{name}}</label>
|
||||
<textarea rows="3" name="{{column}}">{{value}}</textarea>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeJson}}
|
||||
<div class="form-group json {{column}}">
|
||||
<label for="{{column}}">{{name}}</label>
|
||||
<textarea class="gpg-text" rows="3" name="{{column}}" placeholder="{"data":"value"}">{{value}}</textarea>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeGpg}}
|
||||
<div class="form-group gpg {{column}}">
|
||||
<label for="{{column}}">{{name}}</label>
|
||||
{{#if ../hasPubkey}}
|
||||
<button class="btn-download-pubkey" type="submit" form="download-pubkey">{{#translate}}Download signature verification key{{/translate}}</button>
|
||||
{{/if}}
|
||||
<textarea class="form-control gpg-text" rows="4" name="{{column}}" placeholder="{{#translate}}Begins with{{/translate}} '-----BEGIN PGP PUBLIC KEY BLOCK-----'">{{value}}</textarea>
|
||||
<span class="help-block">
|
||||
{{#translate}}Insert your GPG public key here to encrypt messages sent to your address{{/translate}} <em>({{#translate}}optional{{/translate}})</em>
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeDateUs}}
|
||||
<div class="form-group date fm-date-us {{column}}">
|
||||
<label for="{{column}}">{{name}}</label>
|
||||
<input type="text" name="{{column}}" placeholder="MM/DD/YYYY" value="{{value}}">
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeDateEur}}
|
||||
<div class="form-group date fm-date-eur {{column}}">
|
||||
<label for="{{column}}">{{name}}</label>
|
||||
<input type="text" name="{{column}}" placeholder="DD/MM/YYYY" value="{{value}}">
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeBirthdayUs}}
|
||||
<div class="form-group date fm-birthday-us {{column}}">
|
||||
<label for="{{column}}">{{name}}</label>
|
||||
<input type="text" name="{{column}}" placeholder="MM/DD" value="{{value}}">
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeBirthdayEur}}
|
||||
<div class="form-group date fm-birthday-eur {{column}}">
|
||||
<label for="{{column}}">{{name}}</label>
|
||||
<input type="text" name="{{column}}" placeholder="DD/MM" value="{{value}}">
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeDropdown}}
|
||||
<div class="form-group dropdown {{key}}">
|
||||
<label for="{{key}}">{{name}}</label>
|
||||
<select name="{{key}}" class="form-control">
|
||||
<option value="">
|
||||
–– {{#translate}}Select{{/translate}} ––
|
||||
</option>
|
||||
{{#each options}}
|
||||
<option value="{{column}}" {{#if value}} selected {{/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeRadio}}
|
||||
<div class="form-group radio {{key}}">
|
||||
<label for="{{key}}">{{name}}</label>
|
||||
{{#each options}}
|
||||
<label class="label-radio">
|
||||
<input type="radio" name="{{../key}}" value="{{column}}" {{#if value}} checked {{/if}}> {{name}}
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeCheckbox}}
|
||||
<div class="form-group checkbox">
|
||||
<label>{{name}}</label>
|
||||
{{#each options}}
|
||||
<label class="label-checkbox">
|
||||
<input type="checkbox" name="{{column}}" value="1" {{#if value}} checked {{/if}}> {{name}}
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{/each}}
|
20
views/subscription/partials/subscription-flash-messages.hbs
Normal file
20
views/subscription/partials/subscription-flash-messages.hbs
Normal file
|
@ -0,0 +1,20 @@
|
|||
{{{flashMessages}}}
|
||||
|
||||
{{#if isConfirmNotice}}
|
||||
<div class="alert alert-warning js-warning" role="alert">
|
||||
<strong>{{#translate}}Warning!{{/translate}}</strong> {{#translate}}If JavaScript was not enabled then no confirmation message was sent{{/translate}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if needsJsWarning}}
|
||||
<div class="alert alert-danger js-warning" role="alert">
|
||||
<strong>{{#translate}}Warning!{{/translate}}</strong>
|
||||
{{#translate}}JavaScript must be enabled in order for this form to work{{/translate}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.js-warning').forEach(function(el) {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
</script>
|
56
views/subscription/partials/subscription-footer-scripts.hbs
Normal file
56
views/subscription/partials/subscription-footer-scripts.hbs
Normal file
|
@ -0,0 +1,56 @@
|
|||
<script>
|
||||
var btnBgColor = '{{btnBgColor}}';
|
||||
var btnBgColorHover = '{{btnBgColorHover}}';
|
||||
|
||||
if (typeof moment !== 'undefined' && moment.tz) {
|
||||
(function () {
|
||||
var tz = moment.tz.guess();
|
||||
if (tz) {
|
||||
document.querySelectorAll('.tz-detect').forEach(function(el) {
|
||||
el.value = tz;
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
document.querySelectorAll('a[href="#submit"]').forEach(function(a) {
|
||||
a.onclick = function() {
|
||||
var form = document.getElementById('main-form');
|
||||
form && form.submit();
|
||||
return false;
|
||||
};
|
||||
});
|
||||
|
||||
// Fixes MJML Button until they do ...
|
||||
// https://github.com/mjmlio/mjml/issues/359
|
||||
|
||||
if (window.btnBgColor) {
|
||||
(function() {
|
||||
var s = document.createElement('style');
|
||||
var c = document.createTextNode(
|
||||
'.td-btn:hover { background-color: ' + window.btnBgColorHover + '; }' +
|
||||
'.td-btn { cursor: pointer !important; }' +
|
||||
'.a-btn { background-color: transparent !important; }'
|
||||
);
|
||||
s.appendChild(c);
|
||||
document.getElementsByTagName('head')[0].appendChild(s);
|
||||
document.querySelectorAll('a').forEach(function(a) {
|
||||
if (a.parentNode.getAttribute('bgcolor') === window.btnBgColor) {
|
||||
a.target = '_self';
|
||||
a.className += 'a-btn';
|
||||
a.parentNode.className += 'td-btn';
|
||||
a.parentNode.onclick = function() {
|
||||
a.click();
|
||||
};
|
||||
}
|
||||
});
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- custom scripts from config -->
|
||||
{{#each customSubscriptionScripts}}
|
||||
<script src="{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{> tracking_scripts}}
|
|
@ -0,0 +1,22 @@
|
|||
<form id="main-form" method="post" action="/subscription/{{lcid}}/manage-address">
|
||||
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">{{#translate}}Existing Email Address{{/translate}}</label>
|
||||
<input type="email" name="email" id="email" placeholder="" value="{{email}}" readonly>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email-new">{{#translate}}New Email Address{{/translate}}</label>
|
||||
<input type="email" name="email-new" id="email-new" placeholder="{{#translate}}Your new email address{{/translate}}" value="{{email}}">
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{{#translate}}You will receive a confirmation request to your new email address that you need to accept before your email is actually changed{{/translate}}
|
||||
</p>
|
||||
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">{{#translate}}Update Email Address{{/translate}}</button>
|
||||
|
||||
</form>
|
19
views/subscription/partials/subscription-manage-form.hbs
Normal file
19
views/subscription/partials/subscription-manage-form.hbs
Normal file
|
@ -0,0 +1,19 @@
|
|||
{{#if hasPubkey}}
|
||||
<form method="post" id="download-pubkey" action="/subscription/publickey" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
</form>
|
||||
{{/if}}
|
||||
|
||||
<form id="main-form" method="post" action="/subscription/{{lcid}}/manage">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
<input type="hidden" class="tz-detect" name="tz" id="tz" value="{{tz}}">
|
||||
|
||||
{{> subscription_custom_fields}}
|
||||
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">{{#translate}}Update Profile{{/translate}}</button>
|
||||
</form>
|
||||
|
||||
<script src="/moment/moment.min.js"></script>
|
||||
<script src="/moment/moment-timezone-with-data.min.js"></script>
|
24
views/subscription/partials/subscription-subscribe-form.hbs
Normal file
24
views/subscription/partials/subscription-subscribe-form.hbs
Normal file
|
@ -0,0 +1,24 @@
|
|||
{{#if hasPubkey}}
|
||||
<form method="post" id="download-pubkey" action="/subscription/publickey" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
</form>
|
||||
{{/if}}
|
||||
|
||||
<form id="main-form" method="post" action="/subscription/{{cid}}/subscribe">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" class="tz-detect" name="tz" id="tz" value="{{tz}}">
|
||||
<input type="hidden" name="address" value="">
|
||||
<input type="hidden" name="sub" id="sub" value="">
|
||||
|
||||
{{> subscription_custom_fields}}
|
||||
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">{{#translate}}Subscribe to list{{/translate}}</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('sub').value = new Date().getTime();
|
||||
</script>
|
||||
|
||||
<script src="/moment/moment.min.js"></script>
|
||||
<script src="/moment/moment-timezone-with-data.min.js"></script>
|
|
@ -0,0 +1,20 @@
|
|||
<form method="post" id="main-form" action="/subscription/{{lcid}}/unsubscribe">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="campaign" value="{{campaign}}">
|
||||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">{{#translate}}Email address{{/translate}}</label>
|
||||
<input type="email" name="email" id="email" placeholder="" value="{{email}}" autofocus required>
|
||||
</div>
|
||||
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">{{#translate}}Unsubscribe{{/translate}}</button>
|
||||
</form>
|
||||
|
||||
{{#if email}}
|
||||
{{#if autosubmit}}
|
||||
<script>
|
||||
document.getElementById('main-form').submit();
|
||||
</script>
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -1,142 +0,0 @@
|
|||
{{#if hasPubkey}}
|
||||
<form method="post" id="download-pubkey" action="/subscription/publickey">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
</form>
|
||||
{{/if}}
|
||||
|
||||
<div class="alert alert-warning alert-dismissible" role="alert" id="js-warning">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<strong>{{#translate}}Warning!{{/translate}}</strong>
|
||||
{{#translate}}JavaScript must be enabled in order for the subscription form to work{{/translate}}
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('js-warning').style.display = 'none';
|
||||
</script>
|
||||
|
||||
<form method="post" action="/subscription/{{cid}}/subscribe">
|
||||
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" class="tz-detect" name="tz" id="tz" value="{{tz}}">
|
||||
|
||||
<input type="hidden" name="address" value="">
|
||||
<input type="hidden" name="sub" id="sub" value="">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">{{#translate}}Email Address{{/translate}}</label>
|
||||
<input type="email" class="form-control" name="email" id="email" placeholder="" value="{{email}}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="first-name">{{#translate}}First Name{{/translate}}</label>
|
||||
<input type="text" class="form-control" name="first-name" id="first-name" placeholder="" value="{{firstName}}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="last-name">{{#translate}}Last Name{{/translate}}</label>
|
||||
<input type="text" class="form-control" name="last-name" id="last-name" placeholder="" value="{{lastName}}">
|
||||
</div>
|
||||
|
||||
{{#each customFields}}
|
||||
<div class="form-group">
|
||||
<label>{{name}}</label>
|
||||
|
||||
{{#if typeText}}
|
||||
<input type="text" class="form-control" name="{{column}}" value="{{value}}">
|
||||
{{/if}}
|
||||
|
||||
{{#if typeNumber}}
|
||||
<input type="number" class="form-control" name="{{column}}" value="{{value}}">
|
||||
{{/if}}
|
||||
|
||||
{{#if typeWebsite}}
|
||||
<input type="url" class="form-control" name="{{column}}" value="{{value}}">
|
||||
{{/if}}
|
||||
|
||||
{{#if typeLongtext}}
|
||||
<textarea class="form-control" rows="3" name="{{column}}">{{value}}</textarea>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeJson}}
|
||||
<textarea class="form-control gpg-text" rows="3" name="{{column}}" placeholder="{"data":"value"}">{{value}}</textarea>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeGpg}}
|
||||
{{#if ../hasPubkey}}
|
||||
<div class="pull-right">
|
||||
<button type="submit" class="btn btn-link btn-xs" form="download-pubkey"><span class="glyphicon glyphicon-cloud-download" aria-hidden="true"></span> {{#translate}}Download signature verification key{{/translate}}</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
<textarea class="form-control gpg-text" rows="3" name="{{column}}" placeholder="{{#translate}}Begins with{{/translate}} '-----BEGIN PGP PUBLIC KEY BLOCK-----'">{{value}}</textarea>
|
||||
<span class="help-block">{{#translate}}Insert your GPG public key here to encrypt messages sent to your address{{/translate}} <em>({{#translate}}optional{{/translate}})</em></span>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeDateUs}}
|
||||
<div class="input-group date fm-date-us">
|
||||
<input type="text" class="form-control" name="{{column}}" placeholder="MM/DD/YYYY" value="{{value}}"><span class="input-group-addon"><i class="glyphicon glyphicon-th"></i></span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeDateEur}}
|
||||
<div class="input-group date fm-date-eur">
|
||||
<input type="text" class="form-control" name="{{column}}" placeholder="DD/MM/YYYY" value="{{value}}"><span class="input-group-addon"><i class="glyphicon glyphicon-th"></i></span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeBirthdayUs}}
|
||||
<div class="input-group date fm-birthday-us">
|
||||
<input type="text" class="form-control" name="{{column}}" placeholder="MM/DD" value="{{value}}"><span class="input-group-addon"><i class="glyphicon glyphicon-th"></i></span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeBirthdayEur}}
|
||||
<div class="input-group date fm-birthday-eur">
|
||||
<input type="text" class="form-control" name="{{column}}" placeholder="DD/MM" value="{{value}}"><span class="input-group-addon"><i class="glyphicon glyphicon-th"></i></span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeDropdown}}
|
||||
<select name="{{key}}" class="form-control">
|
||||
<option value="">
|
||||
–– {{#translate}}Select{{/translate}} ––
|
||||
</option>
|
||||
{{#each options}}
|
||||
<option value="{{column}}" {{#if value}} selected {{/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
{{/if}}
|
||||
|
||||
{{#if typeRadio}}
|
||||
{{#each options}}
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="{{../key}}" value="{{column}}" {{#if value}} checked {{/if}}> {{name}}
|
||||
</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if typeCheckbox}}
|
||||
{{#each options}}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="{{column}}" value="1" {{#if value}} checked {{/if}}> {{name}}
|
||||
</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
<div class="form-group" id="js-subscribe" style="display: none">
|
||||
<button type="submit" class="btn btn-primary">{{#translate}}Subscribe to list{{/translate}}</button>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('js-subscribe').style.display = 'block';
|
||||
</script>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('sub').value = new Date().getTime();
|
||||
</script>
|
|
@ -1,21 +0,0 @@
|
|||
<h2>{{#translate}}Subscription Confirmed{{/translate}}</h2>
|
||||
|
||||
<p>
|
||||
{{#translate}}Your subscription to our list has been confirmed.{{/translate}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{{#translate}}Thank you for subscribing!{{/translate}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a class="btn btn-primary" href="{{homepage}}" role="button">
|
||||
<span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span>
|
||||
{{#translate}}continue to our website{{/translate}}
|
||||
</a>
|
||||
{{#translate}}or{{/translate}}
|
||||
<a class="btn btn-primary" href="{{preferences}}" role="button">
|
||||
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
|
||||
{{#translate}}manage your preferences{{/translate}}
|
||||
</a>
|
||||
</p>
|
|
@ -1,9 +0,0 @@
|
|||
<h2>{{#translate}}Unsubscribe Successful{{/translate}}</h2>
|
||||
|
||||
<p>{{#translate}}You have been removed from:{{/translate}} {{title}}.</p>
|
||||
|
||||
<p>
|
||||
<a class="btn btn-primary" href="{{homepage}}" role="button">
|
||||
<span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> {{#translate}}return to our website{{/translate}}
|
||||
</a>
|
||||
</p>
|
|
@ -1,27 +0,0 @@
|
|||
<h2>{{#translate}}Unsubscribe{{/translate}}</h2>
|
||||
|
||||
<p>
|
||||
{{#translate}}Enter your email address to unsubscribe from:{{/translate}} {{title}}
|
||||
</p>
|
||||
|
||||
<form method="post" id="unsubscribe-form" action="/subscription/{{lcid}}/unsubscribe">
|
||||
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="campaign" value="{{campaign}}">
|
||||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">{{#translate}}Email address{{/translate}}</label>
|
||||
<input type="email" class="form-control" name="email" id="email" placeholder="" value="{{email}}" autofocus required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" id="unsubscribe-button" class="btn btn-primary">{{#translate}}Unsubscribe{{/translate}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{#if email}}
|
||||
{{#if autosubmit}}
|
||||
<script src="/javascript/autosubmit.js"></script>
|
||||
{{/if}}
|
||||
{{/if}}
|
|
@ -1,12 +0,0 @@
|
|||
<h3>{{#translate}}Profile Updated{{/translate}}</h3>
|
||||
|
||||
<p>
|
||||
{{#translate}}Your profile information has been updated.{{/translate}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a class="btn btn-primary" href="{{homepage}}" role="button">
|
||||
<span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span>
|
||||
{{#translate}}return to our website{{/translate}}
|
||||
</a>
|
||||
</p>
|
13
views/subscription/web-confirm-notice.mjml.hbs
Normal file
13
views/subscription/web-confirm-notice.mjml.hbs
Normal file
|
@ -0,0 +1,13 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Almost Finished{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}We need to confirm your email address. To complete the subscription process, please click the link in the email we just sent you.{{/translate}}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{homepage}}">
|
||||
{{#translate}}Return to our website{{/translate}}
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
13
views/subscription/web-manage-address.mjml.hbs
Normal file
13
views/subscription/web-manage-address.mjml.hbs
Normal file
|
@ -0,0 +1,13 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Update Your Email Address{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text>
|
||||
{{> subscription_manage_address_form}}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="#submit">
|
||||
{{#translate}}Update Email Address{{/translate}}
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
16
views/subscription/web-manage.mjml.hbs
Normal file
16
views/subscription/web-manage.mjml.hbs
Normal file
|
@ -0,0 +1,16 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Update Your Preferences{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text>
|
||||
{{> subscription_manage_form}}<!-- don't indent me! -->
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="#submit">
|
||||
{{#translate}}Update Profile{{/translate}}
|
||||
</mj-button>
|
||||
<mj-text mj-class="p">
|
||||
<a href="/subscription/{{lcid}}/unsubscribe/{{cid}}">{{#translate}}Unsubscribe{{/translate}}</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
13
views/subscription/web-subscribe.mjml.hbs
Normal file
13
views/subscription/web-subscribe.mjml.hbs
Normal file
|
@ -0,0 +1,13 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Subscribe to List{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text>
|
||||
{{> subscription_subscribe_form}}<!-- don't indent me! -->
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="#submit">
|
||||
{{#translate}}Subscribe to list{{/translate}}
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
13
views/subscription/web-subscribed.mjml.hbs
Normal file
13
views/subscription/web-subscribed.mjml.hbs
Normal file
|
@ -0,0 +1,13 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Subscription Confirmed{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}Your subscription to our list has been confirmed.{{/translate}}<br>{{#translate}}Thank you for subscribing!{{/translate}}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{homepage}}">
|
||||
{{#translate}}Return to our website{{/translate}}
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
13
views/subscription/web-unsubscribe-notice.mjml.hbs
Normal file
13
views/subscription/web-unsubscribe-notice.mjml.hbs
Normal file
|
@ -0,0 +1,13 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Unsubscribe Successful{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}You have been removed from:{{/translate}} {{title}}.
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{homepage}}">
|
||||
{{#translate}}Return to our website{{/translate}}
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
16
views/subscription/web-unsubscribe.mjml.hbs
Normal file
16
views/subscription/web-unsubscribe.mjml.hbs
Normal file
|
@ -0,0 +1,16 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Unsubscribe{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}Enter your email address to unsubscribe from:{{/translate}} {{title}}
|
||||
</mj-text>
|
||||
<mj-text>
|
||||
{{> subscription_unsubscribe_form}}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="#submit">
|
||||
{{#translate}}Unsubscribe{{/translate}}
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
13
views/subscription/web-updated-notice.mjml.hbs
Normal file
13
views/subscription/web-updated-notice.mjml.hbs
Normal file
|
@ -0,0 +1,13 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Profile Updated{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}Your profile information has been updated.{{/translate}}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{homepage}}">
|
||||
{{#translate}}Return to our website{{/translate}}
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
Loading…
Reference in a new issue