{canDelete &&
-
+ {sourceEdit &&
}
{sourceEdit}
diff --git a/client/src/campaigns/Content.js b/client/src/campaigns/Content.js
new file mode 100644
index 00000000..135fdf4f
--- /dev/null
+++ b/client/src/campaigns/Content.js
@@ -0,0 +1,195 @@
+'use strict';
+
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
+import {translate} from 'react-i18next';
+import {
+ requiresAuthenticatedUser,
+ Title,
+ withPageHelpers
+} from '../lib/page'
+import {
+ Button,
+ ButtonRow,
+ Form,
+ FormSendMethod,
+ StaticField,
+ withForm
+} from '../lib/form';
+import {withErrorHandling} from '../lib/error-handling';
+import mailtrainConfig from 'mailtrainConfig';
+import {
+ getEditForm,
+ getTemplateTypes,
+ getTypeForm
+} from '../templates/helpers';
+import axios from '../lib/axios';
+import styles from "../lib/styles.scss";
+import {getUrl} from "../lib/urls";
+import {ResourceType} from "../lib/mosaico";
+
+const overridables = ['from_name', 'from_email', 'reply_to', 'subject'];
+
+@translate()
+@withForm
+@withPageHelpers
+@withErrorHandling
+@requiresAuthenticatedUser
+export default class CustomContent extends Component {
+ constructor(props) {
+ super(props);
+
+ const t = props.t;
+
+ console.log(props);
+ this.templateTypes = getTemplateTypes(props.t, 'data_sourceCustom_', ResourceType.CAMPAIGN);
+
+ this.customTemplateTypeOptions = [];
+ for (const key of mailtrainConfig.editors) {
+ this.customTemplateTypeOptions.push({key, label: this.templateTypes[key].typeName});
+ }
+
+ this.state = {
+ showMergeTagReference: false,
+ elementInFullscreen: false
+ };
+
+ this.initForm();
+ }
+
+ static propTypes = {
+ entity: PropTypes.object
+ }
+
+ componentDidMount() {
+ this.getFormValuesFromEntity(this.props.entity, data => {
+ data.data_sourceCustom_type = data.data.sourceCustom.type;
+ data.data_sourceCustom_data = data.data.sourceCustom.data;
+ data.data_sourceCustom_html = data.data.sourceCustom.html;
+ data.data_sourceCustom_text = data.data.sourceCustom.text;
+
+ this.templateTypes[data.data.sourceCustom.type].afterLoad(data);
+ });
+ }
+
+ localValidateFormValues(state) {
+ const t = this.props.t;
+
+ const customTemplateTypeKey = state.getIn(['data_sourceCustom_type', 'value']);
+
+ if (customTemplateTypeKey) {
+ this.templateTypes[customTemplateTypeKey].validate(state);
+ }
+ }
+
+ async submitHandler() {
+ const t = this.props.t;
+
+ const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
+ await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
+
+ let sendMethod, url;
+ if (this.props.entity) {
+ sendMethod = FormSendMethod.PUT;
+ url = `rest/campaigns/${this.props.entity.id}`
+ } else {
+ sendMethod = FormSendMethod.POST;
+ url = 'rest/campaigns'
+ }
+
+ this.disableForm();
+ this.setFormStatusMessage('info', t('Saving ...'));
+
+ const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
+ this.templateTypes[data.data_sourceCustom_type].beforeSave(data);
+
+ data.data.sourceCustom = {
+ type: data.data_sourceCustom_type,
+ data: data.data_sourceCustom_data,
+ html: data.data_sourceCustom_html,
+ text: data.data_sourceCustom_text
+ };
+
+ for (const key in data) {
+ if (key.startsWith('data_')) {
+ delete data[key];
+ }
+ }
+ });
+
+ if (submitResponse) {
+ if (this.props.entity) {
+ this.navigateToWithFlashMessage('/campaigns', 'success', t('Campaign saved'));
+ } else {
+ this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/edit`, 'success', t('Campaign saved'));
+ }
+ } else {
+ this.enableForm();
+ this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
+ }
+ }
+
+ async extractPlainText() {
+ const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
+ await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
+
+ const html = this.getFormValue('data_sourceCustom_html');
+ if (!html) {
+ return;
+ }
+
+ if (this.isFormDisabled()) {
+ return;
+ }
+
+ this.disableForm();
+
+ const response = await axios.post(getUrl('rest/html-to-text', { html }));
+
+ this.updateFormValue('data_sourceCustom_text', response.data.text);
+
+ this.enableForm();
+ }
+
+ async toggleMergeTagReference() {
+ this.setState({
+ showMergeTagReference: !this.state.showMergeTagReference
+ });
+ }
+
+ async setElementInFullscreen(elementInFullscreen) {
+ this.setState({
+ elementInFullscreen
+ });
+ }
+
+ render() {
+ const t = this.props.t;
+
+ // TODO: Toggle HTML preview
+
+ const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
+
+ // FIXME - data_sourceCustom_type is initialized only after first render
+
+ return (
+
+
{t('Edit Custom Content')}
+
+
+
+ );
+ }
+}
diff --git a/client/src/campaigns/List.js b/client/src/campaigns/List.js
index 5518fbdb..245811d1 100644
--- a/client/src/campaigns/List.js
+++ b/client/src/campaigns/List.js
@@ -25,6 +25,7 @@ import {
CampaignType
} from "../../../shared/campaigns";
import {checkPermissions} from "../lib/permissions";
+import {getCampaignTypeLabels} from "./helpers";
@translate()
@withPageHelpers
@@ -44,11 +45,7 @@ export default class List extends Component {
[CampaignStatus.ACTIVE]: t('Active')
};
- this.campaignTypes = {
- [CampaignType.REGULAR]: t('Regular'),
- [CampaignType.TRIGGERED]: t('Triggered'),
- [CampaignType.RSS]: t('RSS')
- };
+ this.campaignTypes = getCampaignTypeLabels(t);
this.state = {};
}
@@ -109,14 +106,26 @@ export default class List extends Component {
});
}
- if (perms.includes('manageFiles') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
+ if (perms.includes('edit') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE || campaignSource === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
+ actions.push({
+ label:
,
+ link: `/campaigns/${data[0]}/content`
+ });
+ }
+
+ if (perms.includes('viewFiles') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE || campaignSource === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
actions.push({
label: ,
link: `/campaigns/${data[0]}/files`
});
}
- // FIXME: add attachments
+ if (perms.includes('viewAttachments')) {
+ actions.push({
+ label: ,
+ link: `/campaigns/${data[0]}/attachments`
+ });
+ }
if (perms.includes('share')) {
actions.push({
diff --git a/client/src/campaigns/helpers.js b/client/src/campaigns/helpers.js
new file mode 100644
index 00000000..c539da46
--- /dev/null
+++ b/client/src/campaigns/helpers.js
@@ -0,0 +1,14 @@
+'use strict';
+
+import {CampaignType} from "../../../shared/campaigns";
+
+export function getCampaignTypeLabels(t) {
+
+ const campaignTypeLabels = {
+ [CampaignType.REGULAR]: t('Regular'),
+ [CampaignType.TRIGGERED]: t('Triggered'),
+ [CampaignType.RSS]: t('RSS')
+ };
+
+ return campaignTypeLabels;
+}
diff --git a/client/src/campaigns/root.js b/client/src/campaigns/root.js
index 90d3839f..9c84361a 100644
--- a/client/src/campaigns/root.js
+++ b/client/src/campaigns/root.js
@@ -3,6 +3,7 @@
import React from 'react';
import CampaignsCUD from './CUD';
+import Content from './Content';
import CampaignsList from './List';
import Share from '../shares/Share';
import Files from "../lib/files";
@@ -29,13 +30,24 @@ function getMenus(t) {
visible: resolved => resolved.campaign.permissions.includes('edit'),
panelRender: props =>
},
+ content: {
+ title: t('Content'),
+ link: params => `/campaigns/${params.campaignId}/content`,
+ visible: resolved => resolved.campaign.permissions.includes('edit') && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || resolved.campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN),
+ panelRender: props =>
+ },
files: {
title: t('Files'),
link: params => `/campaigns/${params.campaignId}/files`,
- visible: resolved => resolved.campaign.permissions.includes('edit') && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE),
- panelRender: props =>
+ visible: resolved => resolved.campaign.permissions.includes('viewFiles') && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || resolved.campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN),
+ panelRender: props =>
+ },
+ attachments: {
+ title: t('Attachments'),
+ link: params => `/campaigns/${params.campaignId}/attachments`,
+ visible: resolved => resolved.campaign.permissions.includes('viewAttachments'),
+ panelRender: props =>
},
- // FIXME: add attachments
share: {
title: t('Share'),
link: params => `/campaigns/${params.campaignId}/share`,
diff --git a/client/src/lib/files.js b/client/src/lib/files.js
index 93ed0722..714060bf 100644
--- a/client/src/lib/files.js
+++ b/client/src/lib/files.js
@@ -32,8 +32,10 @@ export default class Files extends Component {
static propTypes = {
title: PropTypes.string,
- entity: PropTypes.object,
- entityTypeId: PropTypes.string,
+ entity: PropTypes.object.isRequired,
+ entityTypeId: PropTypes.string.isRequired,
+ entitySubTypeId: PropTypes.string.isRequired,
+ managePermission: PropTypes.string.isRequired,
usePublicDownloadUrls: PropTypes.bool
}
@@ -65,7 +67,7 @@ export default class Files extends Component {
for (const file of files) {
data.append('files[]', file)
}
- axios.post(getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entity.id}`), data)
+ axios.post(getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${this.props.entity.id}`), data)
.then(res => {
this.filesTable.refresh();
const message = this.getFilesUploadedMessage(res);
@@ -93,7 +95,7 @@ export default class Files extends Component {
try {
this.setFlashMessage('info', t('Deleting file ...'));
- await axios.delete(getUrl(`rest/files/${this.props.entityTypeId}/${fileToDeleteId}`));
+ await axios.delete(getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${fileToDeleteId}`));
this.filesTable.refresh();
this.setFlashMessage('info', t('File deleted'));
} catch (err) {
@@ -110,24 +112,26 @@ export default class Files extends Component {
{ data: 3, title: "Size" },
{
actions: data => {
+ const actions = [];
let downloadUrl;
if (this.props.usePublicDownloadUrls) {
- downloadUrl =`/files/${this.props.entityTypeId}/${this.props.entity.id}/${data[2]}`;
+ downloadUrl =`/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${this.props.entity.id}/${data[2]}`;
} else {
- downloadUrl =`rest/files/${this.props.entityTypeId}/${data[0]}`;
+ downloadUrl =`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${data[0]}`;
}
- const actions = [
- {
- label: ,
- href: downloadUrl
- },
- {
+ actions.push({
+ label: ,
+ href: downloadUrl
+ });
+
+ if (this.props.entity.permissions.includes(this.props.managePermission)) {
+ actions.push({
label: ,
action: () => this.deleteFile(data[0], data[1])
- }
- ];
+ });
+ }
return actions;
}
@@ -146,10 +150,15 @@ export default class Files extends Component {
]}>
{t('Are you sure you want to delete file "{{name}}"?', {name: this.state.fileToDeleteName})}
-
- {state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
-
- this.filesTable = node} dataUrl={`rest/files-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={columns} />
+
+ {
+ this.props.entity.permissions.includes(this.props.managePermission) &&
+
+ {state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
+
+ }
+
+ this.filesTable = node} dataUrl={`rest/files-table/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${this.props.entity.id}`} columns={columns} />
);
}
diff --git a/client/src/lib/styles.scss b/client/src/lib/styles.scss
index 07427dac..160e7afb 100644
--- a/client/src/lib/styles.scss
+++ b/client/src/lib/styles.scss
@@ -26,6 +26,10 @@
text-align: right;
}
+.buttonRow {
+ margin-top: 15px;
+}
+
.buttonRow > * {
margin-right: 15px;
}
diff --git a/client/src/templates/List.js b/client/src/templates/List.js
index 77635baf..495c964f 100644
--- a/client/src/templates/List.js
+++ b/client/src/templates/List.js
@@ -80,7 +80,7 @@ export default class List extends Component {
});
}
- if (perms.includes('manageFiles')) {
+ if (perms.includes('viewFiles')) {
actions.push({
label: ,
link: `/templates/${data[0]}/files`
diff --git a/client/src/templates/helpers.js b/client/src/templates/helpers.js
index 063c3d06..3b1da84b 100644
--- a/client/src/templates/helpers.js
+++ b/client/src/templates/helpers.js
@@ -323,8 +323,6 @@ export function getEditForm(owner, typeKey, prefix = '') {
}
export function getTypeForm(owner, typeKey, isEdit) {
- return
- {owner.templateTypes[typeKey].getTypeForm(owner, isEdit)}
-
;
+ return owner.templateTypes[typeKey].getTypeForm(owner, isEdit);
}
diff --git a/client/src/templates/mosaico/List.js b/client/src/templates/mosaico/List.js
index 2760b83c..19fd397d 100644
--- a/client/src/templates/mosaico/List.js
+++ b/client/src/templates/mosaico/List.js
@@ -64,6 +64,20 @@ export default class List extends Component {
});
}
+ if (perms.includes('viewFiles')) {
+ actions.push({
+ label: ,
+ link: `/templates/mosaico/${data[0]}/files`
+ });
+ }
+
+ if (perms.includes('viewFiles')) {
+ actions.push({
+ label: ,
+ link: `/templates/mosaico/${data[0]}/blocks`
+ });
+ }
+
if (perms.includes('share')) {
actions.push({
label: ,
diff --git a/client/src/templates/root.js b/client/src/templates/root.js
index 04a16eaf..70f86abe 100644
--- a/client/src/templates/root.js
+++ b/client/src/templates/root.js
@@ -33,8 +33,8 @@ function getMenus(t) {
files: {
title: t('Files'),
link: params => `/templates/${params.templateId}/files`,
- visible: resolved => resolved.template.permissions.includes('edit'),
- panelRender: props =>
+ visible: resolved => resolved.template.permissions.includes('viewFiles'),
+ panelRender: props =>
},
share: {
title: t('Share'),
@@ -69,8 +69,14 @@ function getMenus(t) {
files: {
title: t('Files'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/files`,
- visible: resolved => resolved.mosaicoTemplate.permissions.includes('edit'),
- panelRender: props =>
+ visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'),
+ panelRender: props =>
+ },
+ blocks: {
+ title: t('Block thumbnails'),
+ link: params => `/templates/mosaico/${params.mosaiceTemplateId}/blocks`,
+ visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'),
+ panelRender: props =>
},
share: {
title: t('Share'),
diff --git a/lib/file-helpers.js b/lib/file-helpers.js
index 156c0c67..48b4c3c3 100644
--- a/lib/file-helpers.js
+++ b/lib/file-helpers.js
@@ -10,9 +10,9 @@ const multer = require('multer')({
dest: uploadedFilesDir
});
-function installUploadHandler(router, url, getUrl = null, dontReplace = false) {
+function installUploadHandler(router, url, replacementBehavior, type = null, subType = null) {
router.postAsync(url, passport.loggedIn, multer.array('files[]'), async (req, res) => {
- return res.json(await files.createFiles(req.context, req.params.type, req.params.entityId, req.files, getUrl, dontReplace));
+ return res.json(await files.createFiles(req.context, type || req.params.type, subType || req.params.subType, req.params.entityId, req.files, replacementBehavior));
});
}
diff --git a/lib/permissions.js b/lib/permissions.js
index 28af538b..0c00209b 100644
--- a/lib/permissions.js
+++ b/lib/permissions.js
@@ -20,13 +20,36 @@ const entityTypes = {
entitiesTable: 'campaigns',
sharesTable: 'shares_campaign',
permissionsTable: 'permissions_campaign',
- filesTable: 'files_campaign'
+ files: {
+ file: {
+ table: 'files_campaign_file',
+ permissions: {
+ view: 'viewFiles',
+ manage: 'manageFiles'
+ }
+ },
+ attachment: {
+ table: 'files_campaign_attachment',
+ permissions: {
+ view: 'viewAttachments',
+ manage: 'manageAttachments'
+ }
+ }
+ }
},
template: {
entitiesTable: 'templates',
sharesTable: 'shares_template',
permissionsTable: 'permissions_template',
- filesTable: 'files_template'
+ files: {
+ file: {
+ table: 'files_template_file',
+ permissions: {
+ view: 'viewFiles',
+ manage: 'manageFiles'
+ }
+ }
+ }
},
sendConfiguration: {
entitiesTable: 'send_configurations',
@@ -47,7 +70,22 @@ const entityTypes = {
entitiesTable: 'mosaico_templates',
sharesTable: 'shares_mosaico_template',
permissionsTable: 'permissions_mosaico_template',
- filesTable: 'files_mosaico_template'
+ files: {
+ file: {
+ table: 'files_mosaico_template_file',
+ permissions: {
+ view: 'viewFiles',
+ manage: 'manageFiles'
+ }
+ },
+ block: {
+ table: 'files_mosaico_template_block',
+ permissions: {
+ view: 'viewFiles',
+ manage: 'manageFiles'
+ }
+ }
+ }
}
};
diff --git a/lib/report-processor.js b/lib/report-processor.js
index 0bee35c8..67bda8f2 100644
--- a/lib/report-processor.js
+++ b/lib/report-processor.js
@@ -104,7 +104,7 @@ module.exports.start = async (reportId) => {
if (!workers[reportId]) {
log.info('ReportProcessor', 'Scheduling report id: %s', reportId);
await reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, last_run: null});
- tryStartWorkers();
+ await tryStartWorkers();
} else {
log.info('ReportProcessor', 'Worker for report id: %s is already running.', reportId);
}
@@ -125,7 +125,7 @@ module.exports.stop = async reportId => {
module.exports.init = async () => {
try {
await reports.bulkChangeState(reports.ReportState.PROCESSING, reports.ReportState.SCHEDULED);
- tryStartWorkers();
+ await tryStartWorkers();
} catch (err) {
log.error('ReportProcessor', err);
}
diff --git a/models/campaigns.js b/models/campaigns.js
index 7567235e..563a38fd 100644
--- a/models/campaigns.js
+++ b/models/campaigns.js
@@ -9,6 +9,7 @@ const { enforce, filterObject } = require('../lib/helpers');
const shares = require('./shares');
const namespaceHelpers = require('../lib/namespace-helpers');
const files = require('./files');
+const templates = require('./templates');
const { CampaignSource, CampaignType} = require('../shared/campaigns');
const segments = require('./segments');
@@ -33,16 +34,22 @@ async function listDTAjax(context, params) {
);
}
-async function getById(context, id) {
- return await knex.transaction(async tx => {
- await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'view');
- const entity = await tx('campaigns').where('id', id).first();
+async function getByIdTx(tx, context, id, withPermissions = true) {
+ await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'view');
+ const entity = await tx('campaigns').where('id', id).first();
+ if (withPermissions) {
entity.permissions = await shares.getPermissionsTx(tx, context, 'campaign', id);
+ }
- entity.data = JSON.parse(entity.data);
+ entity.data = JSON.parse(entity.data);
- return entity;
+ return entity;
+}
+
+async function getById(context, id, withPermissions = true) {
+ return await knex.transaction(async tx => {
+ return await getByIdTx(tx, context, id, withPermissions);
});
}
@@ -50,7 +57,7 @@ async function _validateAndPreprocess(tx, context, entity, isCreate) {
await namespaceHelpers.validateEntity(tx, entity);
if (isCreate) {
- enforce(entity.type === CampaignType.REGULAR && entity.type === CampaignType.RSS && entity.type === CampaignType.TRIGGERED, 'Unknown campaign type');
+ 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');
@@ -66,6 +73,8 @@ async function _validateAndPreprocess(tx, context, entity, isCreate) {
await segments.getByIdTx(tx, context, entity.list, entity.segment);
}
+ await shares.enforceEntityPermissionTx(tx, context, 'send_configuration', entity.send_configuration, 'viewPublic');
+
entity.data = JSON.stringify(entity.data);
}
@@ -73,9 +82,30 @@ async function create(context, entity) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign');
- let copyFilesFromTemplateId;
+ let copyFilesFrom = null;
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
- copyFilesFromTemplateId = entity.data.sourceTemplate;
+ 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
+ };
+ } 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);
+
+ entity.data.sourceCustom = sourceCampaign.data.sourceCustom;
}
await _validateAndPreprocess(tx, context, entity, true);
@@ -119,8 +149,8 @@ async function create(context, entity) {
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
- if (copyFilesFromTemplateId) {
- files.copyAllTx(tx, context, 'template', copyFilesFromTemplateId, 'campaign', id);
+ if (copyFilesFrom) {
+ await files.copyAllTx(tx, context, copyFilesFrom.entityType, copyFilesFrom.entityId, 'campaign', id);
}
return id;
@@ -168,6 +198,7 @@ async function remove(context, id) {
module.exports = {
hash,
listDTAjax,
+ getByIdTx,
getById,
create,
updateWithConsistencyCheck,
diff --git a/models/files.js b/models/files.js
index 0e6c80e8..1978ebc1 100644
--- a/models/files.js
+++ b/models/files.js
@@ -10,49 +10,63 @@ const interoperableErrors = require('../shared/interoperable-errors');
const permissions = require('../lib/permissions');
const {getTrustedUrl} = require('../lib/urls');
+const crypto = require('crypto');
+const bluebird = require('bluebird');
+const cryptoPseudoRandomBytes = bluebird.promisify(crypto.pseudoRandomBytes);
+
const entityTypes = permissions.getEntityTypes();
const filesDir = path.join(__dirname, '..', 'files');
-function enforceTypePermitted(type) {
- enforce(type in entityTypes && entityTypes[type].filesTable);
+const ReplacementBehavior = {
+ NONE: 0,
+ REPLACE: 1,
+ RENAME: 2
+};
+
+function enforceTypePermitted(type, subType) {
+ enforce(type in entityTypes && entityTypes[type].files && entityTypes[type].files[subType]);
}
-function getFilePath(type, entityId, filename) {
- return path.join(path.join(filesDir, type, entityId.toString()), filename);
+function getFilePath(type, subType, entityId, filename) {
+ return path.join(path.join(filesDir, type, subType, entityId.toString()), filename);
}
-function getFileUrl(context, type, entityId, filename) {
- return getTrustedUrl(`files/${type}/${entityId}/${filename}`, context)
+function getFileUrl(context, type, subType, entityId, filename) {
+ return getTrustedUrl(`files/${type}/${subType}/${entityId}/${filename}`, context)
}
-function getFilesTable(type) {
- return entityTypes[type].filesTable;
+function getFilesTable(type, subType) {
+ return entityTypes[type].files[subType].table;
}
-async function listDTAjax(context, type, entityId, params) {
- enforceTypePermitted(type);
- await shares.enforceEntityPermission(context, type, entityId, 'viewFiles');
+function getFilesPermission(type, subType, operation) {
+ return entityTypes[type].files[subType].permissions[operation];
+}
+
+async function listDTAjax(context, type, subType, entityId, params) {
+ enforceTypePermitted(type, subType);
+ await shares.enforceEntityPermission(context, type, entityId, getFilesPermission(type, subType, 'view'));
return await dtHelpers.ajaxList(
params,
- builder => builder.from(getFilesTable(type)).where({entity: entityId}),
+ builder => builder.from(getFilesTable(type, subType)).where({entity: entityId}),
['id', 'originalname', 'filename', 'size', 'created']
);
}
-async function list(context, type, entityId) {
- enforceTypePermitted(type);
+async function list(context, type, subType, entityId) {
+ enforceTypePermitted(type, subType);
return await knex.transaction(async tx => {
- await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'viewFiles');
- return await tx(getFilesTable(type)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
+ await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'view'));
+ return await tx(getFilesTable(type, subType)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
});
}
-async function getFileById(context, type, id) {
- enforceTypePermitted(type);
+async function getFileById(context, type, subType, id) {
+ enforceTypePermitted(type, subType);
const file = await knex.transaction(async tx => {
- const file = await tx(getFilesTable(type)).where('id', id).first();
- await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'viewFiles');
+ const file = await tx(getFilesTable(type, subType)).where('id', id).first();
+ await shares.enforceEntityPermissionTx(tx, context, type, file.entity, getFilesPermission(type, subType, 'view'));
return file;
});
@@ -63,15 +77,15 @@ async function getFileById(context, type, id) {
return {
mimetype: file.mimetype,
name: file.originalname,
- path: getFilePath(type, file.entity, file.filename)
+ path: getFilePath(type, subType, file.entity, file.filename)
};
}
-async function _getFileBy(context, type, entityId, key, value) {
- enforceTypePermitted(type);
+async function _getFileBy(context, type, subType, entityId, key, value) {
+ enforceTypePermitted(type, subType);
const file = await knex.transaction(async tx => {
- await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'viewFiles');
- const file = await tx(getFilesTable(type)).where({entity: entityId, [key]: value}).first();
+ await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'view'));
+ const file = await tx(getFilesTable(type, subType)).where({entity: entityId, [key]: value}).first();
return file;
});
@@ -82,30 +96,31 @@ async function _getFileBy(context, type, entityId, key, value) {
return {
mimetype: file.mimetype,
name: file.originalname,
- path: getFilePath(type, file.entity, file.filename)
+ path: getFilePath(type, subType, file.entity, file.filename)
};
}
-async function getFileByOriginalName(context, type, entityId, name) {
- return await _getFileBy(context, type, entityId, 'originalname', name)
+async function getFileByOriginalName(context, type, subType, entityId, name) {
+ return await _getFileBy(context, type, subType, entityId, 'originalname', name)
}
-async function getFileByFilename(context, type, entityId, name) {
- return await _getFileBy(context, type, entityId, 'filename', name)
+async function getFileByFilename(context, type, subType, entityId, name) {
+ return await _getFileBy(context, type, subType, entityId, 'filename', name)
}
-async function getFileByUrl(context, type, entityId, url) {
- const urlPrefix = getTrustedUrl(`files/${type}/${entityId}/`, context);
+async function getFileByUrl(context, type, subType, entityId, url) {
+ const urlPrefix = getTrustedUrl(`files/${type}/${subType}/${entityId}/`, context);
if (url.startsWith(urlPrefix)) {
const name = url.substring(urlPrefix.length);
- return await getFileByFilename(context, type, entityId, name);
+ return await getFileByFilename(context, type, subType, entityId, name);
} else {
throw new interoperableErrors.NotFoundError();
}
}
-async function createFiles(context, type, entityId, files, getUrl = null, dontReplace = false) {
- enforceTypePermitted(type);
+// Adds files to an entity. The source data can be either a file (then it's path is contained in file.path) or in-memory data (then it's content is in file.data).
+async function createFiles(context, type, subType, entityId, files, replacementBehavior) {
+ enforceTypePermitted(type, subType);
if (files.length == 0) {
// No files uploaded
return {uploaded: 0};
@@ -118,31 +133,39 @@ async function createFiles(context, type, entityId, files, getUrl = null, dontRe
const filesRet = [];
await knex.transaction(async tx => {
- await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'manageFiles');
+ await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'manage'));
- const existingNamesRows = await tx(getFilesTable(type)).where('entity', entityId).select(['filename', 'originalname']);
- const existingNameMap = new Map();
+ const existingNamesRows = await tx(getFilesTable(type, subType)).where('entity', entityId).select(['id', 'filename', 'originalname']);
+
+ const existingNameSet = new Set();
for (const row of existingNamesRows) {
- existingNameMap.set(row.originalname, row);
+ existingNameSet.add(row.originalname);
}
- const originalNameSet = new Set();
+ // The processedNameSet holds originalnames of entries which have been already processed in the upload batch. It prevents uploading two files with the same originalname
+ const processedNameSet = new Set();
+
// Create entities for files
for (const file of files) {
const parsedOriginalName = path.parse(file.originalname);
let originalName = parsedOriginalName.base;
- if (dontReplace) {
+ if (!file.filename) {
+ // This is taken from multer/storage/disk.js and adapted for async/await
+ file.filename = (await cryptoPseudoRandomBytes(16)).toString('hex');
+ }
+
+ if (replacementBehavior === ReplacementBehavior.RENAME) {
let suffix = 1;
- while (existingNameMap.has(originalName) || originalNameSet.has(originalName)) {
+ while (existingNameSet.has(originalName) || processedNameSet.has(originalName)) {
originalName = parsedOriginalName.name + '-' + suffix + parsedOriginalName.ext;
suffix++;
}
}
- if (originalNameSet.has(originalName)) {
- // The file has an original name same as another file
+ if (replacementBehavior === ReplacementBehavior.NONE && (existingNameSet.has(originalName) || processedNameSet.has(originalName))) {
+ // The file has an original name same as another file in the same upload batch or it has an original name same as another already existing file
ignoredFiles.push(file);
} else {
@@ -161,46 +184,61 @@ async function createFiles(context, type, entityId, files, getUrl = null, dontRe
name: file.filename,
originalName: originalName,
size: file.size,
- type: file.mimetype,
+ type: file.mimetype
};
- filesRetEntry.url = getFileUrl(context, type, entityId, file.filename);
- filesRetEntry.thumbnailUrl = getFileUrl(context, type, entityId, file.filename); // TODO - use smaller thumbnails
+ filesRetEntry.url = getFileUrl(context, type, subType, entityId, file.filename);
+
+ if (file.mimetype.startsWith('image/')) {
+ filesRetEntry.thumbnailUrl = getFileUrl(context, type, subType, entityId, file.filename); // TODO - use smaller thumbnails,
+ }
filesRet.push(filesRetEntry);
+ }
- if (existingNameMap.has(originalName)) {
- removedFiles.push(existingNameMap.get(originalName));
+ processedNameSet.add(originalName);
+ }
+
+ if (replacementBehavior === ReplacementBehavior.REPLACE) {
+ for (const row of existingNamesRows) {
+ const idsToRemove = [];
+ if (processedNameSet.has(row.originalname)) {
+ removedFiles.push(row);
+ idsToRemove.push(row.id);
}
}
- originalNameSet.add(originalName);
+ await tx(getFilesTable(type, subType)).where('entity', entityId).whereIn('id', idsToRemove).del();
}
- const originalNameArray = Array.from(originalNameSet);
- await tx(getFilesTable(type)).where('entity', entityId).whereIn('originalname', originalNameArray).del();
-
if (fileEntities) {
- await tx(getFilesTable(type)).insert(fileEntities);
+ await tx(getFilesTable(type, subType)).insert(fileEntities);
}
});
// Move new files from upload directory to files directory
for (const file of filesToMove) {
- const filePath = getFilePath(type, entityId, file.filename);
- // The names should be unique, so overwrite is disabled
- // The directory is created if it does not exist
- // Empty options argument is passed, otherwise fails
- await fs.moveAsync(file.path, filePath, {});
+ const filePath = getFilePath(type, subType, entityId, file.filename);
+
+ if (file.path) {
+ // The names should be unique, so overwrite is disabled
+ // The directory is created if it does not exist
+ // Empty options argument is passed, otherwise fails
+ await fs.moveAsync(file.path, filePath, {});
+ } else if (file.data) {
+ await fs.outputFile(filePath, file.data);
+ }
}
// Remove replaced files from files directory
for (const file of removedFiles) {
- const filePath = getFilePath(type, entityId, file.filename);
+ const filePath = getFilePath(type, subType, entityId, file.filename);
await fs.removeAsync(filePath);
}
// Remove ignored files from upload directory
for (const file of ignoredFiles) {
- await fs.removeAsync(file.path);
+ if (file.path) {
+ await fs.removeAsync(file.path);
+ }
}
return {
@@ -212,38 +250,38 @@ async function createFiles(context, type, entityId, files, getUrl = null, dontRe
};
}
-async function removeFile(context, type, id) {
- enforceTypePermitted(type);
+async function removeFile(context, type, subType, id) {
+ enforceTypePermitted(type, subType);
const file = await knex.transaction(async tx => {
- const file = await tx(getFilesTable(type)).where('id', id).select('entity', 'filename').first();
- await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
- await tx(getFilesTable(type)).where('id', id).del();
+ const file = await tx(getFilesTable(type, subType)).where('id', id).select('entity', 'filename').first();
+ await shares.enforceEntityPermissionTx(tx, context, type, file.entity, getFilesPermission(type, subType, 'manage'));
+ await tx(getFilesTable(type, subType)).where('id', id).del();
return {filename: file.filename, entity: file.entity};
});
- const filePath = getFilePath(type, file.entity, file.filename);
+ const filePath = getFilePath(type, subType, file.entity, file.filename);
await fs.removeAsync(filePath);
}
-async function copyAllTx(tx, context, fromType, fromEntityId, toType, toEntityId) {
- enforceTypePermitted(fromType);
- await shares.enforceEntityPermissionTx(tx, context, fromType, fromEntityId, 'viewFiles');
+async function copyAllTx(tx, context, fromType, fromSubType, fromEntityId, toType, toSubType, toEntityId) {
+ enforceTypePermitted(fromType, fromSubType);
+ await shares.enforceEntityPermissionTx(tx, context, fromType, fromEntityId, getFilesPermission(fromType, fromSubType, 'view'));
- enforceTypePermitted(toType);
- await shares.enforceEntityPermissionTx(tx, context, toType, toEntityId, 'manageFiles');
+ enforceTypePermitted(toType, toSubType);
+ await shares.enforceEntityPermissionTx(tx, context, toType, toEntityId, getFilesPermission(toType, toSubType, 'manage'));
- const rows = await tx(getFilesTable(fromType)).where({entity: fromEntityId});
+ const rows = await tx(getFilesTable(fromType, fromSubType)).where({entity: fromEntityId});
for (const row of rows) {
- const fromFilePath = getFilePath(fromType, fromEntityId, row.filename);
- const toFilePath = getFilePath(toType, toEntityId, row.filename);
+ const fromFilePath = getFilePath(fromType, fromSubType, fromEntityId, row.filename);
+ const toFilePath = getFilePath(toType, toSubType, toEntityId, row.filename);
await fs.copyAsync(fromFilePath, toFilePath, {});
delete row.id;
row.entity = toEntityId;
}
- await tx(getFilesTable(toType)).insert(rows);
+ await tx(getFilesTable(toType, toSubType)).insert(rows);
}
@@ -259,5 +297,6 @@ module.exports = {
removeFile,
getFileUrl,
getFilePath,
- copyAllTx
+ copyAllTx,
+ ReplacementBehavior
};
\ No newline at end of file
diff --git a/models/templates.js b/models/templates.js
index 07d4fb2c..667a6c51 100644
--- a/models/templates.js
+++ b/models/templates.js
@@ -15,17 +15,21 @@ function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys));
}
+async function getByIdTx(tx, context, id, withPermissions = true) {
+ await shares.enforceEntityPermissionTx(tx, context, 'template', id, 'view');
+ const entity = await tx('templates').where('id', id).first();
+ entity.data = JSON.parse(entity.data);
+
+ if (withPermissions) {
+ entity.permissions = await shares.getPermissionsTx(tx, context, 'template', id);
+ }
+
+ return entity;
+}
+
async function getById(context, id, withPermissions = true) {
return await knex.transaction(async tx => {
- await shares.enforceEntityPermissionTx(tx, context, 'template', id, 'view');
- const entity = await tx('templates').where('id', id).first();
- entity.data = JSON.parse(entity.data);
-
- if (withPermissions) {
- entity.permissions = await shares.getPermissionsTx(tx, context, 'template', id);
- }
-
- return entity;
+ return await getByIdTx(tx, context, id, withPermissions);
});
}
@@ -100,6 +104,7 @@ async function remove(context, id) {
module.exports = {
hash,
+ getByIdTx,
getById,
listDTAjax,
create,
diff --git a/package-lock.json b/package-lock.json
index 0c871af6..a3f0afe8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1308,6 +1308,11 @@
}
}
},
+ "crypto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
+ "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="
+ },
"csrf": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.6.tgz",
diff --git a/package.json b/package.json
index fd8d4eab..9e54b160 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
"connect-redis": "^3.3.0",
"cookie-parser": "^1.4.3",
"cors": "^2.8.4",
+ "crypto": "^1.0.1",
"csurf": "^1.9.0",
"csv-parse": "^1.2.3",
"device": "^0.3.8",
diff --git a/routes/mosaico.js b/routes/mosaico.js
index 403f7f99..32ae59bb 100644
--- a/routes/mosaico.js
+++ b/routes/mosaico.js
@@ -135,10 +135,9 @@ function getRouter(trusted) {
});
// Mosaico looks for block thumbnails in edres folder relative to index.html of the template. We respond to such requests here.
- // We use the following naming convention in Mosaico templates for the block thumbnails: edres/xxx -> edres-xxx
router.getAsync('/templates/:mosaicoTemplateId/edres/:fileName', async (req, res, next) => {
try {
- const file = await files.getFileByOriginalName(contextHelpers.getAdminContext(), 'mosaicoTemplate', req.params.mosaicoTemplateId, req.params.fileName);
+ const file = await files.getFileByOriginalName(contextHelpers.getAdminContext(), 'mosaicoTemplate', 'block', req.params.mosaicoTemplateId, req.params.fileName);
res.type(file.mimetype);
return res.download(file.path, file.name);
} catch (err) {
@@ -154,18 +153,18 @@ function getRouter(trusted) {
router.use('/templates/:mosaicoTemplateId/edres', express.static(path.join(__dirname, '..', 'client', 'public', 'mosaico', 'templates', 'versafix-1', 'edres')));
- fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', getSandboxUrl, true);
+ fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file');
router.getAsync('/upload/:type/:entityId', passport.loggedIn, async (req, res) => {
- const entries = await files.list(req.context, req.params.type, req.params.entityId);
+ const entries = await files.list(req.context, req.params.type, 'file', req.params.entityId);
const filesOut = [];
for (const entry of entries) {
filesOut.push({
name: entry.originalname,
- url: files.getFileUrl(req.context, req.params.type, req.params.entityId, entry.filename),
+ url: files.getFileUrl(req.context, req.params.type, 'file', req.params.entityId, entry.filename),
size: entry.size,
- thumbnailUrl: files.getFileUrl(req.context, req.params.type, req.params.entityId, entry.filename) // TODO - use smaller thumbnails
+ thumbnailUrl: files.getFileUrl(req.context, req.params.type, 'file', req.params.entityId, entry.filename) // TODO - use smaller thumbnails
})
}
@@ -175,9 +174,6 @@ function getRouter(trusted) {
});
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
- const resourceType = req.query.type;
- const resourceId = req.query.id;
-
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, trusted);
let languageStrings = null;
@@ -229,7 +225,7 @@ function getRouter(trusted) {
if (url.startsWith(mosaicoLegacyUrlPrefix)) {
filePath = path.join(__dirname, '..', 'client', 'public' , 'mosaico', 'uploads', url.substring(mosaicoLegacyUrlPrefix.length));
} else {
- const file = await files.getFileByUrl(contextHelpers.getAdminContext(), req.params.type, req.params.entityId, url);
+ const file = await files.getFileByUrl(contextHelpers.getAdminContext(), req.params.type, 'file', req.params.entityId, url);
filePath = file.path;
}
diff --git a/routes/rest/files.js b/routes/rest/files.js
index 35919eaa..f3b9216f 100644
--- a/routes/rest/files.js
+++ b/routes/rest/files.js
@@ -5,21 +5,21 @@ const files = require('../../models/files');
const router = require('../../lib/router-async').create();
const fileHelpers = require('../../lib/file-helpers');
-router.postAsync('/files-table/:type/:entityId', passport.loggedIn, async (req, res) => {
- return res.json(await files.listDTAjax(req.context, req.params.type, req.params.entityId, req.body));
+router.postAsync('/files-table/:type/:subType/:entityId', passport.loggedIn, async (req, res) => {
+ return res.json(await files.listDTAjax(req.context, req.params.type, req.params.subType, req.params.entityId, req.body));
});
-router.getAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
- const file = await files.getFileById(req.context, req.params.type, req.params.fileId);
+router.getAsync('/files/:type/:subType/:fileId', passport.loggedIn, async (req, res) => {
+ const file = await files.getFileById(req.context, req.params.type, req.params.subType, req.params.fileId);
res.type(file.mimetype);
return res.download(file.path, file.name);
});
-router.deleteAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
- await files.removeFile(req.context, req.params.type, req.params.fileId);
+router.deleteAsync('/files/:type/:subType/:fileId', passport.loggedIn, async (req, res) => {
+ await files.removeFile(req.context, req.params.type, req.params.subType, req.params.fileId);
return res.json();
});
-fileHelpers.installUploadHandler(router, '/files/:type/:entityId');
+fileHelpers.installUploadHandler(router, '/files/:type/:subType/:entityId', files.ReplacementBehavior.REPLACE);
module.exports = router;
\ No newline at end of file
diff --git a/routes/rest/shares.js b/routes/rest/shares.js
index 122337f3..21e13eed 100644
--- a/routes/rest/shares.js
+++ b/routes/rest/shares.js
@@ -69,7 +69,7 @@ router.postAsync('/permissions-check', passport.loggedIn, async (req, res) => {
router.postAsync('/permissions-rebuild', passport.loggedIn, async (req, res) => {
shares.enforceGlobalPermission(req.context, 'rebuildPermissions');
- shares.rebuildPermissions();
+ await shares.rebuildPermissions();
return res.json(result);
});
diff --git a/routes/subscription.js b/routes/subscription.js
index fbd7d16e..3ae130d0 100644
--- a/routes/subscription.js
+++ b/routes/subscription.js
@@ -505,7 +505,7 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req,
const autoUnsubscribe = req.query.auto === 'yes';
if (autoUnsubscribe) {
- handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, res, next);
+ await handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, res, next);
} else if (req.query.formTest ||
list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
diff --git a/setup/knex/migrations/20170506102634_base.js b/setup/knex/migrations/20170506102634_base.js
index 52d39e37..308eaa77 100644
--- a/setup/knex/migrations/20170506102634_base.js
+++ b/setup/knex/migrations/20170506102634_base.js
@@ -120,5 +120,4 @@ exports.up = (knex, Promise) => (async() => {
})();
exports.down = (knex, Promise) => (async() => {
- // return knex.schema.dropTable('users');
})();
\ No newline at end of file
diff --git a/setup/knex/migrations/20170507083345_create_namespaces.js b/setup/knex/migrations/20170507083345_create_namespaces.js
index 4535392a..cbb5b443 100644
--- a/setup/knex/migrations/20170507083345_create_namespaces.js
+++ b/setup/knex/migrations/20170507083345_create_namespaces.js
@@ -31,5 +31,4 @@ exports.up = (knex, Promise) => (async() => {
})();
exports.down = (knex, Promise) => (async() => {
- await knex.schema.dropTable('namespaces');
})();
\ No newline at end of file
diff --git a/setup/knex/migrations/20170507084114_create_permissions.js b/setup/knex/migrations/20170507084114_create_permissions.js
index b14ee841..8f7c0c38 100644
--- a/setup/knex/migrations/20170507084114_create_permissions.js
+++ b/setup/knex/migrations/20170507084114_create_permissions.js
@@ -32,9 +32,4 @@ exports.up = (knex, Promise) => (async() => {
})();
exports.down = (knex, Promise) => (async() => {
- for (const entityType of shareableEntityTypes) {
- await knex.schema
- .dropTable(`shares_${entityType}`)
- .dropTable(`permissions_${entityType}`);
- }
})();
diff --git a/setup/knex/migrations/20180110120444_add_files.js b/setup/knex/migrations/20180110120444_add_files.js
index 3c5bebc5..a6aa6d41 100644
--- a/setup/knex/migrations/20180110120444_add_files.js
+++ b/setup/knex/migrations/20180110120444_add_files.js
@@ -1,25 +1,39 @@
-const entityTypesWithFiles = ['template', 'campaign'];
+const entityTypesWithFiles = {
+ campaign: {
+ file: 'files_campaign_file',
+ attachment: 'files_campaign_attachment',
+ },
+ template: {
+ file: 'files_template_file'
+ },
+ mosaicoTemplate: {
+ file: 'files_mosaico_template_file',
+ block: 'files_mosaico_template_block'
+ }
+};
+
exports.up = (knex, Promise) => (async() => {
- for (const entityType of entityTypesWithFiles) {
+ for (const type in entityTypesWithFiles) {
+ const typeEntry = entityTypesWithFiles[type];
- await knex.schema.createTable(`files_${entityType}`, table => {
- table.increments('id').primary();
- table.integer('entity').unsigned().notNullable().references(`${entityType}s.id`);
- table.string('filename');
- table.string('originalname');
- table.string('mimetype');
- table.string('encoding');
- table.integer('size');
- table.timestamp('created').defaultTo(knex.fn.now());
- table.index(['entity', 'originalname'])
- })
+ for (const subType in typeEntry) {
+ const subTypeEntry = typeEntry[subType];
+ await knex.schema.createTable(subTypeEntry, table => {
+ table.increments('id').primary();
+ table.integer('entity').unsigned().notNullable().references(`${type}s.id`);
+ table.string('filename');
+ table.string('originalname');
+ table.string('mimetype');
+ table.string('encoding');
+ table.integer('size');
+ table.timestamp('created').defaultTo(knex.fn.now());
+ table.index(['entity', 'originalname'])
+ });
+ }
}
})();
exports.down = (knex, Promise) => (async() => {
- for (const entityType of entityTypesWithFiles) {
- await knex.schema.dropTable(`files_${entityType}`);
- }
})();
diff --git a/setup/knex/migrations/20180401120444_create_mosaico_templates.js b/setup/knex/migrations/20180401120444_create_mosaico_templates.js
index b3fe5b4d..a0c62005 100644
--- a/setup/knex/migrations/20180401120444_create_mosaico_templates.js
+++ b/setup/knex/migrations/20180401120444_create_mosaico_templates.js
@@ -52,10 +52,4 @@ exports.up = (knex, Promise) => (async() => {
})();
exports.down = (knex, Promise) => (async() => {
- await knex.schema
- .dropTable('shares_mosaico_template')
- .dropTable('permissions_mosaico_template')
- .dropTable('files_mosaico_template')
- .dropTable('mosaico_templates')
- ;
})();
diff --git a/setup/knex/migrations/20180414120444_transform_settings.js b/setup/knex/migrations/20180414120444_transform_settings.js
index 30f8a1e7..a5ff6d88 100644
--- a/setup/knex/migrations/20180414120444_transform_settings.js
+++ b/setup/knex/migrations/20180414120444_transform_settings.js
@@ -121,9 +121,4 @@ exports.up = (knex, Promise) => (async() => {
exports.down = (knex, Promise) => (async() => {
- await knex.schema
- .dropTable('shares_send_configuration')
- .dropTable('permissions_send_configuration')
- .dropTable('send_configurations')
- ;
})();
diff --git a/setup/knex/migrations/20180718220444_upgrade_campaigns.js b/setup/knex/migrations/20180718220444_upgrade_campaigns.js
index 957076d2..e9f3e2e2 100644
--- a/setup/knex/migrations/20180718220444_upgrade_campaigns.js
+++ b/setup/knex/migrations/20180718220444_upgrade_campaigns.js
@@ -56,6 +56,8 @@ scheduled - used only for campaign type NORMAL
const { getSystemSendConfigurationId } = require('../../../shared/send-configurations');
const { CampaignSource, CampaignType} = require('../../../shared/campaigns');
+const files = require('../../../models/files');
+const contextHelpers = require('../../../lib/context-helpers');
exports.up = (knex, Promise) => (async() => {
@@ -77,11 +79,11 @@ exports.up = (knex, Promise) => (async() => {
let editorType = campaign.editor_name;
const editorData = JSON.parse(campaign.editor_data || '{}');
- if (editorType == 'summernote') {
+ if (editorType === 'summernote') {
editorType = 'ckeditor';
}
- if (editorType == 'mosaico') {
+ if (editorType === 'mosaico') {
editorType = 'mosaicoWithFsTemplate';
editorData.mosaicoFsTemplate = editorData.template;
delete editorData.template;
@@ -115,6 +117,20 @@ exports.up = (knex, Promise) => (async() => {
campaign.data = JSON.stringify(data);
await knex('campaigns').where('id', campaign.id).update(campaign);
+
+ const attachments = await knex('attachments').where('campaign', campaign.id);
+ const attachmentFiles = [];
+ for (const attachment of attachments) {
+ attachmentFiles.push({
+ originalname: attachment.filename,
+ mimetype: attachment.content_type,
+ // encoding: file.encoding,
+ size: attachment.size,
+ created: attachment.created,
+ data: attachment.content
+ });
+ }
+ await files.createFiles(contextHelpers.getAdminContext(), 'campaign', 'attachment', campaign.id, attachmentFiles, files.ReplacementBehavior.NONE);
}
await knex.schema.table('campaigns', table => {
@@ -138,6 +154,7 @@ exports.up = (knex, Promise) => (async() => {
await knex.schema.dropTableIfExists('campaign');
await knex.schema.dropTableIfExists('campaign_tracker');
+ await knex.schema.dropTableIfExists('attachments');
})();
exports.down = (knex, Promise) => (async() => {
diff --git a/setup/knex/migrations/20180718220445_drop_template_table.js b/setup/knex/migrations/20180718220445_drop_subscription_table.js
similarity index 100%
rename from setup/knex/migrations/20180718220445_drop_template_table.js
rename to setup/knex/migrations/20180718220445_drop_subscription_table.js
diff --git a/shared/campaigns.js b/shared/campaigns.js
index 966df8d9..e09dceb0 100644
--- a/shared/campaigns.js
+++ b/shared/campaigns.js
@@ -6,8 +6,9 @@ const CampaignSource = {
TEMPLATE: 1,
CUSTOM: 2,
CUSTOM_FROM_TEMPLATE: 3,
- URL: 4,
- RSS: 5,
+ CUSTOM_FROM_CAMPAIGN: 4,
+ URL: 5,
+ RSS: 6,
MAX: 6
};