Editing of campaigns seems to work

This commit is contained in:
Tomas Bures 2018-08-03 17:05:55 +05:30
parent b1c667d13d
commit 7b46c4b4b0
27 changed files with 335 additions and 130 deletions

View file

@ -217,6 +217,7 @@ export default class CUD extends Component {
localValidateFormValues(state) { localValidateFormValues(state) {
const t = this.props.t; const t = this.props.t;
const isEdit = !!this.props.entity;
if (!state.getIn(['name', 'value'])) { if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty')); state.setIn(['name', 'error'], t('Name must not be empty'));
@ -259,17 +260,17 @@ export default class CUD extends Component {
} }
} }
if (sourceTypeKey === CampaignSource.TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) { if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
if (!state.getIn(['data_sourceTemplate', 'value'])) { if (!state.getIn(['data_sourceTemplate', 'value'])) {
state.setIn(['data_sourceTemplate', 'error'], t('Template must be selected')); state.setIn(['data_sourceTemplate', 'error'], t('Template must be selected'));
} }
} else if (sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) { } else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
if (!state.getIn(['data_sourceCampaign', 'value'])) { if (!state.getIn(['data_sourceCampaign', 'value'])) {
state.setIn(['data_sourceCampaign', 'error'], t('Campaign must be selected')); state.setIn(['data_sourceCampaign', 'error'], t('Campaign must be selected'));
} }
} else if (sourceTypeKey === CampaignSource.CUSTOM) { } else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM) {
// The type is used only in create form. In case of CUSTOM_FROM_TEMPLATE or CUSTOM_FROM_CAMPAIGN, it is determined by the source template, so no need to check it here // The type is used only in create form. In case of CUSTOM_FROM_TEMPLATE or CUSTOM_FROM_CAMPAIGN, it is determined by the source template, so no need to check it here
const customTemplateTypeKey = state.getIn(['data_sourceCustom_type', 'value']); const customTemplateTypeKey = state.getIn(['data_sourceCustom_type', 'value']);
if (!customTemplateTypeKey) { if (!customTemplateTypeKey) {
@ -302,7 +303,7 @@ export default class CUD extends Component {
let sendMethod, url; let sendMethod, url;
if (this.props.entity) { if (this.props.entity) {
sendMethod = FormSendMethod.PUT; sendMethod = FormSendMethod.PUT;
url = `rest/campaigns/${this.props.entity.id}` url = `rest/campaigns-settings/${this.props.entity.id}`;
} else { } else {
sendMethod = FormSendMethod.POST; sendMethod = FormSendMethod.POST;
url = 'rest/campaigns' url = 'rest/campaigns'
@ -312,6 +313,8 @@ export default class CUD extends Component {
this.setFormStatusMessage('info', t('Saving ...')); this.setFormStatusMessage('info', t('Saving ...'));
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
data.source = Number.parseInt(data.source);
if (!data.useSegmentation) { if (!data.useSegmentation) {
data.segment = null; data.segment = null;
} }
@ -460,18 +463,20 @@ export default class CUD extends Component {
help = t('Selecting a template creates a campaign specific copy from it.'); help = t('Selecting a template creates a campaign specific copy from it.');
} }
templateEdit = <TableSelect id="data_sourceTemplate" label={t('Template')} withHeader dropdown dataUrl='rest/templates-table' columns={templatesColumns} selectionLabelIndex={1} help={help}/>; // The "key" property here and in the TableSelect below is to tell React that these tables are different and should be rendered by different instances. Otherwise, React will use
// only one instance, which fails because Table does not handle updates in "columns" property
templateEdit = <TableSelect key="templateSelect" id="data_sourceTemplate" label={t('Template')} withHeader dropdown dataUrl='rest/templates-table' columns={templatesColumns} selectionLabelIndex={1} help={help}/>;
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) { } else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
const campaignsColumns = [ const campaignsColumns = [
{ data: 1, title: t('Name') }, { data: 1, title: t('Name') },
{ data: 2, title: t('Description') }, { data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.campaignTypes[data] }, { data: 3, title: t('Type'), render: data => this.campaignTypes[data] },
{ data: 7, title: t('Created'), render: data => moment(data).fromNow() }, { data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 8, title: t('Namespace') } { data: 5, title: t('Namespace') }
]; ];
templateEdit = <TableSelect id="data_sourceCampaign" label={t('Campaign')} withHeader dropdown dataUrl='rest/campaigns-table' columns={campaignsColumns} selectionLabelIndex={1} help={t('Content of the selected campaign will be copied into this campaign.')}/>; templateEdit = <TableSelect key="campaignSelect" id="data_sourceCampaign" label={t('Campaign')} withHeader dropdown dataUrl='rest/campaigns-with-content-table' columns={campaignsColumns} selectionLabelIndex={1} help={t('Content of the selected campaign will be copied into this campaign.')}/>;
} else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM) { } else if (!isEdit && sourceTypeKey === CampaignSource.CUSTOM) {
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type'); const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');

View file

@ -88,14 +88,8 @@ export default class CustomContent extends Component {
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type'); const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this); await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
let sendMethod, url; const sendMethod = FormSendMethod.PUT;
if (this.props.entity) { const url = `rest/campaigns-content/${this.props.entity.id}`;
sendMethod = FormSendMethod.PUT;
url = `rest/campaigns/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = 'rest/campaigns'
}
this.disableForm(); this.disableForm();
this.setFormStatusMessage('info', t('Saving ...')); this.setFormStatusMessage('info', t('Saving ...'));

View file

@ -20,7 +20,7 @@ function getMenus(t) {
':campaignId([0-9]+)': { ':campaignId([0-9]+)': {
title: resolved => t('Campaign "{{name}}"', {name: resolved.campaign.name}), title: resolved => t('Campaign "{{name}}"', {name: resolved.campaign.name}),
resolve: { resolve: {
campaign: params => `rest/campaigns/${params.campaignId}` campaign: params => `rest/campaigns-settings/${params.campaignId}`
}, },
link: params => `/campaigns/${params.campaignId}/edit`, link: params => `/campaigns/${params.campaignId}/edit`,
navs: { navs: {
@ -33,20 +33,23 @@ function getMenus(t) {
content: { content: {
title: t('Content'), title: t('Content'),
link: params => `/campaigns/${params.campaignId}/content`, link: params => `/campaigns/${params.campaignId}/content`,
resolve: {
campaignContent: params => `rest/campaigns-content/${params.campaignId}`
},
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), 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 => <Content entity={props.resolved.campaign} /> panelRender: props => <Content entity={props.resolved.campaignContent} />
}, },
files: { files: {
title: t('Files'), title: t('Files'),
link: params => `/campaigns/${params.campaignId}/files`, link: params => `/campaigns/${params.campaignId}/files`,
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), 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 => <Files title={t('Files')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="file" managePermission="manageFiles"/> panelRender: props => <Files title={t('Files')} help={t('These files are publicly available via HTTP so that they can be linked to from the content of the campaign.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="file" managePermission="manageFiles"/>
}, },
attachments: { attachments: {
title: t('Attachments'), title: t('Attachments'),
link: params => `/campaigns/${params.campaignId}/attachments`, link: params => `/campaigns/${params.campaignId}/attachments`,
visible: resolved => resolved.campaign.permissions.includes('viewAttachments'), visible: resolved => resolved.campaign.permissions.includes('viewAttachments'),
panelRender: props => <Files title={t('Attachments')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/> panelRender: props => <Files title={t('Attachments')} help={t('These files will be attached to the campaign emails as proper attachments. This means they count towards to resulting eventual size of the email.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/>
}, },
share: { share: {
title: t('Share'), title: t('Share'),

View file

@ -3,7 +3,10 @@
import React, {Component} from "react"; import React, {Component} from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import {translate} from "react-i18next"; import {translate} from "react-i18next";
import {requiresAuthenticatedUser} from "./page"; import {
requiresAuthenticatedUser,
Title
} from "./page";
import {withErrorHandling} from "./error-handling"; import {withErrorHandling} from "./error-handling";
import {Table} from "./table"; import {Table} from "./table";
import Dropzone from "react-dropzone"; import Dropzone from "react-dropzone";
@ -32,6 +35,7 @@ export default class Files extends Component {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
help: PropTypes.string,
entity: PropTypes.object.isRequired, entity: PropTypes.object.isRequired,
entityTypeId: PropTypes.string.isRequired, entityTypeId: PropTypes.string.isRequired,
entitySubTypeId: PropTypes.string.isRequired, entitySubTypeId: PropTypes.string.isRequired,
@ -151,6 +155,10 @@ export default class Files extends Component {
{t('Are you sure you want to delete file "{{name}}"?', {name: this.state.fileToDeleteName})} {t('Are you sure you want to delete file "{{name}}"?', {name: this.state.fileToDeleteName})}
</ModalDialog> </ModalDialog>
{this.props.title && <Title>{this.props.title}</Title>}
{this.props.help && <p>{this.props.help}</p>}
{ {
this.props.entity.permissions.includes(this.props.managePermission) && this.props.entity.permissions.includes(this.props.managePermission) &&
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName="dropZoneActive"> <Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName="dropZoneActive">

View file

@ -147,8 +147,8 @@ export class MosaicoSandbox extends Component {
}); });
const config = { const config = {
imgProcessorBackend: getTrustedUrl(`mosaico/img/${this.props.entityTypeId}/${this.props.entityId}`), imgProcessorBackend: getTrustedUrl('mosaico/img'),
emailProcessorBackend: getSandboxUrl('mosaico/dl/'), emailProcessorBackend: getSandboxUrl('mosaico/dl'),
fileuploadConfig: { fileuploadConfig: {
url: getSandboxUrl(`mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`) url: getSandboxUrl(`mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`)
}, },

View file

@ -20,6 +20,11 @@
:global .ace_editor { :global .ace_editor {
border: 1px solid #ccc; border: 1px solid #ccc;
} }
.buttonRow:last-child {
// This is to move Save/Delete buttons a bit down
margin-top: 15px;
}
} }
.dayPickerWrapper { .dayPickerWrapper {
@ -27,7 +32,6 @@
} }
.buttonRow { .buttonRow {
margin-top: 15px;
} }
.buttonRow > * { .buttonRow > * {

View file

@ -78,13 +78,20 @@ export default class List extends Component {
}); });
} }
if (perms.includes('manageFields')) { if (perms.includes('viewFields')) {
actions.push({ actions.push({
label: <Icon icon="th-list" title={t('Manage Fields')}/>, label: <Icon icon="th-list" title={t('Fields')}/>,
link: `/lists/${data[0]}/fields` link: `/lists/${data[0]}/fields`
}); });
} }
if (perms.includes('viewSegments')) {
actions.push({
label: <Icon icon="tag" title={t('Segments')}/>,
link: `/lists/${data[0]}/segments`
});
}
if (perms.includes('share')) { if (perms.includes('share')) {
actions.push({ actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>, label: <Icon icon="share-alt" title={t('Share')}/>,

View file

@ -40,18 +40,28 @@ export default class List extends Component {
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false }, { data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
{ data: 3, title: t('Merge Tag') }, { data: 3, title: t('Merge Tag') },
{ {
actions: data => [{ actions: data => {
label: <Icon icon="edit" title={t('Edit')}/>, const actions = [];
link: `/lists/${this.props.list.id}/fields/${data[0]}/edit`
}] if (this.props.list.permissions.includes('manageFields')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/lists/${this.props.list.id}/fields/${data[0]}/edit`
});
}
return actions;
}
} }
]; ];
return ( return (
<div> <div>
<Toolbar> {this.props.list.permissions.includes('manageFields') &&
<NavButton linkTo={`/lists/${this.props.list.id}/fields/create`} className="btn-primary" icon="plus" label={t('Create Field')}/> <Toolbar>
</Toolbar> <NavButton linkTo={`/lists/${this.props.list.id}/fields/create`} className="btn-primary" icon="plus" label={t('Create Field')}/>
</Toolbar>
}
<Title>{t('Fields')}</Title> <Title>{t('Fields')}</Title>

View file

@ -70,7 +70,7 @@ function getMenus(t) {
fields: { fields: {
title: t('Fields'), title: t('Fields'),
link: params => `/lists/${params.listId}/fields/`, link: params => `/lists/${params.listId}/fields/`,
visible: resolved => resolved.list.permissions.includes('manageFields'), visible: resolved => resolved.list.permissions.includes('viewFields'),
panelRender: props => <FieldsList list={props.resolved.list} />, panelRender: props => <FieldsList list={props.resolved.list} />,
children: { children: {
':fieldId([0-9]+)': { ':fieldId([0-9]+)': {
@ -100,7 +100,7 @@ function getMenus(t) {
segments: { segments: {
title: t('Segments'), title: t('Segments'),
link: params => `/lists/${params.listId}/segments`, link: params => `/lists/${params.listId}/segments`,
visible: resolved => resolved.list.permissions.includes('manageSegments'), visible: resolved => resolved.list.permissions.includes('viewSegments'),
panelRender: props => <SegmentsList list={props.resolved.list} />, panelRender: props => <SegmentsList list={props.resolved.list} />,
children: { children: {
':segmentId([0-9]+)': { ':segmentId([0-9]+)': {

View file

@ -32,18 +32,28 @@ export default class List extends Component {
const columns = [ const columns = [
{ data: 1, title: t('Name') }, { data: 1, title: t('Name') },
{ {
actions: data => [{ actions: data => {
label: <Icon icon="edit" title={t('Edit')}/>, const actions = [];
link: `/lists/${this.props.list.id}/segments/${data[0]}/edit`
}] if (this.props.list.permissions.includes('manageSegments')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/lists/${this.props.list.id}/segments/${data[0]}/edit`
});
}
return actions;
}
} }
]; ];
return ( return (
<div> <div>
<Toolbar> {this.props.list.permissions.includes('manageSegments') &&
<NavButton linkTo={`/lists/${this.props.list.id}/segments/create`} className="btn-primary" icon="plus" label={t('Create Segment')}/> <Toolbar>
</Toolbar> <NavButton linkTo={`/lists/${this.props.list.id}/segments/create`} className="btn-primary" icon="plus" label={t('Create Segment')}/>
</Toolbar>
}
<Title>{t('Segment')}</Title> <Title>{t('Segment')}</Title>

View file

@ -37,8 +37,8 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
const initVals = templateTypes[templateType].initData(); const initVals = templateTypes[templateType].initData();
for (const key in initVals) { for (const key in initVals) {
if (!mutState.hasIn([prefix + key])) { if (!mutState.hasIn([key])) {
mutState.setIn([prefix + key, 'value'], initVals[key]); mutState.setIn([key, 'value'], initVals[key]);
} }
} }
} }
@ -97,6 +97,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
}; };
}, },
beforeSave: data => { beforeSave: data => {
console.log(data);
data[prefix + 'data'] = { data[prefix + 'data'] = {
mosaicoTemplate: data[prefix + 'mosaicoTemplate'], mosaicoTemplate: data[prefix + 'mosaicoTemplate'],
metadata: data[prefix + 'mosaicoData'].metadata, metadata: data[prefix + 'mosaicoData'].metadata,
@ -150,8 +151,8 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
}); });
}, },
initData: () => ({ initData: () => ({
mosaicoFsTemplate: mailtrainConfig.mosaico.fsTemplates[0][0], [prefix + 'mosaicoFsTemplate']: mailtrainConfig.mosaico.fsTemplates[0][0],
mosaicoData: {} [prefix + 'mosaicoData']: {}
}), }),
afterLoad: data => { afterLoad: data => {
data[prefix + 'mosaicoFsTemplate'] = data[prefix + 'data'].mosaicoFsTemplate; data[prefix + 'mosaicoFsTemplate'] = data[prefix + 'data'].mosaicoFsTemplate;
@ -169,7 +170,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
clearBeforeSave(data); clearBeforeSave(data);
}, },
afterTypeChange: mutState => { afterTypeChange: mutState => {
initFieldsIfMissing(mutState, 'mosaico'); initFieldsIfMissing(mutState, 'mosaicoWithFsTemplate');
}, },
validate: state => {} validate: state => {}
}; };

View file

@ -34,7 +34,7 @@ function getMenus(t) {
title: t('Files'), title: t('Files'),
link: params => `/templates/${params.templateId}/files`, link: params => `/templates/${params.templateId}/files`,
visible: resolved => resolved.template.permissions.includes('viewFiles'), visible: resolved => resolved.template.permissions.includes('viewFiles'),
panelRender: props => <Files title={t('Files')} entity={props.resolved.template} entityTypeId="template" entitySubTypeId="file" managePermission="manageFiles"/> panelRender: props => <Files title={t('Files')} help={t('These files are publicly available via HTTP so that they can be linked to from the content of the campaign.')} entity={props.resolved.template} entityTypeId="template" entitySubTypeId="file" managePermission="manageFiles"/>
}, },
share: { share: {
title: t('Share'), title: t('Share'),
@ -70,13 +70,13 @@ function getMenus(t) {
title: t('Files'), title: t('Files'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/files`, link: params => `/templates/mosaico/${params.mosaiceTemplateId}/files`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'), visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'),
panelRender: props => <Files title={t('Files')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="file" managePermission="manageFiles" /> panelRender: props => <Files title={t('Files')} help={t('These files are publicly available via HTTP so that they can be linked to from the Mosaico template.')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="file" managePermission="manageFiles" />
}, },
blocks: { blocks: {
title: t('Block thumbnails'), title: t('Block thumbnails'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/blocks`, link: params => `/templates/mosaico/${params.mosaiceTemplateId}/blocks`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'), visible: resolved => resolved.mosaicoTemplate.permissions.includes('viewFiles'),
panelRender: props => <Files title={t('Block thumbnails')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="block" managePermission="manageFiles" /> panelRender: props => <Files title={t('Block thumbnails')} help={t('These files will be used by Mosaico to search for block thumbnails (the "edres" directory). Place here one file per block type that you have defined in the Mosaico template. Each file must have the same name as the block id. The file will be used as the thumbnail of the corresponding block.')}entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" entitySubTypeId="block" managePermission="manageFiles" />
}, },
share: { share: {
title: t('Share'), title: t('Share'),

View file

@ -2,7 +2,6 @@
const passport = require('./passport'); const passport = require('./passport');
const config = require('config'); const config = require('config');
const permissions = require('./permissions');
const forms = require('../models/forms'); const forms = require('../models/forms');
const shares = require('../models/shares'); const shares = require('../models/shares');
const urls = require('./urls'); const urls = require('./urls');

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const permissions = require('../lib/permissions'); const entitySettings = require('./entity-settings');
async function ajaxListTx(tx, params, queryFun, columns, options) { async function ajaxListTx(tx, params, queryFun, columns, options) {
options = options || {}; options = options || {};
@ -109,7 +109,7 @@ async function ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryF
const permCols = []; const permCols = [];
for (const fetchSpec of fetchSpecs) { for (const fetchSpec of fetchSpecs) {
const entityType = permissions.getEntityType(fetchSpec.entityTypeId); const entityType = entitySettings.getEntityType(fetchSpec.entityTypeId);
permCols.push({ permCols.push({
name: `permissions_${fetchSpec.entityTypeId}`, name: `permissions_${fetchSpec.entityTypeId}`,
query: builder => builder query: builder => builder
@ -128,7 +128,7 @@ async function ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryF
let query = queryFun(builder); let query = queryFun(builder);
for (const fetchSpec of fetchSpecs) { for (const fetchSpec of fetchSpecs) {
const entityType = permissions.getEntityType(fetchSpec.entityTypeId); const entityType = entitySettings.getEntityType(fetchSpec.entityTypeId);
if (fetchSpec.requiredOperations) { if (fetchSpec.requiredOperations) {
query = query.innerJoin( query = query.innerJoin(

View file

@ -1,5 +1,11 @@
'use strict'; 'use strict';
const ReplacementBehavior = {
NONE: 1,
REPLACE: 2,
RENAME: 3
};
const entityTypes = { const entityTypes = {
namespace: { namespace: {
entitiesTable: 'namespaces', entitiesTable: 'namespaces',
@ -26,14 +32,16 @@ const entityTypes = {
permissions: { permissions: {
view: 'viewFiles', view: 'viewFiles',
manage: 'manageFiles' manage: 'manageFiles'
} },
defaultReplacementBehavior: ReplacementBehavior.REPLACE
}, },
attachment: { attachment: {
table: 'files_campaign_attachment', table: 'files_campaign_attachment',
permissions: { permissions: {
view: 'viewAttachments', view: 'viewAttachments',
manage: 'manageAttachments' manage: 'manageAttachments'
} },
defaultReplacementBehavior: ReplacementBehavior.NONE
} }
} }
}, },
@ -47,7 +55,8 @@ const entityTypes = {
permissions: { permissions: {
view: 'viewFiles', view: 'viewFiles',
manage: 'manageFiles' manage: 'manageFiles'
} },
defaultReplacementBehavior: ReplacementBehavior.REPLACE
} }
} }
}, },
@ -76,14 +85,16 @@ const entityTypes = {
permissions: { permissions: {
view: 'viewFiles', view: 'viewFiles',
manage: 'manageFiles' manage: 'manageFiles'
} },
defaultReplacementBehavior: ReplacementBehavior.REPLACE
}, },
block: { block: {
table: 'files_mosaico_template_block', table: 'files_mosaico_template_block',
permissions: { permissions: {
view: 'viewFiles', view: 'viewFiles',
manage: 'manageFiles' manage: 'manageFiles'
} },
defaultReplacementBehavior: ReplacementBehavior.REPLACE
} }
} }
} }
@ -105,5 +116,6 @@ function getEntityType(entityTypeId) {
module.exports = { module.exports = {
getEntityTypes, getEntityTypes,
getEntityType getEntityType,
ReplacementBehavior
} }

View file

@ -10,7 +10,7 @@ const multer = require('multer')({
dest: uploadedFilesDir dest: uploadedFilesDir
}); });
function installUploadHandler(router, url, replacementBehavior, type = null, subType = null) { function installUploadHandler(router, url, replacementBehavior, type, subType) {
router.postAsync(url, passport.loggedIn, multer.array('files[]'), async (req, res) => { router.postAsync(url, passport.loggedIn, multer.array('files[]'), async (req, res) => {
return res.json(await files.createFiles(req.context, type || req.params.type, subType || req.params.subType, req.params.entityId, req.files, replacementBehavior)); return res.json(await files.createFiles(req.context, type || req.params.type, subType || req.params.subType, req.params.entityId, req.files, replacementBehavior));
}); });

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const knex = require('./knex');
const { enforce } = require('./helpers'); const { enforce } = require('./helpers');
const shares = require('../models/shares');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');
async function validateEntity(tx, entity) { async function validateEntity(tx, entity) {

View file

@ -19,8 +19,32 @@ const allowedKeysCommon = ['name', 'description', 'list', 'segment', 'namespace'
const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]); const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
const allowedKeysUpdate = new Set([...allowedKeysCommon]); const allowedKeysUpdate = new Set([...allowedKeysCommon]);
function hash(entity) { const Content = {
return hasher.hash(filterObject(entity, allowedKeysUpdate)); 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) { 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'); 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) { if (withPermissions) {
entity.permissions = await shares.getPermissionsTx(tx, context, 'campaign', id); entity.permissions = await shares.getPermissionsTx(tx, context, 'campaign', id);
} }
entity.data = JSON.parse(entity.data);
return entity; 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 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) { async function _validateAndPreprocess(tx, context, entity, isCreate, content) {
await namespaceHelpers.validateEntity(tx, entity); if (content === Content.ALL || content === Content.WITHOUT_SOURCE_CUSTOM) {
await namespaceHelpers.validateEntity(tx, entity);
if (isCreate) { 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) { if (entity.source === CampaignSource.TEMPLATE || entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view'); 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 (sourceCustom.type === 'mosaico' || sourceCustom.type === 'mosaicoWithFsTemplate') {
sourceCustom.data.model = convertText(sourceCustom.data.model);
if (entity.segment) { sourceCustom.data.model = convertText(sourceCustom.data.model);
// Check that the segment under the list exists sourceCustom.data.metadata = convertText(sourceCustom.data.metadata);
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);
} }
async function create(context, entity) { async function create(context, entity) {
@ -97,6 +176,7 @@ async function create(context, entity) {
html: template.html, html: template.html,
text: template.text text: template.text
}; };
} else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) { } else if (entity.source === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
copyFilesFrom = { copyFilesFrom = {
entityType: 'campaign', entityType: 'campaign',
@ -104,15 +184,19 @@ async function create(context, entity) {
}; };
const sourceCampaign = await getByIdTx(tx, context, entity.data.sourceCampaign, false); 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; 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); const filteredEntity = filterObject(entity, allowedKeysCreate);
filteredEntity.cid = shortid.generate(); filteredEntity.cid = shortid.generate();
const data = filteredEntity.data;
filteredEntity.data = JSON.stringify(filteredEntity.data);
const ids = await tx('campaigns').insert(filteredEntity); const ids = await tx('campaigns').insert(filteredEntity);
const id = ids[0]; const id = ids[0];
@ -150,14 +234,20 @@ async function create(context, entity) {
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id }); await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
if (copyFilesFrom) { 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; return id;
}); });
} }
async function updateWithConsistencyCheck(context, entity) { async function updateWithConsistencyCheck(context, entity, content) {
await knex.transaction(async tx => { await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.id, 'edit'); await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.id, 'edit');
@ -167,16 +257,31 @@ async function updateWithConsistencyCheck(context, entity) {
} }
existing.data = JSON.parse(existing.data); existing.data = JSON.parse(existing.data);
const existingHash = hash(existing); const existingHash = hash(existing, content);
if (existingHash !== entity.originalHash) { if (existingHash !== entity.originalHash) {
throw new interoperableErrors.ChangedError(); 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 }); await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: entity.id });
}); });
@ -196,8 +301,10 @@ async function remove(context, id) {
module.exports = { module.exports = {
Content,
hash, hash,
listDTAjax, listDTAjax,
listWithContentDTAjax,
getByIdTx, getByIdTx,
getById, getById,
create, create,

View file

@ -7,22 +7,18 @@ const shares = require('./shares');
const fs = require('fs-extra-promise'); const fs = require('fs-extra-promise');
const path = require('path'); const path = require('path');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');
const permissions = require('../lib/permissions'); const entitySettings = require('../lib/entity-settings');
const {getTrustedUrl} = require('../lib/urls'); const {getTrustedUrl} = require('../lib/urls');
const crypto = require('crypto'); const crypto = require('crypto');
const bluebird = require('bluebird'); const bluebird = require('bluebird');
const cryptoPseudoRandomBytes = bluebird.promisify(crypto.pseudoRandomBytes); const cryptoPseudoRandomBytes = bluebird.promisify(crypto.pseudoRandomBytes);
const entityTypes = permissions.getEntityTypes(); const entityTypes = entitySettings.getEntityTypes();
const filesDir = path.join(__dirname, '..', 'files'); const filesDir = path.join(__dirname, '..', 'files');
const ReplacementBehavior = { const ReplacementBehavior = entitySettings.ReplacementBehavior;
NONE: 0,
REPLACE: 1,
RENAME: 2
};
function enforceTypePermitted(type, subType) { function enforceTypePermitted(type, subType) {
enforce(type in entityTypes && entityTypes[type].files && entityTypes[type].files[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) return await _getFileBy(context, type, subType, entityId, 'filename', name)
} }
async function getFileByUrl(context, type, subType, entityId, url) { async function getFileByUrl(context, url) {
const urlPrefix = getTrustedUrl(`files/${type}/${subType}/${entityId}/`, context); const urlPrefix = getTrustedUrl('files/', context);
if (url.startsWith(urlPrefix)) { 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); return await getFileByFilename(context, type, subType, entityId, name);
} else { } else {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
@ -126,6 +138,10 @@ async function createFiles(context, type, subType, entityId, files, replacementB
return {uploaded: 0}; return {uploaded: 0};
} }
if (!replacementBehavior) {
replacementBehavior = entityTypes[type].files[subType].defaultReplacementBehavior;
}
const fileEntities = []; const fileEntities = [];
const filesToMove = []; const filesToMove = [];
const ignoredFiles = []; const ignoredFiles = [];
@ -280,7 +296,9 @@ async function copyAllTx(tx, context, fromType, fromSubType, fromEntityId, toTyp
row.entity = toEntityId; row.entity = toEntityId;
} }
await tx(getFilesTable(toType, toSubType)).insert(rows); if (rows.length > 0) {
await tx(getFilesTable(toType, toSubType)).insert(rows);
}
} }

View file

@ -5,7 +5,7 @@ const hasher = require('node-object-hash')();
const { enforce, filterObject } = require('../lib/helpers'); const { enforce, filterObject } = require('../lib/helpers');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');
const shares = require('./shares'); const shares = require('./shares');
const permissions = require('../lib/permissions'); const entitySettings = require('../lib/entity-settings');
const namespaceHelpers = require('../lib/namespace-helpers'); const namespaceHelpers = require('../lib/namespace-helpers');
@ -14,7 +14,7 @@ const allowedKeys = new Set(['name', 'description', 'namespace']);
async function listTree(context) { async function listTree(context) {
// FIXME - process permissions // 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 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 // This goes in three steps: 1) tree with all namespaces is built with parent-children links, 2) the namespaces that are not accessible

View file

@ -4,7 +4,7 @@ const knex = require('../lib/knex');
const config = require('config'); const config = require('config');
const { enforce } = require('../lib/helpers'); const { enforce } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-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 interoperableErrors = require('../shared/interoperable-errors');
const log = require('npmlog'); const log = require('npmlog');
const {getGlobalNamespaceId} = require('../shared/namespaces'); const {getGlobalNamespaceId} = require('../shared/namespaces');
@ -15,7 +15,7 @@ const {getGlobalNamespaceId} = require('../shared/namespaces');
async function listByEntityDTAjax(context, entityTypeId, entityId, params) { async function listByEntityDTAjax(context, entityTypeId, entityId, params) {
return await knex.transaction(async (tx) => { return await knex.transaction(async (tx) => {
const entityType = permissions.getEntityType(entityTypeId); const entityType = entitySettings.getEntityType(entityTypeId);
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share'); await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
return await dtHelpers.ajaxListTx( return await dtHelpers.ajaxListTx(
@ -41,7 +41,7 @@ async function listByUserDTAjax(context, entityTypeId, userId, params) {
await enforceEntityPermissionTx(tx, context, 'namespace', user.namespace, 'manageUsers'); await enforceEntityPermissionTx(tx, context, 'namespace', user.namespace, 'manageUsers');
const entityType = permissions.getEntityType(entityTypeId); const entityType = entitySettings.getEntityType(entityTypeId);
return await dtHelpers.ajaxListWithPermissionsTx( return await dtHelpers.ajaxListWithPermissionsTx(
tx, tx,
@ -61,7 +61,7 @@ async function listByUserDTAjax(context, entityTypeId, userId, params) {
async function listUnassignedUsersDTAjax(context, entityTypeId, entityId, params) { async function listUnassignedUsersDTAjax(context, entityTypeId, entityId, params) {
return await knex.transaction(async (tx) => { return await knex.transaction(async (tx) => {
const entityType = permissions.getEntityType(entityTypeId); const entityType = entitySettings.getEntityType(entityTypeId);
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share'); await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
@ -93,7 +93,7 @@ async function listRolesDTAjax(entityTypeId, params) {
} }
async function assign(context, entityTypeId, entityId, userId, role) { async function assign(context, entityTypeId, entityId, userId, role) {
const entityType = permissions.getEntityType(entityTypeId); const entityType = entitySettings.getEntityType(entityTypeId);
await knex.transaction(async tx => { await knex.transaction(async tx => {
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share'); await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
@ -129,17 +129,17 @@ async function assign(context, entityTypeId, entityId, userId, role) {
async function rebuildPermissionsTx(tx, restriction) { async function rebuildPermissionsTx(tx, restriction) {
restriction = restriction || {}; restriction = restriction || {};
const namespaceEntityType = permissions.getEntityType('namespace'); const namespaceEntityType = entitySettings.getEntityType('namespace');
// Collect entity types we care about // Collect entity types we care about
let restrictedEntityTypes; let restrictedEntityTypes;
if (restriction.entityTypeId) { if (restriction.entityTypeId) {
const entityType = permissions.getEntityType(restriction.entityTypeId); const entityType = entitySettings.getEntityType(restriction.entityTypeId);
restrictedEntityTypes = { restrictedEntityTypes = {
[restriction.entityTypeId]: entityType [restriction.entityTypeId]: entityType
}; };
} else { } else {
restrictedEntityTypes = permissions.getEntityTypes(); restrictedEntityTypes = entitySettings.getEntityTypes();
} }
@ -374,7 +374,7 @@ async function regenerateRoleNamesTable() {
await knex.transaction(async tx => { await knex.transaction(async tx => {
await tx('generated_role_names').del(); 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) { for (const entityTypeId of entityTypeIds) {
const roles = config.roles[entityTypeId]; const roles = config.roles[entityTypeId];
@ -397,7 +397,7 @@ function throwPermissionDenied() {
} }
async function removeDefaultShares(tx, user) { async function removeDefaultShares(tx, user) {
const namespaceEntityType = permissions.getEntityType('namespace'); const namespaceEntityType = entitySettings.getEntityType('namespace');
const roleConf = config.roles.global[user.role]; const roleConf = config.roles.global[user.role];
@ -467,7 +467,7 @@ async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredO
return false; return false;
} }
const entityType = permissions.getEntityType(entityTypeId); const entityType = entitySettings.getEntityType(entityTypeId);
if (typeof requiredOperations === 'string') { if (typeof requiredOperations === 'string') {
requiredOperations = [ requiredOperations ]; 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'); 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) const rows = await tx(entityType.permissionsTable)
.select('operation') .select('operation')

View file

@ -201,7 +201,7 @@ function getRouter(trusted) {
}); });
} else { } else {
router.getAsync('/img/:type/:entityId', async (req, res) => { router.getAsync('/img', async (req, res) => {
const method = req.query.method; const method = req.query.method;
const params = req.query.params; const params = req.query.params;
let [width, height] = params.split(','); let [width, height] = params.split(',');
@ -225,7 +225,7 @@ function getRouter(trusted) {
if (url.startsWith(mosaicoLegacyUrlPrefix)) { if (url.startsWith(mosaicoLegacyUrlPrefix)) {
filePath = path.join(__dirname, '..', 'client', 'public' , 'mosaico', 'uploads', url.substring(mosaicoLegacyUrlPrefix.length)); filePath = path.join(__dirname, '..', 'client', 'public' , 'mosaico', 'uploads', url.substring(mosaicoLegacyUrlPrefix.length));
} else { } else {
const file = await files.getFileByUrl(contextHelpers.getAdminContext(), req.params.type, 'file', req.params.entityId, url); const file = await files.getFileByUrl(contextHelpers.getAdminContext(), url);
filePath = file.path; filePath = file.path;
} }

View file

@ -10,9 +10,19 @@ router.postAsync('/campaigns-table', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.listDTAjax(req.context, req.body)); return res.json(await campaigns.listDTAjax(req.context, req.body));
}); });
router.getAsync('/campaigns/:campaignId', passport.loggedIn, async (req, res) => { router.postAsync('/campaigns-with-content-table', passport.loggedIn, async (req, res) => {
const campaign = await campaigns.getById(req.context, req.params.campaignId); return res.json(await campaigns.listWithContentDTAjax(req.context, req.body));
campaign.hash = campaigns.hash(campaign); });
router.getAsync('/campaigns-settings/:campaignId', passport.loggedIn, async (req, res) => {
const campaign = await campaigns.getById(req.context, req.params.campaignId, true, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
campaign.hash = campaigns.hash(campaign, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
return res.json(campaign);
});
router.getAsync('/campaigns-content/:campaignId', passport.loggedIn, async (req, res) => {
const campaign = await campaigns.getById(req.context, req.params.campaignId, true, campaigns.Content.ONLY_SOURCE_CUSTOM);
campaign.hash = campaigns.hash(campaign, campaigns.Content.ONLY_SOURCE_CUSTOM);
return res.json(campaign); return res.json(campaign);
}); });
@ -20,11 +30,19 @@ router.postAsync('/campaigns', passport.loggedIn, passport.csrfProtection, async
return res.json(await campaigns.create(req.context, req.body)); return res.json(await campaigns.create(req.context, req.body));
}); });
router.putAsync('/campaigns/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.putAsync('/campaigns-settings/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const entity = req.body; const entity = req.body;
entity.id = parseInt(req.params.campaignId); entity.id = parseInt(req.params.campaignId);
await campaigns.updateWithConsistencyCheck(req.context, entity); await campaigns.updateWithConsistencyCheck(req.context, entity, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
return res.json();
});
router.putAsync('/campaigns-content/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const entity = req.body;
entity.id = parseInt(req.params.campaignId);
await campaigns.updateWithConsistencyCheck(req.context, entity, campaigns.Content.ONLY_SOURCE_CUSTOM);
return res.json(); return res.json();
}); });

View file

@ -20,6 +20,6 @@ router.deleteAsync('/files/:type/:subType/:fileId', passport.loggedIn, async (re
return res.json(); return res.json();
}); });
fileHelpers.installUploadHandler(router, '/files/:type/:subType/:entityId', files.ReplacementBehavior.REPLACE); fileHelpers.installUploadHandler(router, '/files/:type/:subType/:entityId');
module.exports = router; module.exports = router;

View file

@ -3,7 +3,6 @@
const passport = require('../../lib/passport'); const passport = require('../../lib/passport');
const _ = require('../../lib/translate')._; const _ = require('../../lib/translate')._;
const shares = require('../../models/shares'); const shares = require('../../models/shares');
const permissions = require('../../lib/permissions');
const router = require('../../lib/router-async').create(); const router = require('../../lib/router-async').create();

View file

@ -940,7 +940,7 @@ async function migrateAttachments(knex) {
data: attachment.content data: attachment.content
}); });
} }
await files.createFiles(contextHelpers.getAdminContext(), 'campaign', 'attachment', campaign.id, attachmentFiles, files.ReplacementBehavior.NONE); await files.createFiles(contextHelpers.getAdminContext(), 'campaign', 'attachment', campaign.id, attachmentFiles);
} }
await knex.schema.dropTableIfExists('attachments'); await knex.schema.dropTableIfExists('attachments');

View file

@ -11,7 +11,12 @@ function base(text, trustedBaseUrl, sandboxBaseUrl) {
sandboxBaseUrl = sandboxBaseUrl.substring(0, sandboxBaseUrl.length - 1); sandboxBaseUrl = sandboxBaseUrl.substring(0, sandboxBaseUrl.length - 1);
} }
return text.split('[URL_BASE]').join(trustedBaseUrl).split('[SANDBOX_URL_BASE]').join(sandboxBaseUrl); text = text.split('[URL_BASE]').join(trustedBaseUrl);
text = text.split('[SANDBOX_URL_BASE]').join(sandboxBaseUrl);
text = text.split('[ENCODED_URL_BASE]').join(encodeURIComponent(trustedBaseUrl));
text = text.split('[ENCODED_SANDBOX_URL_BASE]').join(encodeURIComponent(sandboxBaseUrl));
return text;
} }
function unbase(text, trustedBaseUrl, sandboxBaseUrl, treatSandboxAsTrusted = false) { function unbase(text, trustedBaseUrl, sandboxBaseUrl, treatSandboxAsTrusted = false) {
@ -23,7 +28,12 @@ function unbase(text, trustedBaseUrl, sandboxBaseUrl, treatSandboxAsTrusted = fa
sandboxBaseUrl = sandboxBaseUrl.substring(0, sandboxBaseUrl.length - 1); sandboxBaseUrl = sandboxBaseUrl.substring(0, sandboxBaseUrl.length - 1);
} }
return text.split(trustedBaseUrl).join('[URL_BASE]').split(sandboxBaseUrl).join(treatSandboxAsTrusted ? '[URL_BASE]' : '[SANDBOX_URL_BASE]'); text = text.split(trustedBaseUrl).join('[URL_BASE]');
text = text.split(sandboxBaseUrl).join(treatSandboxAsTrusted ? '[URL_BASE]' : '[SANDBOX_URL_BASE]');
text = text.split(encodeURIComponent(trustedBaseUrl)).join('[ENCODED_URL_BASE]');
text = text.split(encodeURIComponent(sandboxBaseUrl)).join(treatSandboxAsTrusted ? '[ENCODED_URL_BASE]' : '[ENCODED_SANDBOX_URL_BASE]');
return text;
} }
module.exports = { module.exports = {