Editing of campaigns seems to work
This commit is contained in:
parent
b1c667d13d
commit
7b46c4b4b0
27 changed files with 335 additions and 130 deletions
|
@ -19,8 +19,32 @@ const allowedKeysCommon = ['name', 'description', 'list', 'segment', 'namespace'
|
|||
const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
|
||||
const allowedKeysUpdate = new Set([...allowedKeysCommon]);
|
||||
|
||||
function hash(entity) {
|
||||
return hasher.hash(filterObject(entity, allowedKeysUpdate));
|
||||
const Content = {
|
||||
ALL: 0,
|
||||
WITHOUT_SOURCE_CUSTOM: 1,
|
||||
ONLY_SOURCE_CUSTOM: 2
|
||||
};
|
||||
|
||||
function hash(entity, content) {
|
||||
let filteredEntity;
|
||||
|
||||
if (content === Content.ALL) {
|
||||
filteredEntity = filterObject(entity, allowedKeysUpdate);
|
||||
|
||||
} else if (content === Content.WITHOUT_SOURCE_CUSTOM) {
|
||||
filteredEntity = filterObject(entity, allowedKeysUpdate);
|
||||
filteredEntity.data = {...filteredEntity.data};
|
||||
delete filteredEntity.data.sourceCustom;
|
||||
|
||||
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
|
||||
filteredEntity = {
|
||||
data: {
|
||||
sourceCustom: entity.data.sourceCustom
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return hasher.hash(filteredEntity);
|
||||
}
|
||||
|
||||
async function listDTAjax(context, params) {
|
||||
|
@ -34,48 +58,103 @@ async function listDTAjax(context, params) {
|
|||
);
|
||||
}
|
||||
|
||||
async function getByIdTx(tx, context, id, withPermissions = true) {
|
||||
async function listWithContentDTAjax(context, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
||||
params,
|
||||
builder => builder.from('campaigns')
|
||||
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
|
||||
.whereIn('campaigns.source', [CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_TEMPLATE, CampaignSource.CUSTOM_FROM_CAMPAIGN]),
|
||||
['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.type', 'campaigns.created', 'namespaces.name']
|
||||
);
|
||||
}
|
||||
|
||||
async function getByIdTx(tx, context, id, withPermissions = true, content = Content.ALL) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'view');
|
||||
const entity = await tx('campaigns').where('id', id).first();
|
||||
let entity = await tx('campaigns').where('id', id).first();
|
||||
|
||||
entity.data = JSON.parse(entity.data);
|
||||
|
||||
if (content === Content.WITHOUT_SOURCE_CUSTOM) {
|
||||
delete entity.data.sourceCustom;
|
||||
|
||||
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
|
||||
entity = {
|
||||
id: entity.id,
|
||||
|
||||
data: {
|
||||
sourceCustom: entity.data.sourceCustom
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (withPermissions) {
|
||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'campaign', id);
|
||||
}
|
||||
|
||||
entity.data = JSON.parse(entity.data);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
async function getById(context, id, withPermissions = true) {
|
||||
async function getById(context, id, withPermissions = true, content = Content.ALL) {
|
||||
return await knex.transaction(async tx => {
|
||||
return await getByIdTx(tx, context, id, withPermissions);
|
||||
return await getByIdTx(tx, context, id, withPermissions, content);
|
||||
});
|
||||
}
|
||||
|
||||
async function _validateAndPreprocess(tx, context, entity, isCreate) {
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
async function _validateAndPreprocess(tx, context, entity, isCreate, content) {
|
||||
if (content === Content.ALL || content === Content.WITHOUT_SOURCE_CUSTOM) {
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
||||
if (isCreate) {
|
||||
enforce(entity.type === CampaignType.REGULAR || entity.type === CampaignType.RSS || entity.type === CampaignType.TRIGGERED, 'Unknown campaign type');
|
||||
if (isCreate) {
|
||||
enforce(entity.type === CampaignType.REGULAR || entity.type === CampaignType.RSS || entity.type === CampaignType.TRIGGERED, 'Unknown campaign type');
|
||||
|
||||
if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
|
||||
if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
|
||||
}
|
||||
|
||||
enforce(Number.isInteger(entity.source));
|
||||
enforce(entity.source >= CampaignSource.MIN && entity.source <= CampaignSource.MAX, 'Unknown campaign source');
|
||||
}
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', entity.list, 'view');
|
||||
|
||||
if (entity.segment) {
|
||||
// Check that the segment under the list exists
|
||||
await segments.getByIdTx(tx, context, entity.list, entity.segment);
|
||||
}
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', entity.send_configuration, 'viewPublic');
|
||||
}
|
||||
}
|
||||
|
||||
function convertFileURLs(sourceCustom, fromEntityType, fromEntityId, toEntityType, toEntityId) {
|
||||
|
||||
function convertText(text) {
|
||||
if (text) {
|
||||
const fromUrl = `/files/${fromEntityType}/file/${fromEntityId}`;
|
||||
const toUrl = `/files/${toEntityType}/file/${toEntityId}`;
|
||||
|
||||
const encodedFromUrl = encodeURIComponent(fromUrl);
|
||||
const encodedToUrl = encodeURIComponent(toUrl);
|
||||
|
||||
text = text.split('[URL_BASE]' + fromUrl).join('[URL_BASE]' + toUrl);
|
||||
text = text.split('[SANDBOX_URL_BASE]' + fromUrl).join('[SANDBOX_URL_BASE]' + toUrl);
|
||||
text = text.split('[ENCODED_URL_BASE]' + encodedFromUrl).join('[ENCODED_URL_BASE]' + encodedToUrl);
|
||||
text = text.split('[ENCODED_SANDBOX_URL_BASE]' + encodedFromUrl).join('[ENCODED_SANDBOX_URL_BASE]' + encodedToUrl);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
enforce(entity.source >= CampaignSource.MIN && entity.source <= CampaignSource.MAX, 'Unknown campaign source');
|
||||
sourceCustom.html = convertText(sourceCustom.html);
|
||||
sourceCustom.text = convertText(sourceCustom.text);
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', entity.list, 'view');
|
||||
|
||||
if (entity.segment) {
|
||||
// Check that the segment under the list exists
|
||||
await segments.getByIdTx(tx, context, entity.list, entity.segment);
|
||||
if (sourceCustom.type === 'mosaico' || sourceCustom.type === 'mosaicoWithFsTemplate') {
|
||||
sourceCustom.data.model = convertText(sourceCustom.data.model);
|
||||
sourceCustom.data.model = convertText(sourceCustom.data.model);
|
||||
sourceCustom.data.metadata = convertText(sourceCustom.data.metadata);
|
||||
}
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'send_configuration', entity.send_configuration, 'viewPublic');
|
||||
|
||||
entity.data = JSON.stringify(entity.data);
|
||||
}
|
||||
|
||||
async function create(context, entity) {
|
||||
|
@ -97,6 +176,7 @@ async function create(context, entity) {
|
|||
html: template.html,
|
||||
text: template.text
|
||||
};
|
||||
|
||||
} else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
|
||||
copyFilesFrom = {
|
||||
entityType: 'campaign',
|
||||
|
@ -104,15 +184,19 @@ async function create(context, entity) {
|
|||
};
|
||||
|
||||
const sourceCampaign = await getByIdTx(tx, context, entity.data.sourceCampaign, false);
|
||||
enforce(sourceCampaign.source === CampaignSource.CUSTOM || sourceCampaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceCampaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN, 'Incorrect source type of the source campaign.');
|
||||
|
||||
entity.data.sourceCustom = sourceCampaign.data.sourceCustom;
|
||||
}
|
||||
|
||||
await _validateAndPreprocess(tx, context, entity, true);
|
||||
await _validateAndPreprocess(tx, context, entity, true, Content.ALL);
|
||||
|
||||
const filteredEntity = filterObject(entity, allowedKeysCreate);
|
||||
filteredEntity.cid = shortid.generate();
|
||||
|
||||
const data = filteredEntity.data;
|
||||
|
||||
filteredEntity.data = JSON.stringify(filteredEntity.data);
|
||||
const ids = await tx('campaigns').insert(filteredEntity);
|
||||
const id = ids[0];
|
||||
|
||||
|
@ -150,14 +234,20 @@ async function create(context, entity) {
|
|||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
|
||||
|
||||
if (copyFilesFrom) {
|
||||
await files.copyAllTx(tx, context, copyFilesFrom.entityType, copyFilesFrom.entityId, 'campaign', id);
|
||||
await files.copyAllTx(tx, context, copyFilesFrom.entityType, 'file', copyFilesFrom.entityId, 'campaign', 'file', id);
|
||||
|
||||
convertFileURLs(data.sourceCustom, copyFilesFrom.entityType, copyFilesFrom.entityId, 'campaign', id);
|
||||
await tx('campaigns')
|
||||
.update({
|
||||
data: JSON.stringify(data)
|
||||
}).where('id', id);
|
||||
}
|
||||
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, entity) {
|
||||
async function updateWithConsistencyCheck(context, entity, content) {
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.id, 'edit');
|
||||
|
||||
|
@ -167,16 +257,31 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
}
|
||||
|
||||
existing.data = JSON.parse(existing.data);
|
||||
const existingHash = hash(existing);
|
||||
const existingHash = hash(existing, content);
|
||||
if (existingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
await _validateAndPreprocess(tx, context, entity, false);
|
||||
await _validateAndPreprocess(tx, context, entity, false, content);
|
||||
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'campaign', 'createCampaign', 'delete');
|
||||
let filteredEntity = filterObject(entity, allowedKeysUpdate);
|
||||
if (content === Content.ALL) {
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'campaign', 'createCampaign', 'delete');
|
||||
|
||||
await tx('campaigns').where('id', entity.id).update(filterObject(entity, allowedKeysUpdate));
|
||||
} else if (content === Content.WITHOUT_SOURCE_CUSTOM) {
|
||||
filteredEntity.data.sourceCustom = existing.data.sourceCustom;
|
||||
await namespaceHelpers.validateMove(context, filteredEntity, existing, 'campaign', 'createCampaign', 'delete');
|
||||
|
||||
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
|
||||
const data = existing.data;
|
||||
data.sourceCustom = filteredEntity.data.sourceCustom;
|
||||
filteredEntity = {
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
filteredEntity.data = JSON.stringify(filteredEntity.data);
|
||||
await tx('campaigns').where('id', entity.id).update(filteredEntity);
|
||||
|
||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: entity.id });
|
||||
});
|
||||
|
@ -196,8 +301,10 @@ async function remove(context, id) {
|
|||
|
||||
|
||||
module.exports = {
|
||||
Content,
|
||||
hash,
|
||||
listDTAjax,
|
||||
listWithContentDTAjax,
|
||||
getByIdTx,
|
||||
getById,
|
||||
create,
|
||||
|
|
|
@ -7,22 +7,18 @@ const shares = require('./shares');
|
|||
const fs = require('fs-extra-promise');
|
||||
const path = require('path');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
const permissions = require('../lib/permissions');
|
||||
const entitySettings = require('../lib/entity-settings');
|
||||
const {getTrustedUrl} = require('../lib/urls');
|
||||
|
||||
const crypto = require('crypto');
|
||||
const bluebird = require('bluebird');
|
||||
const cryptoPseudoRandomBytes = bluebird.promisify(crypto.pseudoRandomBytes);
|
||||
|
||||
const entityTypes = permissions.getEntityTypes();
|
||||
const entityTypes = entitySettings.getEntityTypes();
|
||||
|
||||
const filesDir = path.join(__dirname, '..', 'files');
|
||||
|
||||
const ReplacementBehavior = {
|
||||
NONE: 0,
|
||||
REPLACE: 1,
|
||||
RENAME: 2
|
||||
};
|
||||
const ReplacementBehavior = entitySettings.ReplacementBehavior;
|
||||
|
||||
function enforceTypePermitted(type, subType) {
|
||||
enforce(type in entityTypes && entityTypes[type].files && entityTypes[type].files[subType]);
|
||||
|
@ -108,10 +104,26 @@ async function getFileByFilename(context, type, subType, entityId, name) {
|
|||
return await _getFileBy(context, type, subType, entityId, 'filename', name)
|
||||
}
|
||||
|
||||
async function getFileByUrl(context, type, subType, entityId, url) {
|
||||
const urlPrefix = getTrustedUrl(`files/${type}/${subType}/${entityId}/`, context);
|
||||
async function getFileByUrl(context, url) {
|
||||
const urlPrefix = getTrustedUrl('files/', context);
|
||||
if (url.startsWith(urlPrefix)) {
|
||||
const name = url.substring(urlPrefix.length);
|
||||
const path = url.substring(urlPrefix.length);
|
||||
const pathElem = path.split('/');
|
||||
|
||||
if (pathElem.length !== 4) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const type = pathElem[0];
|
||||
const subType = pathElem[1];
|
||||
const entityId = Number.parseInt(pathElem[2]);
|
||||
|
||||
if (Number.isNaN(entityId)) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const name = pathElem[3];
|
||||
|
||||
return await getFileByFilename(context, type, subType, entityId, name);
|
||||
} else {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
|
@ -126,6 +138,10 @@ async function createFiles(context, type, subType, entityId, files, replacementB
|
|||
return {uploaded: 0};
|
||||
}
|
||||
|
||||
if (!replacementBehavior) {
|
||||
replacementBehavior = entityTypes[type].files[subType].defaultReplacementBehavior;
|
||||
}
|
||||
|
||||
const fileEntities = [];
|
||||
const filesToMove = [];
|
||||
const ignoredFiles = [];
|
||||
|
@ -280,7 +296,9 @@ async function copyAllTx(tx, context, fromType, fromSubType, fromEntityId, toTyp
|
|||
row.entity = toEntityId;
|
||||
}
|
||||
|
||||
await tx(getFilesTable(toType, toSubType)).insert(rows);
|
||||
if (rows.length > 0) {
|
||||
await tx(getFilesTable(toType, toSubType)).insert(rows);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const hasher = require('node-object-hash')();
|
|||
const { enforce, filterObject } = require('../lib/helpers');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
const shares = require('./shares');
|
||||
const permissions = require('../lib/permissions');
|
||||
const entitySettings = require('../lib/entity-settings');
|
||||
const namespaceHelpers = require('../lib/namespace-helpers');
|
||||
|
||||
|
||||
|
@ -14,7 +14,7 @@ const allowedKeys = new Set(['name', 'description', 'namespace']);
|
|||
async function listTree(context) {
|
||||
// FIXME - process permissions
|
||||
|
||||
const entityType = permissions.getEntityType('namespace');
|
||||
const entityType = entitySettings.getEntityType('namespace');
|
||||
|
||||
// This builds a forest of namespaces that contains only those namespace that the user has access to
|
||||
// This goes in three steps: 1) tree with all namespaces is built with parent-children links, 2) the namespaces that are not accessible
|
||||
|
|
|
@ -4,7 +4,7 @@ const knex = require('../lib/knex');
|
|||
const config = require('config');
|
||||
const { enforce } = require('../lib/helpers');
|
||||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const permissions = require('../lib/permissions');
|
||||
const entitySettings = require('../lib/entity-settings');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
const log = require('npmlog');
|
||||
const {getGlobalNamespaceId} = require('../shared/namespaces');
|
||||
|
@ -15,7 +15,7 @@ const {getGlobalNamespaceId} = require('../shared/namespaces');
|
|||
|
||||
async function listByEntityDTAjax(context, entityTypeId, entityId, params) {
|
||||
return await knex.transaction(async (tx) => {
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
const entityType = entitySettings.getEntityType(entityTypeId);
|
||||
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
|
||||
|
||||
return await dtHelpers.ajaxListTx(
|
||||
|
@ -41,7 +41,7 @@ async function listByUserDTAjax(context, entityTypeId, userId, params) {
|
|||
|
||||
await enforceEntityPermissionTx(tx, context, 'namespace', user.namespace, 'manageUsers');
|
||||
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
const entityType = entitySettings.getEntityType(entityTypeId);
|
||||
|
||||
return await dtHelpers.ajaxListWithPermissionsTx(
|
||||
tx,
|
||||
|
@ -61,7 +61,7 @@ async function listByUserDTAjax(context, entityTypeId, userId, params) {
|
|||
|
||||
async function listUnassignedUsersDTAjax(context, entityTypeId, entityId, params) {
|
||||
return await knex.transaction(async (tx) => {
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
const entityType = entitySettings.getEntityType(entityTypeId);
|
||||
|
||||
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
|
||||
|
||||
|
@ -93,7 +93,7 @@ async function listRolesDTAjax(entityTypeId, params) {
|
|||
}
|
||||
|
||||
async function assign(context, entityTypeId, entityId, userId, role) {
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
const entityType = entitySettings.getEntityType(entityTypeId);
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
|
||||
|
@ -129,17 +129,17 @@ async function assign(context, entityTypeId, entityId, userId, role) {
|
|||
async function rebuildPermissionsTx(tx, restriction) {
|
||||
restriction = restriction || {};
|
||||
|
||||
const namespaceEntityType = permissions.getEntityType('namespace');
|
||||
const namespaceEntityType = entitySettings.getEntityType('namespace');
|
||||
|
||||
// Collect entity types we care about
|
||||
let restrictedEntityTypes;
|
||||
if (restriction.entityTypeId) {
|
||||
const entityType = permissions.getEntityType(restriction.entityTypeId);
|
||||
const entityType = entitySettings.getEntityType(restriction.entityTypeId);
|
||||
restrictedEntityTypes = {
|
||||
[restriction.entityTypeId]: entityType
|
||||
};
|
||||
} else {
|
||||
restrictedEntityTypes = permissions.getEntityTypes();
|
||||
restrictedEntityTypes = entitySettings.getEntityTypes();
|
||||
}
|
||||
|
||||
|
||||
|
@ -374,7 +374,7 @@ async function regenerateRoleNamesTable() {
|
|||
await knex.transaction(async tx => {
|
||||
await tx('generated_role_names').del();
|
||||
|
||||
const entityTypeIds = ['global', ...Object.keys(permissions.getEntityTypes())];
|
||||
const entityTypeIds = ['global', ...Object.keys(entitySettings.getEntityTypes())];
|
||||
|
||||
for (const entityTypeId of entityTypeIds) {
|
||||
const roles = config.roles[entityTypeId];
|
||||
|
@ -397,7 +397,7 @@ function throwPermissionDenied() {
|
|||
}
|
||||
|
||||
async function removeDefaultShares(tx, user) {
|
||||
const namespaceEntityType = permissions.getEntityType('namespace');
|
||||
const namespaceEntityType = entitySettings.getEntityType('namespace');
|
||||
|
||||
const roleConf = config.roles.global[user.role];
|
||||
|
||||
|
@ -467,7 +467,7 @@ async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredO
|
|||
return false;
|
||||
}
|
||||
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
const entityType = entitySettings.getEntityType(entityTypeId);
|
||||
|
||||
if (typeof requiredOperations === 'string') {
|
||||
requiredOperations = [ requiredOperations ];
|
||||
|
@ -603,7 +603,7 @@ async function getPermissionsTx(tx, context, entityTypeId, entityId) {
|
|||
|
||||
enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');
|
||||
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
const entityType = entitySettings.getEntityType(entityTypeId);
|
||||
|
||||
const rows = await tx(entityType.permissionsTable)
|
||||
.select('operation')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue