Added CSV export of subscribers

Fixed some bugs in subscriptions
Updated some packages to avoid warnings about vulnerabilities
Completed RSS feed campaigns
This commit is contained in:
Tomas Bures 2018-11-17 02:54:23 +01:00
parent 8683f8c91e
commit bf69e633c4
47 changed files with 5255 additions and 9651 deletions

View file

@ -22,6 +22,7 @@ const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
const allowedKeysCreateRssEntry = new Set(['type', 'source', 'parent', ...allowedKeysCommon]);
const allowedKeysUpdate = new Set([...allowedKeysCommon]);
const Content = {
@ -32,7 +33,6 @@ const Content = {
SETTINGS_WITH_STATS: 4
};
function hash(entity, content) {
let filteredEntity;
@ -63,11 +63,25 @@ async function listDTAjax(context, params) {
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
params,
builder => builder.from('campaigns')
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace'),
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
.whereNull('campaigns.parent'),
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
);
}
async function listChildrenDTAjax(context, campaignId, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
params,
builder => builder.from('campaigns')
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
.where('campaigns.parent', campaignId),
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
);
}
async function listWithContentDTAjax(context, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
@ -368,12 +382,21 @@ async function _createTx(tx, context, entity, content) {
await _validateAndPreprocess(tx, context, entity, true, content);
const filteredEntity = filterObject(entity, allowedKeysCreate);
const filteredEntity = filterObject(entity, entity.type === CampaignType.RSS_ENTRY ? allowedKeysCreateRssEntry : allowedKeysCreate);
filteredEntity.cid = shortid.generate();
const data = filteredEntity.data;
filteredEntity.data = JSON.stringify(filteredEntity.data);
if (filteredEntity.type === CampaignType.RSS || filteredEntity.type === CampaignType.TRIGGERED) {
filteredEntity.status = CampaignStatus.ACTIVE;
} else if (filteredEntity.type === CampaignType.RSS_ENTRY) {
filteredEntity.status = CampaignStatus.SCHEDULED;
} else {
filteredEntity.status = CampaignStatus.IDLE;
}
const ids = await tx('campaigns').insert(filteredEntity);
const id = ids[0];
@ -386,7 +409,11 @@ async function _createTx(tx, context, entity, content) {
});
}
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
if (filteredEntity.parent) {
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id, parentId: filteredEntity.parent });
} else {
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
}
if (copyFilesFrom) {
await files.copyAllTx(tx, context, copyFilesFrom.entityType, 'file', copyFilesFrom.entityId, 'campaign', 'file', id);
@ -459,29 +486,44 @@ async function updateWithConsistencyCheck(context, entity, content) {
});
}
async function _removeTx(tx, context, id, existing = null) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete');
if (!existing) {
existing = await tx('campaigns').where('id', id).select(['id', 'status', 'type']).first();
}
if (existing.status === CampaignStatus.SENDING) {
return new interoperableErrors.InvalidStateError;
}
enforce(existing.type === CampaignType.REGULAR || existing.type === CampaignType.RSS || existing.type === CampaignType.TRIGGERED, 'This campaign cannot be removed by user.');
const childCampaigns = await tx('campaigns').where('parent', id).select(['id', 'status', 'type']);
for (const childCampaign of childCampaigns) {
await _removeTx(tx, contect, childCampaign.id, childCampaign);
}
await files.removeAllTx(tx, context, 'campaign', 'file', id);
await files.removeAllTx(tx, context, 'campaign', 'attachment', id);
await tx('campaign_lists').where('campaign', id).del();
await tx('campaign_messages').where('campaign', id).del();
await tx('campaign_links').where('campaign', id).del();
await triggers.removeAllByCampaignIdTx(tx, context, id);
await tx('template_dep_campaigns')
.where('campaign', id)
.del();
await tx('campaigns').where('id', id).del();
}
async function remove(context, id) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete');
const existing = tx('campaigns').where('id', id);
if (existing.status === CampaignStatus.SENDING) {
return new interoperableErrors.InvalidStateError;
}
await files.removeAllTx(tx, context, 'campaign', 'file', id);
await files.removeAllTx(tx, context, 'campaign', 'attachment', id);
await tx('campaign_lists').where('campaign', id).del();
await tx('campaign_messages').where('campaign', id).del();
await tx('campaign_links').where('campaign', id).del();
await triggers.removeAllByCampaignIdTx(tx, context, id);
await tx('template_dep_campaigns')
.where('campaign', id)
.del();
await tx('campaigns').where('id', id).del();
await _removeTx(tx, context, id);
});
}
@ -705,8 +747,6 @@ async function _changeStatus(context, campaignId, permittedCurrentStates, newSta
throw new interoperableErrors.InvalidStateError(invalidStateMessage);
}
console.log(scheduled);
await tx('campaigns').where('id', campaignId).update({
status: newState,
scheduled
@ -747,9 +787,20 @@ async function reset(context, campaignId) {
});
}
async function enable(context, campaignId) {
await _changeStatus(context, campaignId, [CampaignStatus.INACTIVE], CampaignStatus.ACTIVE, 'Cannot enable campaign unless it is in INACTIVE state');
}
async function disable(context, campaignId) {
await _changeStatus(context, campaignId, [CampaignStatus.ACTIVE], CampaignStatus.INACTIVE, 'Cannot disable campaign unless it is in ACTIVE state');
}
module.exports.Content = Content;
module.exports.hash = hash;
module.exports.listDTAjax = listDTAjax;
module.exports.listChildrenDTAjax = listChildrenDTAjax;
module.exports.listWithContentDTAjax = listWithContentDTAjax;
module.exports.listOthersWhoseListsAreIncludedDTAjax = listOthersWhoseListsAreIncludedDTAjax;
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
@ -774,6 +825,8 @@ module.exports.getSubscribersQueryGeneratorTx = getSubscribersQueryGeneratorTx;
module.exports.start = start;
module.exports.stop = stop;
module.exports.reset = reset;
module.exports.enable = enable;
module.exports.disable = disable;
module.exports.rawGetByTx = rawGetByTx;
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;

View file

@ -694,10 +694,11 @@ async function forHbs(context, listId, subscription) { // assumes grouped subscr
return forHbsWithFieldsGrouped(flds, subscription);
}
function getMergeTags(fieldsGrouped, subscription) { // assumes grouped subscription
function getMergeTags(fieldsGrouped, subscription, extraTags = {}) { // assumes grouped subscription
const mergeTags = {
'EMAIL': subscription.email,
...getMergeTagsForBases(getTrustedUrl(), getSandboxUrl(), getPublicUrl())
...getMergeTagsForBases(getTrustedUrl(), getSandboxUrl(), getPublicUrl()),
...extraTags
};
for (const fld of fieldsGrouped) {

View file

@ -195,24 +195,6 @@ async function remove(context, id) {
});
}
async function getMergeTags(context, id) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', id, ['view']);
const groupedFields = await fields.listGroupedTx(tx, id);
const mergeTags = [];
for (const field of groupedFields) {
mergeTags.push({
key: field.key,
value: field.name
});
}
return mergeTags;
});
}
module.exports.UnsubscriptionMode = UnsubscriptionMode;
module.exports.hash = hash;
@ -226,4 +208,3 @@ module.exports.getByCid = getByCid;
module.exports.create = create;
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
module.exports.remove = remove;
module.exports.getMergeTags = getMergeTags;

View file

@ -20,7 +20,7 @@ function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys));
}
async function getByIdWithTemplate(context, id) {
async function getByIdWithTemplate(context, id, withPermissions = true) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'report', id, 'view');
@ -33,7 +33,9 @@ async function getByIdWithTemplate(context, id) {
entity.user_fields = JSON.parse(entity.user_fields);
entity.params = JSON.parse(entity.params);
entity.permissions = await shares.getPermissionsTx(tx, context, 'report', id);
if (withPermissions) {
entity.permissions = await shares.getPermissionsTx(tx, context, 'report', id);
}
return entity;
});

View file

@ -100,7 +100,14 @@ async function assign(context, entityTypeId, entityId, userId, role) {
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id');
enforce(await tx(entityType.entitiesTable).where('id', entityId).select('id').first(), 'Invalid entity id');
const extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
const entity = await tx(entityType.entitiesTable).where('id', entityId).select(['id', ...extraColumns]).first();
enforce(entity, 'Invalid entity id');
if (entityType.dependentPermissions) {
enforce(!entityType.dependentPermissions.getParent(entity), 'Cannot share/unshare a dependent entity');
}
const entry = await tx(entityType.sharesTable).where({user: userId, entity: entityId}).select('role').first();
@ -310,13 +317,51 @@ async function rebuildPermissionsTx(tx, restriction) {
}
await expungeQuery;
const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace']);
const extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace', ...extraColumns]);
const notToBeInserted = new Set();
if (restriction.entityId) {
entitiesQuery.where('id', restriction.entityId);
if (restriction.parentId) {
notToBeInserted.add(restriction.parentId);
entitiesQuery.whereIn('id', [restriction.entityId, restriction.parentId]);
} else {
entitiesQuery.where('id', restriction.entityId);
}
}
const entities = await entitiesQuery;
for (const entity of entities) {
// TODO - process restriction.parentId
const parentEntities = new Map();
let nonChildEntities;
if (entityType.dependentPermissions) {
nonChildEntities = [];
for (const entity of entities) {
const parent = entityType.dependentPermissions.getParent(entity);
if (parent) {
let childEntities;
if (parentEntities.has(parent)) {
childEntities = parentEntities.get(parent);
} else {
childEntities = [];
parentEntities.set(parent, childEntities);
}
childEntities.push(entity.id);
} else {
nonChildEntities.push(entity);
}
}
} else {
nonChildEntities = entities;
}
for (const entity of nonChildEntities) {
const permsPerUser = new Map();
if (entity.namespace) { // The root namespace has not parent namespace, thus the test
@ -350,15 +395,37 @@ async function rebuildPermissionsTx(tx, restriction) {
}
}
for (const userPermsPair of permsPerUser.entries()) {
const data = [];
if (!notToBeInserted.has(entity.id)) {
for (const userPermsPair of permsPerUser.entries()) {
const data = [];
for (const operation of userPermsPair[1]) {
data.push({user: userPermsPair[0], entity: entity.id, operation});
for (const operation of userPermsPair[1]) {
data.push({user: userPermsPair[0], entity: entity.id, operation});
}
if (data.length > 0) {
await tx(entityType.permissionsTable).insert(data);
}
}
}
if (data.length > 0) {
await tx(entityType.permissionsTable).insert(data);
if (parentEntities.has(entity.id)) {
const childEntities = parentEntities.get(entity.id);
for (const childId of childEntities) {
for (const userPermsPair of permsPerUser.entries()) {
const data = [];
for (const operation of userPermsPair[1]) {
if (operation !== 'share') {
data.push({user: userPermsPair[0], entity: childId, operation});
}
}
if (data.length > 0) {
await tx(entityType.permissionsTable).insert(data);
}
}
}
}
}

View file

@ -81,7 +81,7 @@ function getSubscriptionTableName(listId) {
return `subscription__${listId}`;
}
async function getGroupedFieldsMap(tx, listId) {
async function getGroupedFieldsMapTx(tx, listId) {
const groupedFields = await fields.listGroupedTx(tx, listId);
const result = {};
for (const fld of groupedFields) {
@ -189,7 +189,7 @@ function hashByAllowedKeys(allowedKeys, entity) {
async function hashByList(listId, entity) {
return await knex.transaction(async tx => {
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
const allowedKeys = getAllowedKeys(groupedFieldsMap);
return hashByAllowedKeys(allowedKeys, entity);
});
@ -204,7 +204,7 @@ async function _getByTx(tx, context, listId, key, value, grouped) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
if (grouped) {
groupSubscription(groupedFieldsMap, entity);
@ -248,7 +248,7 @@ async function listDTAjax(context, listId, segmentId, params) {
// to group the fields. Then we copy relevant values form grouped subscription to ajaxList's data which then get
// returned to the client. During the copy, we also render the values.
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
const listFlds = await fields.listByOrderListTx(tx, listId, ['column', 'id']);
const columns = [
@ -387,7 +387,7 @@ async function list(context, listId, grouped = true, offset, limit) {
const entities = await entitiesQry;
if (grouped) {
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
for (const entity of entities) {
groupSubscription(groupedFieldsMap, entity);
@ -401,6 +401,48 @@ async function list(context, listId, grouped = true, offset, limit) {
});
}
// Note that this does not do all the work in the transaction. Thus it is prone to fail if the list is deleted in during the run of the function
async function* listIterator(context, listId, segmentId, grouped = true) {
let groupedFieldsMap;
let addSegmentQuery;
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
if (grouped) {
groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
}
addSegmentQuery = segmentId ? await segments.getQueryGeneratorTx(tx, listId, segmentId) : () => {};
});
let lastId = 0;
while (true) {
const entities = await knex(getSubscriptionTableName(listId))
.orderBy('id', 'asc')
.where('id', '>', lastId)
.where(function() {
addSegmentQuery(this);
})
.limit(500);
if (entities.length > 0) {
for (const entity of entities) {
if (grouped) {
groupSubscription(groupedFieldsMap, entity);
}
yield entity;
}
lastId = entities[entities.length - 1].id;
} else {
break;
}
}
}
async function serverValidate(context, listId, data) {
return await knex.transaction(async tx => {
const result = {};
@ -563,7 +605,7 @@ async function createTxWithGroupedFieldsMap(tx, context, listId, groupedFieldsMa
async function create(context, listId, entity, source, meta) {
return await knex.transaction(async tx => {
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
return await createTxWithGroupedFieldsMap(tx, context, listId, groupedFieldsMap, entity, source, meta);
});
}
@ -577,7 +619,7 @@ async function updateWithConsistencyCheck(context, listId, entity, source) {
throw new interoperableErrors.NotFoundError();
}
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
const allowedKeys = getAllowedKeys(groupedFieldsMap);
groupSubscription(groupedFieldsMap, existing);
@ -718,7 +760,7 @@ async function updateManaged(context, listId, cid, entity) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
const update = {};
for (const key in groupedFieldsMap) {
@ -764,11 +806,12 @@ module.exports.getByCidTx = getByCidTx;
module.exports.getByCid = getByCid;
module.exports.getByEmail = getByEmail;
module.exports.list = list;
module.exports.listIterator = listIterator;
module.exports.listDTAjax = listDTAjax;
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
module.exports.serverValidate = serverValidate;
module.exports.create = create;
module.exports.getGroupedFieldsMap = getGroupedFieldsMap;
module.exports.getGroupedFieldsMapTx = getGroupedFieldsMapTx;
module.exports.createTxWithGroupedFieldsMap = createTxWithGroupedFieldsMap;
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
module.exports.remove = remove;