2017-07-11 09:28:44 +00:00
'use strict' ;
const knex = require ( '../lib/knex' ) ;
2018-08-01 10:00:20 +00:00
const hasher = require ( 'node-object-hash' ) ( ) ;
2017-07-11 09:28:44 +00:00
const dtHelpers = require ( '../lib/dt-helpers' ) ;
2017-07-13 11:27:03 +00:00
const interoperableErrors = require ( '../shared/interoperable-errors' ) ;
2018-07-31 04:34:28 +00:00
const shortid = require ( 'shortid' ) ;
2018-08-01 10:00:20 +00:00
const { enforce , filterObject } = require ( '../lib/helpers' ) ;
2017-08-13 18:11:58 +00:00
const shares = require ( './shares' ) ;
2018-08-01 10:00:20 +00:00
const namespaceHelpers = require ( '../lib/namespace-helpers' ) ;
2018-07-31 04:34:28 +00:00
const files = require ( './files' ) ;
2018-08-02 10:19:27 +00:00
const templates = require ( './templates' ) ;
2018-07-31 04:34:28 +00:00
const { CampaignSource , CampaignType } = require ( '../shared/campaigns' ) ;
const segments = require ( './segments' ) ;
2017-07-11 09:28:44 +00:00
2018-07-31 04:34:28 +00:00
const allowedKeysCommon = [ 'name' , 'description' , 'list' , 'segment' , 'namespace' ,
2018-08-01 10:00:20 +00:00
'send_configuration' , 'from_name_override' , 'from_email_override' , 'reply_to_override' , 'subject_override' , 'data' , 'click_tracking_disabled' , 'open_tracking_disabled' ] ;
2018-07-31 04:34:28 +00:00
2018-08-01 10:00:20 +00:00
const allowedKeysCreate = new Set ( [ 'type' , 'source' , ... allowedKeysCommon ] ) ;
2018-07-31 04:34:28 +00:00
const allowedKeysUpdate = new Set ( [ ... allowedKeysCommon ] ) ;
2018-08-03 11:35:55 +00:00
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 ) ;
2018-08-01 10:00:20 +00:00
}
2018-07-31 04:34:28 +00:00
async function listDTAjax ( context , params ) {
2017-08-13 18:11:58 +00:00
return await dtHelpers . ajaxListWithPermissions (
context ,
[ { entityTypeId : 'campaign' , requiredOperations : [ 'view' ] } ] ,
params ,
builder => builder . from ( 'campaigns' )
. innerJoin ( 'namespaces' , 'namespaces.id' , 'campaigns.namespace' ) ,
2018-07-31 04:34:28 +00:00
[ 'campaigns.id' , 'campaigns.name' , 'campaigns.description' , 'campaigns.type' , 'campaigns.status' , 'campaigns.scheduled' , 'campaigns.source' , 'campaigns.created' , 'namespaces.name' ]
2017-08-13 18:11:58 +00:00
) ;
}
2017-07-13 11:27:03 +00:00
2018-08-03 11:35:55 +00:00
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 ) {
2018-08-02 10:19:27 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , id , 'view' ) ;
2018-08-03 11:35:55 +00:00
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
}
} ;
}
2018-08-01 10:00:20 +00:00
2018-08-02 10:19:27 +00:00
if ( withPermissions ) {
2017-08-13 18:11:58 +00:00
entity . permissions = await shares . getPermissionsTx ( tx , context , 'campaign' , id ) ;
2018-08-02 10:19:27 +00:00
}
return entity ;
}
2018-08-01 10:00:20 +00:00
2018-08-03 11:35:55 +00:00
async function getById ( context , id , withPermissions = true , content = Content . ALL ) {
2018-08-02 10:19:27 +00:00
return await knex . transaction ( async tx => {
2018-08-03 11:35:55 +00:00
return await getByIdTx ( tx , context , id , withPermissions , content ) ;
2017-08-13 18:11:58 +00:00
} ) ;
2017-07-13 11:27:03 +00:00
}
2017-07-11 09:28:44 +00:00
2018-08-03 11:35:55 +00:00
async function _validateAndPreprocess ( tx , context , entity , isCreate , content ) {
if ( content === Content . ALL || content === Content . WITHOUT _SOURCE _CUSTOM ) {
await namespaceHelpers . validateEntity ( tx , entity ) ;
2018-07-31 04:34:28 +00:00
2018-08-03 11:35:55 +00:00
if ( isCreate ) {
enforce ( entity . type === CampaignType . REGULAR || entity . type === CampaignType . RSS || entity . type === CampaignType . TRIGGERED , 'Unknown campaign type' ) ;
2018-08-01 10:00:20 +00:00
2018-08-03 11:35:55 +00:00
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' ) ;
2018-08-01 10:00:20 +00:00
}
2018-08-03 11:35:55 +00:00
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' ) ;
2018-07-31 04:34:28 +00:00
}
2018-08-03 11:35:55 +00:00
}
function convertFileURLs ( sourceCustom , fromEntityType , fromEntityId , toEntityType , toEntityId ) {
2018-07-31 04:34:28 +00:00
2018-08-03 11:35:55 +00:00
function convertText ( text ) {
if ( text ) {
const fromUrl = ` /files/ ${ fromEntityType } /file/ ${ fromEntityId } ` ;
const toUrl = ` /files/ ${ toEntityType } /file/ ${ toEntityId } ` ;
2018-07-31 04:34:28 +00:00
2018-08-03 11:35:55 +00:00
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 ) ;
}
2018-07-31 04:34:28 +00:00
2018-08-03 11:35:55 +00:00
return text ;
2018-07-31 04:34:28 +00:00
}
2018-08-03 11:35:55 +00:00
sourceCustom . html = convertText ( sourceCustom . html ) ;
sourceCustom . text = convertText ( sourceCustom . text ) ;
2018-08-02 10:19:27 +00:00
2018-08-03 11:35:55 +00:00
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 ) ;
}
2018-07-31 04:34:28 +00:00
}
async function create ( context , entity ) {
return await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'namespace' , entity . namespace , 'createCampaign' ) ;
2018-08-02 10:19:27 +00:00
let copyFilesFrom = null ;
2018-07-31 04:34:28 +00:00
if ( entity . source === CampaignSource . CUSTOM _FROM _TEMPLATE ) {
2018-08-02 10:19:27 +00:00
copyFilesFrom = {
entityType : 'template' ,
entityId : entity . data . sourceTemplate
} ;
const template = await templates . getByIdTx ( tx , context , entity . data . sourceTemplate , false ) ;
entity . data . sourceCustom = {
type : template . type ,
data : template . data ,
html : template . html ,
text : template . text
} ;
2018-08-03 11:35:55 +00:00
2018-08-02 10:19:27 +00:00
} else if ( entity . source === CampaignSource . CUSTOM _FROM _CAMPAIGN ) {
copyFilesFrom = {
entityType : 'campaign' ,
entityId : entity . data . sourceCampaign
} ;
const sourceCampaign = await getByIdTx ( tx , context , entity . data . sourceCampaign , false ) ;
2018-08-03 11:35:55 +00:00
enforce ( sourceCampaign . source === CampaignSource . CUSTOM || sourceCampaign . source === CampaignSource . CUSTOM _FROM _TEMPLATE || sourceCampaign . source === CampaignSource . CUSTOM _FROM _CAMPAIGN , 'Incorrect source type of the source campaign.' ) ;
2018-08-02 10:19:27 +00:00
entity . data . sourceCustom = sourceCampaign . data . sourceCustom ;
2018-07-31 04:34:28 +00:00
}
2018-08-03 11:35:55 +00:00
await _validateAndPreprocess ( tx , context , entity , true , Content . ALL ) ;
2018-07-31 04:34:28 +00:00
const filteredEntity = filterObject ( entity , allowedKeysCreate ) ;
filteredEntity . cid = shortid . generate ( ) ;
2018-08-03 11:35:55 +00:00
const data = filteredEntity . data ;
filteredEntity . data = JSON . stringify ( filteredEntity . data ) ;
2018-07-31 04:34:28 +00:00
const ids = await tx ( 'campaigns' ) . insert ( filteredEntity ) ;
const id = ids [ 0 ] ;
await knex . schema . raw ( 'CREATE TABLE `campaign__' + id + '` (\n' +
' `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n' +
' `list` int(10) unsigned NOT NULL,\n' +
' `segment` int(10) unsigned NOT NULL,\n' +
' `subscription` int(10) unsigned NOT NULL,\n' +
' `status` tinyint(4) unsigned NOT NULL DEFAULT \'0\',\n' +
' `response` varchar(255) DEFAULT NULL,\n' +
' `response_id` varchar(255) CHARACTER SET ascii DEFAULT NULL,\n' +
' `updated` timestamp NULL DEFAULT NULL,\n' +
' `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n' +
' PRIMARY KEY (`id`),\n' +
' UNIQUE KEY `list` (`list`,`segment`,`subscription`),\n' +
' KEY `created` (`created`),\n' +
' KEY `response_id` (`response_id`),\n' +
' KEY `status_index` (`status`),\n' +
' KEY `subscription_index` (`subscription`)\n' +
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n' ) ;
await knex . schema . raw ( 'CREATE TABLE `campaign__tracker' + id + '` (\n' +
' `list` int(10) unsigned NOT NULL,\n' +
' `subscriber` int(10) unsigned NOT NULL,\n' +
' `link` int(10) NOT NULL,\n' +
' `ip` varchar(100) CHARACTER SET ascii DEFAULT NULL,\n' +
' `device_type` varchar(50) DEFAULT NULL,\n' +
' `country` varchar(2) CHARACTER SET ascii DEFAULT NULL,\n' +
' `count` int(11) unsigned NOT NULL DEFAULT \'1\',\n' +
' `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n' +
' PRIMARY KEY (`list`,`subscriber`,`link`),\n' +
' KEY `created_index` (`created`)\n' +
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n' ) ;
await shares . rebuildPermissionsTx ( tx , { entityTypeId : 'campaign' , entityId : id } ) ;
2018-08-02 10:19:27 +00:00
if ( copyFilesFrom ) {
2018-08-03 11:35:55 +00:00
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 ) ;
2018-07-31 04:34:28 +00:00
}
return id ;
} ) ;
}
2018-08-03 11:35:55 +00:00
async function updateWithConsistencyCheck ( context , entity , content ) {
2018-07-31 04:34:28 +00:00
await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , entity . id , 'edit' ) ;
const existing = await tx ( 'campaigns' ) . where ( 'id' , entity . id ) . first ( ) ;
if ( ! existing ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
2018-08-01 10:00:20 +00:00
existing . data = JSON . parse ( existing . data ) ;
2018-08-03 11:35:55 +00:00
const existingHash = hash ( existing , content ) ;
2018-07-31 04:34:28 +00:00
if ( existingHash !== entity . originalHash ) {
throw new interoperableErrors . ChangedError ( ) ;
}
2018-08-03 11:35:55 +00:00
await _validateAndPreprocess ( tx , context , entity , false , content ) ;
2018-07-31 04:34:28 +00:00
2018-08-03 11:35:55 +00:00
let filteredEntity = filterObject ( entity , allowedKeysUpdate ) ;
if ( content === Content . ALL ) {
await namespaceHelpers . validateMove ( context , entity , existing , 'campaign' , 'createCampaign' , 'delete' ) ;
} 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
} ;
}
2018-07-31 04:34:28 +00:00
2018-08-03 11:35:55 +00:00
filteredEntity . data = JSON . stringify ( filteredEntity . data ) ;
await tx ( 'campaigns' ) . where ( 'id' , entity . id ) . update ( filteredEntity ) ;
2018-07-31 04:34:28 +00:00
await shares . rebuildPermissionsTx ( tx , { entityTypeId : 'campaign' , entityId : entity . id } ) ;
} ) ;
}
async function remove ( context , id ) {
await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , id , 'delete' ) ;
2018-08-01 10:00:20 +00:00
// FIXME - deal with deletion of dependent entities (files)
2018-07-31 04:34:28 +00:00
await tx ( 'campaigns' ) . where ( 'id' , id ) . del ( ) ;
await knex . schema . dropTableIfExists ( 'campaign__' + id ) ;
await knex . schema . dropTableIfExists ( 'campaign_tracker__' + id ) ;
} ) ;
}
2017-07-11 09:28:44 +00:00
module . exports = {
2018-08-03 11:35:55 +00:00
Content ,
2018-08-01 10:00:20 +00:00
hash ,
2017-07-13 11:27:03 +00:00
listDTAjax ,
2018-08-03 11:35:55 +00:00
listWithContentDTAjax ,
2018-08-02 10:19:27 +00:00
getByIdTx ,
2018-08-01 10:00:20 +00:00
getById ,
create ,
updateWithConsistencyCheck ,
remove
2017-07-11 09:28:44 +00:00
} ;