617 lines
24 KiB
JavaScript
617 lines
24 KiB
JavaScript
'use strict';
|
|
|
|
let db = require('../db');
|
|
let tools = require('../tools');
|
|
let slugify = require('slugify');
|
|
let lists = require('./lists');
|
|
let shortid = require('shortid');
|
|
let Handlebars = require('handlebars');
|
|
let _ = require('../translate')._;
|
|
let util = require('util');
|
|
|
|
let allowedKeys = ['name', 'key', 'default_value', 'group', 'group_template', 'visible'];
|
|
let allowedTypes;
|
|
|
|
module.exports.grouped = ['radio', 'checkbox', 'dropdown'];
|
|
module.exports.types = {
|
|
text: _('Text'),
|
|
website: _('Website'),
|
|
longtext: _('Multi-line text'),
|
|
gpg: _('GPG Public Key'),
|
|
number: _('Number'),
|
|
radio: _('Radio Buttons'),
|
|
checkbox: _('Checkboxes'),
|
|
dropdown: _('Drop Down'),
|
|
'date-us': _('Date (MM/DD/YYY)'),
|
|
'date-eur': _('Date (DD/MM/YYYY)'),
|
|
'birthday-us': _('Birthday (MM/DD)'),
|
|
'birthday-eur': _('Birthday (DD/MM)'),
|
|
json: _('JSON value for custom rendering'),
|
|
option: _('Option')
|
|
};
|
|
|
|
module.exports.allowedTypes = allowedTypes = Object.keys(module.exports.types);
|
|
|
|
module.exports.genericTypes = {
|
|
text: 'string',
|
|
website: 'string',
|
|
longtext: 'textarea',
|
|
gpg: 'textarea',
|
|
json: 'textarea',
|
|
number: 'number',
|
|
'date-us': 'date',
|
|
'date-eur': 'date',
|
|
'birthday-us': 'birthday',
|
|
'birthday-eur': 'birthday',
|
|
option: 'boolean'
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
let query = 'SELECT * FROM custom_fields WHERE list=? ORDER BY id';
|
|
connection.query(query, [listId], (err, rows) => {
|
|
connection.release();
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
let fieldList = rows && rows.map(row => tools.convertKeys(row)) || [];
|
|
let groups = new Map();
|
|
|
|
// remove grouped rows
|
|
for (let i = fieldList.length - 1; i >= 0; i--) {
|
|
let field = fieldList[i];
|
|
if (module.exports.grouped.indexOf(field.type) >= 0) {
|
|
if (!groups.has(field.id)) {
|
|
groups.set(field.id, []);
|
|
}
|
|
field.options = groups.get(field.id);
|
|
} else if (field.group && field.type === 'option') {
|
|
if (!groups.has(field.group)) {
|
|
groups.set(field.group, [field]);
|
|
} else {
|
|
groups.get(field.group).unshift(field);
|
|
}
|
|
fieldList.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
return callback(null, fieldList);
|
|
});
|
|
});
|
|
};
|
|
|
|
module.exports.get = (id, callback) => {
|
|
id = Number(id) || 0;
|
|
|
|
if (id < 1) {
|
|
return callback(new Error(_('Missing List ID')));
|
|
}
|
|
|
|
db.getConnection((err, connection) => {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
let query = 'SELECT * FROM custom_fields WHERE id=? LIMIT 1';
|
|
connection.query(query, [id], (err, rows) => {
|
|
connection.release();
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
let field = rows && rows[0] && tools.convertKeys(rows[0]) || false;
|
|
field.isGroup = module.exports.grouped.indexOf(field.type) >= 0 || field.type === 'json';
|
|
return callback(null, field);
|
|
});
|
|
});
|
|
};
|
|
|
|
module.exports.create = (listId, field, callback) => {
|
|
listId = Number(listId) || 0;
|
|
|
|
if (listId < 1) {
|
|
return callback(new Error(_('Missing List ID')));
|
|
}
|
|
|
|
field = tools.convertKeys(field);
|
|
|
|
if (field.type === 'option' && !field.group) {
|
|
return callback(new Error(_('Option field requires a group to be selected')));
|
|
}
|
|
|
|
if (field.type !== 'option') {
|
|
field.group = null;
|
|
}
|
|
|
|
field.defaultValue = (field.defaultValue || '').toString().trim() || null;
|
|
field.groupTemplate = (field.groupTemplate || '').toString().trim() || null;
|
|
|
|
addCustomField(listId, field.name, field.defaultValue, field.type, field.group, field.groupTemplate, field.visible, callback);
|
|
};
|
|
|
|
module.exports.update = (id, updates, callback) => {
|
|
updates = updates || {};
|
|
id = Number(id) || 0;
|
|
|
|
updates = tools.convertKeys(updates);
|
|
|
|
if (id < 1) {
|
|
return callback(new Error(_('Missing Field ID')));
|
|
}
|
|
|
|
if (!(updates.name || '').toString().trim()) {
|
|
return callback(new Error(_('Field Name must be set')));
|
|
}
|
|
|
|
if (updates.key) {
|
|
updates.key = slugify(updates.key, '_').toUpperCase();
|
|
}
|
|
|
|
updates.defaultValue = (updates.defaultValue || '').toString().trim() || null;
|
|
updates.groupTemplate = (updates.groupTemplate || '').toString().trim() || null;
|
|
|
|
updates.visible = updates.visible ? 1 : 0;
|
|
|
|
let name = (updates.name || '').toString().trim();
|
|
let keys = ['name'];
|
|
let values = [name];
|
|
|
|
Object.keys(updates).forEach(key => {
|
|
let value = typeof updates[key] === 'string' ? updates[key].trim() : updates[key];
|
|
key = tools.toDbKey(key);
|
|
if (allowedKeys.indexOf(key) >= 0) {
|
|
keys.push(key);
|
|
values.push(value);
|
|
}
|
|
});
|
|
|
|
db.getConnection((err, connection) => {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
values.push(id);
|
|
|
|
connection.query('UPDATE custom_fields SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
|
connection.release();
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
return callback(null, result && result.affectedRows || false);
|
|
});
|
|
});
|
|
};
|
|
|
|
module.exports.delete = (fieldId, callback) => {
|
|
fieldId = Number(fieldId) || 0;
|
|
|
|
if (fieldId < 1) {
|
|
return callback(new Error(_('Missing Field ID')));
|
|
}
|
|
|
|
db.getConnection((err, connection) => {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
let query = 'SELECT * FROM custom_fields WHERE id=? LIMIT 1';
|
|
connection.query(query, [fieldId], (err, rows) => {
|
|
if (err) {
|
|
connection.release();
|
|
return callback(err);
|
|
}
|
|
|
|
if (!rows || !rows.length) {
|
|
connection.release();
|
|
return callback(new Error(_('Custom field not found')));
|
|
}
|
|
|
|
let field = tools.convertKeys(rows[0]);
|
|
if (field.column) {
|
|
connection.query('ALTER TABLE `subscription__' + field.list + '` DROP COLUMN `' + field.column + '`', err => {
|
|
if (err && err.code !== 'ER_CANT_DROP_FIELD_OR_KEY') {
|
|
connection.release();
|
|
return callback(err);
|
|
}
|
|
connection.query('DELETE FROM custom_fields WHERE id=? LIMIT 1', [fieldId], err => {
|
|
if (err) {
|
|
connection.release();
|
|
return callback(err);
|
|
}
|
|
connection.query('DELETE FROM segment_rules WHERE column=? LIMIT 1', [field.column], err => {
|
|
connection.release();
|
|
if (err) {
|
|
// ignore
|
|
}
|
|
return callback(null, true);
|
|
});
|
|
});
|
|
});
|
|
} else {
|
|
// delete all subfields in this group
|
|
let query = 'SELECT id FROM custom_fields WHERE `group`=?';
|
|
connection.query(query, [fieldId], (err, rows) => {
|
|
connection.release();
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
if (!rows || !rows.length) {
|
|
rows = [];
|
|
}
|
|
|
|
let pos = 0;
|
|
let deleteNext = () => {
|
|
if (pos >= rows.length) {
|
|
db.getConnection((err, connection) => {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
connection.query('DELETE FROM custom_fields WHERE id=? LIMIT 1', [fieldId], err => {
|
|
connection.release();
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
return callback(null, true);
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
module.exports.delete(rows[pos++].id, deleteNext);
|
|
};
|
|
|
|
deleteNext();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
function addCustomField(listId, name, defaultValue, type, group, groupTemplate, visible, callback) {
|
|
type = (type || '').toString().trim().toLowerCase();
|
|
group = Number(group) || null;
|
|
listId = Number(listId) || 0;
|
|
|
|
let column = null;
|
|
let key = slugify('merge ' + name, '_').toUpperCase();
|
|
|
|
if (allowedTypes.indexOf(type) < 0) {
|
|
return callback(new Error(util.format(_('Unknown column type %s'), type)));
|
|
}
|
|
|
|
if (!name) {
|
|
return callback(new Error(_('Missing column name')));
|
|
}
|
|
|
|
if (listId <= 0) {
|
|
return callback(new Error(_('Missing list ID')));
|
|
}
|
|
|
|
lists.get(listId, (err, list) => {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
if (!list) {
|
|
return callback(_('Provided List ID not found'));
|
|
}
|
|
|
|
db.getConnection((err, connection) => {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
if (module.exports.grouped.indexOf(type) < 0) {
|
|
column = ('custom_' + slugify(name, '_') + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '');
|
|
}
|
|
|
|
let query = 'INSERT INTO custom_fields (`list`, `name`, `key`,`default_value`, `type`, `group`, `group_template`, `column`, `visible`) VALUES(?,?,?,?,?,?,?,?,?)';
|
|
connection.query(query, [listId, name, key, defaultValue, type, group, groupTemplate, column, visible ? 1 : 0], (err, result) => {
|
|
|
|
if (err) {
|
|
connection.release();
|
|
return callback(err);
|
|
}
|
|
|
|
let fieldId = result && result.insertId;
|
|
let indexQuery;
|
|
|
|
switch (type) {
|
|
case 'text':
|
|
case 'website':
|
|
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` VARCHAR(255) DEFAULT NULL';
|
|
indexQuery = 'CREATE INDEX ' + column + '_index ON `subscription__' + listId + '` (`column`);';
|
|
break;
|
|
case 'gpg':
|
|
case 'longtext':
|
|
case 'json':
|
|
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` TEXT DEFAULT NULL';
|
|
break;
|
|
case 'number':
|
|
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` INT(11) DEFAULT NULL';
|
|
indexQuery = 'CREATE INDEX ' + column + '_index ON `subscription__' + listId + '` (`column`);';
|
|
break;
|
|
case 'option':
|
|
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` TINYINT(4) UNSIGNED NOT NULL DEFAULT \'0\'';
|
|
indexQuery = 'CREATE INDEX ' + column + '_index ON `subscription__' + listId + '` (`column`);';
|
|
break;
|
|
case 'date-us':
|
|
case 'date-eur':
|
|
case 'birthday-us':
|
|
case 'birthday-eur':
|
|
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` TIMESTAMP NULL DEFAULT NULL';
|
|
indexQuery = 'CREATE INDEX ' + column + '_index ON `subscription__' + listId + '` (`column`);';
|
|
break;
|
|
default:
|
|
connection.release();
|
|
return callback(null, fieldId, key);
|
|
}
|
|
|
|
connection.query(query, err => {
|
|
if (err) {
|
|
connection.query('DELETE FROM custom_fields WHERE id=? LIMIT 1', [fieldId], () => connection.release());
|
|
return callback(err);
|
|
}
|
|
if (!indexQuery) {
|
|
connection.release();
|
|
return callback(null, fieldId, key);
|
|
} else {
|
|
connection.query(query, err => {
|
|
if (err) {
|
|
// ignore index errors
|
|
}
|
|
connection.release();
|
|
return callback(null, fieldId, key);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|
let valueList = {};
|
|
let row = [];
|
|
|
|
Object.keys(values || {}).forEach(key => {
|
|
let value = values[key];
|
|
key = tools.toDbKey(key);
|
|
if (key.indexOf('custom_') === 0) {
|
|
valueList[key] = value;
|
|
} else if (key.indexOf('group_g') === 0 && value.indexOf('custom_') === 0) {
|
|
valueList[tools.toDbKey(value)] = 1;
|
|
}
|
|
});
|
|
|
|
fieldList.filter(field => showAll || field.visible).forEach(field => {
|
|
if (onlyExisting && field.column && !valueList.hasOwnProperty(field.column)) {
|
|
// ignore missing values
|
|
return;
|
|
}
|
|
/* eslint-disable indent */
|
|
switch (field.type) {
|
|
case 'text':
|
|
case 'website':
|
|
case 'gpg':
|
|
case 'longtext':
|
|
{
|
|
let item = {
|
|
id: field.id,
|
|
type: field.type,
|
|
name: field.name,
|
|
column: field.column,
|
|
value: (valueList[field.column] || '').toString().trim(),
|
|
visible: !!field.visible,
|
|
mergeTag: field.key,
|
|
mergeValue: (valueList[field.column] || '').toString().trim() || field.defaultValue,
|
|
['type' + (field.type || '').toString().trim().replace(/(?:^|-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
|
};
|
|
row.push(item);
|
|
break;
|
|
}
|
|
case 'json':
|
|
{
|
|
let value;
|
|
let json = (valueList[field.column] || '').toString().trim();
|
|
try {
|
|
let parsed = JSON.parse(json);
|
|
if (Array.isArray(parsed)) {
|
|
parsed = {
|
|
values: parsed
|
|
};
|
|
}
|
|
value = json ? render(field.groupTemplate, parsed) : '';
|
|
} catch (E) {
|
|
value = E.message;
|
|
}
|
|
|
|
let item = {
|
|
id: field.id,
|
|
type: field.type,
|
|
name: field.name,
|
|
column: field.column,
|
|
value: (valueList[field.column] || '').toString().trim(),
|
|
visible: !!field.visible,
|
|
mergeTag: field.key,
|
|
mergeValue: value || field.defaultValue,
|
|
['type' + (field.type || '').toString().trim().replace(/(?:^|-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
|
};
|
|
row.push(item);
|
|
break;
|
|
}
|
|
case 'number':
|
|
{
|
|
let item = {
|
|
id: field.id,
|
|
type: field.type,
|
|
name: field.name,
|
|
column: field.column,
|
|
value: Number(valueList[field.column]) || 0,
|
|
visible: !!field.visible,
|
|
mergeTag: field.key,
|
|
mergeValue: (Number(valueList[field.column]) || Number(field.defaultValue) || 0).toString(),
|
|
['type' + (field.type || '').toString().trim().replace(/(?:^|-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
|
};
|
|
row.push(item);
|
|
break;
|
|
}
|
|
case 'dropdown':
|
|
case 'radio':
|
|
case 'checkbox':
|
|
{
|
|
let hasSelectedOption = (field.options || []).some(subField => subField.column && valueList[subField.column]);
|
|
let item = {
|
|
id: field.id,
|
|
type: field.type,
|
|
name: field.name,
|
|
visible: !!field.visible,
|
|
key: 'group-g' + field.id,
|
|
mergeTag: field.key,
|
|
mergeValue: field.defaultValue,
|
|
['type' + (field.type || '').toString().trim().replace(/(?:^|-)([a-z])/g, (m, c) => c.toUpperCase())]: true,
|
|
groupTemplate: field.groupTemplate,
|
|
options: (field.options || []).map(subField => {
|
|
if (onlyExisting && subField.column && !valueList.hasOwnProperty(subField.column)) {
|
|
if (hasSelectedOption && field.type !== 'checkbox') {
|
|
// Set all radio or dropdown options if a selection for the group is present
|
|
} else if (field.type === 'checkbox' && values['originGroupG' + field.id] === 'webform') {
|
|
// Set all checkbox options if origin is webform (subscribe, manage, or admin edit) #333
|
|
// Atomic updates via API call or CSV import still possible
|
|
} else {
|
|
// ignore missing values
|
|
return false;
|
|
}
|
|
}
|
|
return {
|
|
type: subField.type,
|
|
name: subField.name,
|
|
column: subField.column,
|
|
value: valueList[subField.column] ? 1 : 0,
|
|
visible: !!subField.visible,
|
|
mergeTag: subField.key,
|
|
mergeValue: valueList[subField.column] ? subField.name : subField.defaultValue
|
|
};
|
|
}).filter(subField => subField)
|
|
};
|
|
let subItems = item.options.filter(subField => (showAll || subField.visible) && subField.value).map(subField => subField.name);
|
|
item.value = field.groupTemplate ? render(field.groupTemplate, {
|
|
values: subItems
|
|
}) : subItems.join(', ');
|
|
item.mergeValue = item.value || field.defaultValue;
|
|
row.push(item);
|
|
break;
|
|
}
|
|
case 'date-eur':
|
|
case 'birthday-eur':
|
|
case 'date-us':
|
|
case 'birthday-us':
|
|
{
|
|
let isUs = /-us$/.test(field.type);
|
|
let isYear = field.type.indexOf('date-') === 0;
|
|
let value = valueList[field.column];
|
|
let day, month, year;
|
|
let formatted;
|
|
|
|
if (value && typeof value.getUTCFullYear === 'function') {
|
|
day = value.getUTCDate();
|
|
month = value.getUTCMonth() + 1;
|
|
year = value.getUTCFullYear();
|
|
} else {
|
|
value = (value || '').toString().trim();
|
|
|
|
// try international format first YYYY-MM-DD
|
|
let parts = value.match(/(\d{4})\D+(\d{2})(?:\D+(\d{2})\b)?/);
|
|
if (parts) {
|
|
year = Number(parts[1]) || 2000;
|
|
month = Number(parts[2]) || 0;
|
|
day = Number(parts[3]) || 0;
|
|
value = new Date(Date.UTC(year, month - 1, day));
|
|
} else {
|
|
parts = value.match(/(\d+)\D+(\d+)(?:\D+(\d+)\b)?/);
|
|
if (!parts) {
|
|
value = null;
|
|
} else {
|
|
day = Number(parts[isUs ? 2 : 1]) || 0;
|
|
month = Number(parts[isUs ? 1 : 2]) || 0;
|
|
year = Number(parts[3]) || 2000;
|
|
|
|
if (!day || !month) {
|
|
value = null;
|
|
} else {
|
|
value = new Date(Date.UTC(year, month - 1, day));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (day && month) {
|
|
if (isUs) {
|
|
formatted = (month < 10 ? '0' : '') + month + '/' + (day < 10 ? '0' : '') + day;
|
|
} else {
|
|
formatted = (day < 10 ? '0' : '') + day + '/' + (month < 10 ? '0' : '') + month;
|
|
}
|
|
|
|
if (isYear) {
|
|
formatted += '/' + year;
|
|
}
|
|
} else {
|
|
formatted = null;
|
|
}
|
|
|
|
let item = {
|
|
id: field.id,
|
|
type: field.type,
|
|
name: field.name,
|
|
column: field.column,
|
|
value: useDate ? value : formatted,
|
|
visible: !!field.visible,
|
|
mergeTag: field.key,
|
|
mergeValue: (useDate ? value : formatted) || field.defaultValue,
|
|
['type' + (field.type || '').toString().trim().replace(/(?:^|-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
|
};
|
|
row.push(item);
|
|
break;
|
|
}
|
|
}
|
|
/* eslint-enable indent */
|
|
});
|
|
|
|
return row;
|
|
};
|
|
|
|
module.exports.getValues = (row, showAll) => {
|
|
let result = [];
|
|
row.filter(field => showAll || field.visible).forEach(field => {
|
|
if (field.column) {
|
|
result.push({
|
|
key: field.column,
|
|
value: field.value
|
|
});
|
|
} else if (field.options) {
|
|
field.options.filter(field => showAll || field.visible).forEach(subField => {
|
|
result.push({
|
|
key: subField.column,
|
|
value: subField.value
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
|
|
function render(template, options) {
|
|
let renderer = Handlebars.compile(template);
|
|
return renderer(options);
|
|
}
|