diff --git a/client/src/campaigns/CUD.js b/client/src/campaigns/CUD.js index 945965a9..3ce6b395 100644 --- a/client/src/campaigns/CUD.js +++ b/client/src/campaigns/CUD.js @@ -721,7 +721,7 @@ export default class CUD extends Component { templateEdit =
- + {customTemplateTypeForm}
; diff --git a/client/src/campaigns/Content.js b/client/src/campaigns/Content.js index 4880538a..3b654ce6 100644 --- a/client/src/campaigns/Content.js +++ b/client/src/campaigns/Content.js @@ -277,7 +277,7 @@ export default class CustomContent extends Component { {customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName} - + {customTemplateTypeKey && getTypeForm(this, customTemplateTypeKey, true)} diff --git a/client/src/campaigns/Status.js b/client/src/campaigns/Status.js index c20c7486..ef1c9617 100644 --- a/client/src/campaigns/Status.js +++ b/client/src/campaigns/Status.js @@ -10,7 +10,7 @@ import {getCampaignLabels} from './helpers'; import {Table} from "../lib/table"; import {Button, Icon, ModalDialog} from "../lib/bootstrap-components"; import axios from "../lib/axios"; -import {getPublicUrl, getUrl} from "../lib/urls"; +import {getPublicUrl, getSandboxUrl, getUrl} from "../lib/urls"; import interoperableErrors from '../../../shared/interoperable-errors'; import {CampaignStatus, CampaignType} from "../../../shared/campaigns"; import moment from 'moment-timezone'; @@ -58,10 +58,29 @@ class PreviewForTestUserModalDialog extends Component { async previewAsync() { if (this.isFormWithoutErrors()) { - const campaignCid = this.props.entity.cid; + const entity = this.props.entity; + const campaignCid = entity.cid; const [listCid, subscriptionCid] = this.getFormValue('testUser').split(':'); - window.open(getPublicUrl(`archive/${campaignCid}/${listCid}/${subscriptionCid}`, {withLocale: true}), '_blank'); + if (entity.type === CampaignType.RSS) { + const result = await axios.post(getUrl('rest/restricted-access-token'), { + method: 'rssPreview', + params: { + campaignCid, + listCid + } + }); + + const accessToken = result.data; + window.open(getSandboxUrl(`cpgs/rss-preview/${campaignCid}/${listCid}/${subscriptionCid}`, accessToken, {withLocale: true}), '_blank'); + + } else if (entity.type === CampaignType.REGULAR) { + window.open(getPublicUrl(`archive/${campaignCid}/${listCid}/${subscriptionCid}`, {withLocale: true}), '_blank'); + + } else { + throw new Error('Preview not supported'); + } + } else { this.showFormValidation(); } diff --git a/client/src/lib/sandbox-common.js b/client/src/lib/sandbox-common.js new file mode 100644 index 00000000..a5b93441 --- /dev/null +++ b/client/src/lib/sandbox-common.js @@ -0,0 +1,9 @@ +'use strict'; + +export function getTagLanguageFromEntity(entity, entityTypeId) { + if (entityTypeId === 'template') { + return entity.tag_language; + } else if (entityTypeId === 'campaign') { + return entity.data.sourceCustom.tag_language; + } +} \ No newline at end of file diff --git a/client/src/lib/sandboxed-ckeditor-root.js b/client/src/lib/sandboxed-ckeditor-root.js index 6ab2f49f..d09159b9 100644 --- a/client/src/lib/sandboxed-ckeditor-root.js +++ b/client/src/lib/sandboxed-ckeditor-root.js @@ -27,7 +27,7 @@ class CKEditorSandbox extends Component { const trustedUrlBase = getTrustedUrl(); const sandboxUrlBase = getSandboxUrl(); const publicUrlBase = getPublicUrl(); - const source = this.props.initialSource && base(this.props.initialSource, trustedUrlBase, sandboxUrlBase, publicUrlBase); + const source = this.props.initialSource && base(this.props.initialSource, this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase); this.state = { source @@ -37,6 +37,7 @@ class CKEditorSandbox extends Component { static propTypes = { entityTypeId: PropTypes.string, entityId: PropTypes.number, + tagLanguage: PropTypes.string, initialSource: PropTypes.string } @@ -48,7 +49,7 @@ class CKEditorSandbox extends Component { const preHtml = ''; const postHtml = ''; - const unbasedSource = unbase(this.state.source, trustedUrlBase, sandboxUrlBase, publicUrlBase, true); + const unbasedSource = unbase(this.state.source, this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase, true); return { source: unbasedSource, diff --git a/client/src/lib/sandboxed-ckeditor.js b/client/src/lib/sandboxed-ckeditor.js index 7e0c96d1..f9b644af 100644 --- a/client/src/lib/sandboxed-ckeditor.js +++ b/client/src/lib/sandboxed-ckeditor.js @@ -11,6 +11,7 @@ import {getTrustedUrl} from "./urls"; import {initialHeight} from "./sandboxed-ckeditor-shared"; import {withComponentMixins} from "./decorator-helpers"; +import {getTagLanguageFromEntity} from "./sandbox-common"; const navbarHeight = 34; // Sync this with navbarheight in sandboxed-ckeditor.scss @@ -83,6 +84,7 @@ export class CKEditorHost extends Component { const editorData = { entityTypeId: this.props.entityTypeId, entityId: this.props.entity.id, + tagLanguage: getTagLanguageFromEntity(this.props.entity, this.props.entityTypeId), initialSource: this.props.initialSource }; diff --git a/client/src/lib/sandboxed-codeeditor-root.js b/client/src/lib/sandboxed-codeeditor-root.js index 97326e06..1501985d 100644 --- a/client/src/lib/sandboxed-codeeditor-root.js +++ b/client/src/lib/sandboxed-codeeditor-root.js @@ -66,7 +66,7 @@ class CodeEditorSandbox extends Component { const trustedUrlBase = getTrustedUrl(); const sandboxUrlBase = getSandboxUrl(); const publicUrlBase = getPublicUrl(); - const source = this.props.initialSource ? base(this.props.initialSource, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultSource; + const source = this.props.initialSource ? base(this.props.initialSource, this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultSource; this.state = { source, @@ -87,6 +87,7 @@ class CodeEditorSandbox extends Component { static propTypes = { entityTypeId: PropTypes.string, entityId: PropTypes.number, + tagLanguage: PropTypes.string, initialSource: PropTypes.string, sourceType: PropTypes.string, initialPreview: PropTypes.bool, @@ -98,8 +99,8 @@ class CodeEditorSandbox extends Component { const sandboxUrlBase = getSandboxUrl(); const publicUrlBase = getPublicUrl(); return { - html: unbase(this.getHtml(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true), - source: unbase(this.state.source, trustedUrlBase, sandboxUrlBase, publicUrlBase, true) + html: unbase(this.getHtml(), this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase, true), + source: unbase(this.state.source, this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase, true) }; } @@ -149,7 +150,7 @@ class CodeEditorSandbox extends Component { }); if (!this.refreshTimeoutId) { - this.refreshTimeoutId = setTimeout(() => this.refresh(), refreshTimeout); + this.refreshTimeoutId = setTimeout(this.refreshHandler, refreshTimeout); } } diff --git a/client/src/lib/sandboxed-codeeditor.js b/client/src/lib/sandboxed-codeeditor.js index 21d59a8a..3f2f5420 100644 --- a/client/src/lib/sandboxed-codeeditor.js +++ b/client/src/lib/sandboxed-codeeditor.js @@ -9,6 +9,7 @@ import {UntrustedContentHost} from './untrusted'; import {Icon} from "./bootstrap-components"; import {getTrustedUrl} from "./urls"; import {withComponentMixins} from "./decorator-helpers"; +import {getTagLanguageFromEntity} from "./sandbox-common"; @withComponentMixins([ withTranslation @@ -75,6 +76,7 @@ export class CodeEditorHost extends Component { const editorData = { entityTypeId: this.props.entityTypeId, entityId: this.props.entity.id, + tagLanguage: getTagLanguageFromEntity(this.props.entity, this.props.entityTypeId), initialSource: this.props.initialSource, sourceType: this.props.sourceType, initialPreview: this.state.preview, diff --git a/client/src/lib/sandboxed-grapesjs-root.js b/client/src/lib/sandboxed-grapesjs-root.js index f9c2f76d..00d89fe6 100644 --- a/client/src/lib/sandboxed-grapesjs-root.js +++ b/client/src/lib/sandboxed-grapesjs-root.js @@ -54,6 +54,7 @@ export class GrapesJSSandbox extends Component { static propTypes = { entityTypeId: PropTypes.string, entityId: PropTypes.number, + tagLanguage: PropTypes.string, initialSource: PropTypes.string, initialStyle: PropTypes.string, sourceType: PropTypes.string @@ -75,8 +76,8 @@ export class GrapesJSSandbox extends Component { const sandboxUrlBase = getSandboxUrl(); const publicUrlBase = getPublicUrl(); - const source = unbase(editor.getHtml(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true); - const style = unbase(editor.getCss(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true); + const source = unbase(editor.getHtml(), this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase, true); + const style = unbase(editor.getCss(), this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase, true); let html; @@ -96,7 +97,7 @@ export class GrapesJSSandbox extends Component { const preHtml = ''; const postHtml = ''; - html = preHtml + unbase(htmlBody, trustedUrlBase, sandboxUrlBase, publicUrlBase, true) + postHtml; + html = preHtml + unbase(htmlBody, this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase, true) + postHtml; } @@ -603,8 +604,8 @@ export class GrapesJSSandbox extends Component { config.plugins.push('gjs-preset-newsletter'); } - config.components = props.initialSource ? base(props.initialSource, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultSource; - config.style = props.initialStyle ? base(props.initialStyle, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultStyle; + config.components = props.initialSource ? base(props.initialSource, this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultSource; + config.style = props.initialStyle ? base(props.initialStyle, this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultStyle; config.plugins.push('mailtrain-remove-buttons'); diff --git a/client/src/lib/sandboxed-grapesjs.js b/client/src/lib/sandboxed-grapesjs.js index 42fa23a4..d3de59e3 100644 --- a/client/src/lib/sandboxed-grapesjs.js +++ b/client/src/lib/sandboxed-grapesjs.js @@ -10,6 +10,7 @@ import {Icon} from "./bootstrap-components"; import {getTrustedUrl} from "./urls"; import {withComponentMixins} from "./decorator-helpers"; import {GrapesJSSourceType} from "./sandboxed-grapesjs-shared"; +import {getTagLanguageFromEntity} from "./sandbox-common"; @withComponentMixins([ withTranslation @@ -57,6 +58,7 @@ export class GrapesJSHost extends Component { const editorData = { entityTypeId: this.props.entityTypeId, entityId: this.props.entity.id, + tagLanguage: getTagLanguageFromEntity(this.props.entity, this.props.entityTypeId), initialSource: this.props.initialSource, initialStyle: this.props.initialStyle, sourceType: this.props.sourceType diff --git a/client/src/lib/sandboxed-mosaico-root.js b/client/src/lib/sandboxed-mosaico-root.js index 4a3dfd5a..78bddac0 100644 --- a/client/src/lib/sandboxed-mosaico-root.js +++ b/client/src/lib/sandboxed-mosaico-root.js @@ -27,6 +27,7 @@ class MosaicoSandbox extends Component { static propTypes = { entityTypeId: PropTypes.string, entityId: PropTypes.number, + tagLanguage: PropTypes.string, templateId: PropTypes.number, templatePath: PropTypes.string, initialModel: PropTypes.string, @@ -60,9 +61,9 @@ class MosaicoSandbox extends Component { html = juice(html); return { - html: unbase(html, trustedUrlBase, sandboxUrlBase, publicUrlBase, true), - model: unbase(this.viewModel.exportJSON(), trustedUrlBase, sandboxUrlBase, publicUrlBase), - metadata: unbase(this.viewModel.exportMetadata(), trustedUrlBase, sandboxUrlBase, publicUrlBase) + html: unbase(html, this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase, true), + model: unbase(this.viewModel.exportJSON(), this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase), + metadata: unbase(this.viewModel.exportMetadata(), this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase) }; } @@ -128,8 +129,8 @@ class MosaicoSandbox extends Component { const trustedUrlBase = getTrustedUrl(); const sandboxUrlBase = getSandboxUrl(); const publicUrlBase = getPublicUrl(); - const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, trustedUrlBase, sandboxUrlBase, publicUrlBase)); - const model = this.props.initialModel && JSON.parse(base(this.props.initialModel, trustedUrlBase, sandboxUrlBase, publicUrlBase)); + const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase)); + const model = this.props.initialModel && JSON.parse(base(this.props.initialModel, this.props.tagLanguage, trustedUrlBase, sandboxUrlBase, publicUrlBase)); const template = this.props.templateId ? getSandboxUrl(`mosaico/templates/${this.props.templateId}/index.html`) : this.props.templatePath; const allPlugins = plugins.concat(window.mosaicoPlugins); diff --git a/client/src/lib/sandboxed-mosaico.js b/client/src/lib/sandboxed-mosaico.js index 7704c0d1..352da1fd 100644 --- a/client/src/lib/sandboxed-mosaico.js +++ b/client/src/lib/sandboxed-mosaico.js @@ -9,6 +9,7 @@ import {UntrustedContentHost} from './untrusted'; import {Icon} from "./bootstrap-components"; import {getTrustedUrl} from "./urls"; import {withComponentMixins} from "./decorator-helpers"; +import {getTagLanguageFromEntity} from "./sandbox-common"; @withComponentMixins([ @@ -58,6 +59,7 @@ export class MosaicoHost extends Component { const editorData = { entityTypeId: this.props.entityTypeId, entityId: this.props.entity.id, + tagLanguage: getTagLanguageFromEntity(this.props.entity, this.props.entityTypeId), templateId: this.props.templateId, templatePath: this.props.templatePath, initialModel: this.props.initialModel, diff --git a/client/src/lib/urls.js b/client/src/lib/urls.js index 242a2eaa..b8c5270f 100644 --- a/client/src/lib/urls.js +++ b/client/src/lib/urls.js @@ -15,9 +15,15 @@ function getTrustedUrl(path) { return mailtrainConfig.trustedUrlBase + (path || ''); } -function getSandboxUrl(path, customRestrictedAccessToken) { +function getSandboxUrl(path, customRestrictedAccessToken, opts) { const localRestrictedAccessToken = customRestrictedAccessToken || restrictedAccessToken; - return mailtrainConfig.sandboxUrlBase + localRestrictedAccessToken + '/' + (path || ''); + const url = new URL(localRestrictedAccessToken + '/' + (path || ''), mailtrainConfig.sandboxUrlBase); + + if (opts && opts.withLocale) { + url.searchParams.append('locale', i18n.language); + } + + return url.toString(); } function getPublicUrl(path, opts) { diff --git a/client/src/templates/CUD.js b/client/src/templates/CUD.js index c89166de..59dcb1f9 100644 --- a/client/src/templates/CUD.js +++ b/client/src/templates/CUD.js @@ -376,7 +376,7 @@ export default class CUD extends Component { {typeForm} - + } diff --git a/client/src/templates/helpers.js b/client/src/templates/helpers.js index 52d7a319..c49a5ad0 100644 --- a/client/src/templates/helpers.js +++ b/client/src/templates/helpers.js @@ -167,7 +167,6 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM mutState.setIn([prefix + 'mosaicoTemplate', 'value'], null); } }, - isTagLanguageSelectorDisabledForEdit: true, validate: state => { const mosaicoTemplate = state.getIn([prefix + 'mosaicoTemplate', 'value']); if (!mosaicoTemplate) { @@ -256,7 +255,6 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM }, afterTagLanguageChange: (mutState, isEdit) => { }, - isTagLanguageSelectorDisabledForEdit: false, validate: state => { } }; @@ -346,7 +344,6 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM }, afterTagLanguageChange: (mutState, isEdit) => { }, - isTagLanguageSelectorDisabledForEdit: false, validate: state => { } }; @@ -408,7 +405,6 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM }, afterTagLanguageChange: (mutState, isEdit) => { }, - isTagLanguageSelectorDisabledForEdit: false, validate: state => { } }; @@ -495,7 +491,6 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM }, afterTagLanguageChange: (mutState, isEdit) => { }, - isTagLanguageSelectorDisabledForEdit: false, validate: state => { } }; @@ -683,6 +678,14 @@ export function getEditForm(owner, typeKey, prefix = '') { RSS entry image URL + + + {tg('RSS_ENTRY_CUSTOM_TAGS')} + + + Mailtrain custom tags. The custom tags can be passed in via mt:entries-json element in RSS entry. The text contents of the elements is interpreted as JSON-formatted object.. + + } diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 6914d668..52c6ff1e 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -12,6 +12,15 @@ Optional parameters: --mongoHost XXX - sets mongo host (default: mongo) --redisHost XXX - sets redis host (default: redis) --mySqlHost XXX - sets mysql host (default: mysql) + --withLdap - use if you want to enable LDAP authentication + --ldapHost XXX - LDAP Host for authentication (default: ldap) + --ldapPort XXX - LDAP port (default: 389) + --ldapSecure - use if you want to use LDAP with ldaps protocol + --ldapBindUser XXX - User for LDAP connexion + --ldapBindPass XXX - Password for LDAP connexion + --ldapFilter XXX - LDAP filter + --ldapBaseDN XXX - LDAP base DN + --ldapUidTag XXX - LDAP UID tag (e.g. uid/cn/username) EOF exit 1 @@ -22,6 +31,15 @@ urlBaseTrusted=http://localhost:3000 urlBaseSandbox=http://localhost:3003 urlBasePublic=http://localhost:3004 wwwProxy=false +withLdap=false +ldapHost=ldap +ldapPort=389 +ldapSecure=false +ldapBindUser="" +ldapBindPass="" +ldapFilter="" +ldapBaseDN="" +ldapUidTag="" mongoHost=mongo redisHost=redis mySqlHost=mysql @@ -59,12 +77,73 @@ while [ $# -gt 0 ]; do mySqlHost="$2" shift 2 ;; + --withLdap) + withLdap=true + shift 1 + ;; + --ldapHost) + ldapHost="$2" + shift 2 + ;; + --ldapPort) + ldapPort="$2" + shift 2 + ;; + --ldapSecure) + ldapSecure=true + shift 1 + ;; + --ldapBindUser) + ldapBindUser="$2" + shift 2 + ;; + --ldapBindPass) + ldapBindPass="$2" + shift 2 + ;; + --ldapFilter) + ldapFilter="$2" + shift 2 + ;; + --ldapBaseDN) + ldapBaseDN="$2" + shift 2 + ;; + --ldapUidTag) + ldapUidTag="$2" + shift 2 + ;; *) echo "Error: unrecognized option $1." printHelp esac done +if [ "$ldapBindUser" == "" ]; then + ldapBindUserLine="" +else + ldapBindUserLine="bindUser: $ldapBindUser" +fi +if [ "$ldapBindPass" == "" ]; then + ldapBindPassLine="" +else + ldapBindPassLine="bindPassword: $ldapBindPass" +fi +if [ "$ldapFilter" == "" ]; then + ldapFilterLine="" +else + ldapFilterLine="filter: $ldapFilter" +fi +if [ "$ldapBaseDN" == "" ]; then + ldapBaseDNLine="" +else + ldapBaseDNLine="baseDN: $ldapBaseDN" +fi +if [ "$ldapUidTag" == "" ]; then + ldapUidTagLine="" +else + ldapUidTagLine="uidTag: $ldapUidTag" +fi cat > server/config/production.yaml < server/services/workers/reports/config/production.yaml < { + const entity = await rawGetByTx(tx,'cid', cid); + + await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.id, 'view'); + + return entity; + }); +} + async function _validateAndPreprocess(tx, context, entity, isCreate, content) { if (content === Content.ALL || content === Content.WITHOUT_SOURCE_CUSTOM || content === Content.RSS_ENTRY) { await namespaceHelpers.validateEntity(tx, entity); @@ -991,6 +1001,10 @@ async function testSend(context, data) { attachments: [] }; + if (campaign.type === CampaignType.RSS) { + messageData.rssEntry = await feedcheck.getEntryForPreview(campaign.data.feedUrl); + } + const attachments = await files.listTx(tx, contextHelpers.getAdminContext(), 'campaign', 'attachment', campaignId); for (const attachment of attachments) { messageData.attachments.push({ @@ -1057,6 +1071,30 @@ async function testSend(context, data) { senders.scheduleCheck(); } +async function getRssPreview(context, campaignCid, listCid, subscriptionCid) { + const campaign = await getByCid(context, campaignCid); + await shares.enforceEntityPermission(context, 'campaign', campaign.id, 'view'); + + enforce(campaign.type === CampaignType.RSS); + + const list = await lists.getByCid(context, listCid); + await shares.enforceEntityPermission(context, 'list', list.id, 'viewTestSubscriptions'); + + const subscription = await subscriptions.getByCid(context, list.id, subscriptionCid); + + if (!subscription.is_test) { + shares.throwPermissionDenied(); + } + + const settings = { + campaign, // this prevents message sender from fetching the campaign again + rssEntry: await feedcheck.getEntryForPreview(campaign.data.feedUrl) + }; + + return await messageSender.getMessage(campaignCid, listCid, subscriptionCid, settings); +} + + module.exports.Content = Content; module.exports.hash = hash; @@ -1073,6 +1111,7 @@ module.exports.listLinkClicksDTAjax = listLinkClicksDTAjax; module.exports.getByIdTx = getByIdTx; module.exports.getById = getById; +module.exports.getByCid = getByCid; module.exports.create = create; module.exports.createRssTx = createRssTx; module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck; @@ -1101,4 +1140,5 @@ module.exports.getStatisticsOpened = getStatisticsOpened; module.exports.fetchRssCampaign = fetchRssCampaign; -module.exports.testSend = testSend; \ No newline at end of file +module.exports.testSend = testSend; +module.exports.getRssPreview = getRssPreview; \ No newline at end of file diff --git a/server/models/fields.js b/server/models/fields.js index faac8bd7..2f7148eb 100644 --- a/server/models/fields.js +++ b/server/models/fields.js @@ -8,6 +8,7 @@ const interoperableErrors = require('../../shared/interoperable-errors'); const shares = require('./shares'); const validators = require('../../shared/validators'); const shortid = require('shortid'); +const slugify = require('slugify'); const segments = require('./segments'); const { formatDate, formatBirthday, parseDate, parseBirthday } = require('../../shared/date'); const { getFieldColumn } = require('../../shared/lists'); @@ -542,7 +543,7 @@ async function createTx(tx, context, listId, entity) { let columnName; if (!fieldType.grouped) { - columnName = ('custom_' + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '_'); + columnName = ('custom_' + slugify(entity.name, '_').substring(0, 32) + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '_'); } const filteredEntity = filterObject(entity, allowedKeysCreate); diff --git a/server/models/subscriptions.js b/server/models/subscriptions.js index 2e90be43..e4e97531 100644 --- a/server/models/subscriptions.js +++ b/server/models/subscriptions.js @@ -420,7 +420,7 @@ async function list(context, listId, grouped, offset, limit) { } async function listTestUsersTx(tx, context, listId, segmentId, grouped) { - await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions'); + await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewTestSubscriptions'); let entitiesQry = tx(getSubscriptionTableName(listId)).orderBy('id', 'asc').where('is_test', true).limit(TEST_USERS_LIST_LIMIT); diff --git a/server/routes/campaigns.js b/server/routes/campaigns.js new file mode 100644 index 00000000..da61caa9 --- /dev/null +++ b/server/routes/campaigns.js @@ -0,0 +1,69 @@ +'use strict'; + +const passport = require('../lib/passport'); +const routerFactory = require('../lib/router-async'); +const campaigns = require('../models/campaigns'); +const lists = require('../models/lists'); +const users = require('../models/users'); +const contextHelpers = require('../lib/context-helpers'); +const { AppType } = require('../../shared/app'); + + +users.registerRestrictedAccessTokenMethod('rssPreview', async ({campaignCid, listCid}) => { + + const campaign = await campaigns.getByCid(contextHelpers.getAdminContext(), campaignCid); + const list = await lists.getByCid(contextHelpers.getAdminContext(), listCid); + + return { + permissions: { + 'campaign': { + [campaign.id]: new Set(['view']) + }, + 'list': { + [list.id]: new Set(['view', 'viewTestSubscriptions']) + } + } + }; +}); + +async function getRouter(appType) { + const router = routerFactory.create(); + + if (appType === AppType.SANDBOXED) { + + router.get('/rss-preview/:campaign/:list/:subscription', passport.loggedIn, (req, res, next) => { + campaigns.getRssPreview(req.context, req.params.campaign, req.params.list, req.params.subscription) + .then(result => { + const {html} = result; + + if (html.match(/<\/body\b/i)) { + res.render('partials/tracking-scripts', { + layout: 'archive/layout-raw' + }, (err, scripts) => { + if (err) { + return next(err); + } + const htmlWithScripts = scripts ? html.replace(/<\/body\b/i, match => scripts + match) : html; + + res.render('archive/view', { + layout: 'archive/layout-raw', + message: htmlWithScripts + }); + }); + + } else { + res.render('archive/view', { + layout: 'archive/layout-wrapped', + message: html + }); + } + + }) + .catch(err => next(err)); + }); + } + + return router; +} + +module.exports.getRouter = getRouter; diff --git a/server/routes/sandboxed-mosaico.js b/server/routes/sandboxed-mosaico.js index 98f4854b..ceac02b7 100644 --- a/server/routes/sandboxed-mosaico.js +++ b/server/routes/sandboxed-mosaico.js @@ -141,7 +141,7 @@ async function getRouter(appType) { const tmpl = await mosaicoTemplates.getById(req.context, castToInteger(req.params.mosaicoTemplateId)); res.set('Content-Type', 'text/html'); - res.send(base(tmpl.data.html, getTrustedUrl(), getSandboxUrl('', req.context), getPublicUrl())); + res.send(base(tmpl.data.html, tmpl.tag_language, getTrustedUrl(), getSandboxUrl('', req.context), getPublicUrl())); }); // Mosaico looks for block thumbnails in edres folder relative to index.html of the template. We respond to such requests here. diff --git a/server/routes/subscriptions.js b/server/routes/subscriptions.js index 447348fe..b5b9306a 100644 --- a/server/routes/subscriptions.js +++ b/server/routes/subscriptions.js @@ -1,8 +1,6 @@ 'use strict'; const passport = require('../lib/passport'); -const shares = require('../models/shares'); -const contextHelpers = require('../lib/context-helpers'); const router = require('../lib/router-async').create(); const subscriptions = require('../models/subscriptions'); const {castToInteger} = require('../lib/helpers'); diff --git a/server/services/feedcheck.js b/server/services/feedcheck.js index d97284b0..b731649d 100644 --- a/server/services/feedcheck.js +++ b/server/services/feedcheck.js @@ -3,10 +3,10 @@ const config = require('../lib/config'); const log = require('../lib/log'); const knex = require('../lib/knex'); -const feedparser = require('feedparser-promised'); const { CampaignType, CampaignStatus, CampaignSource } = require('../../shared/campaigns'); const campaigns = require('../models/campaigns'); const contextHelpers = require('../lib/context-helpers'); +const {fetch} = require('../lib/feedcheck'); require('../lib/fork'); const { tLog } = require('../lib/translate'); @@ -19,39 +19,6 @@ let running = false; let periodicTimeout = null; -async function fetch(url) { - const httpOptions = { - uri: url, - headers: { - 'user-agent': 'Mailtrain', - 'accept': 'text/html,application/xhtml+xml' - } - }; - - const items = await feedparser.parse(httpOptions); - - const entries = []; - for (const item of items) { - const entry = { - title: item.title, - date: item.date || item.pubdate || item.pubDate || new Date(), - guid: item.guid || item.link, - link: item.link, - content: item.description || item.summary, - summary: item.summary || item.description, - imageUrl: item.image.url, - }; - - if ('mt:entries-json' in item) { - entry.customTags = JSON.parse(item['mt:entries-json']['#']) - } - - entries.push(entry); - } - - return entries; -} - async function run() { if (running) { return; diff --git a/server/setup/knex/config.js b/server/setup/knex/config.js index 0f86dc97..2e5cea25 100644 --- a/server/setup/knex/config.js +++ b/server/setup/knex/config.js @@ -4,6 +4,6 @@ if (!process.env.NODE_CONFIG_DIR) { process.env.NODE_CONFIG_DIR = __dirname + '/../../config'; } -const config = require('server/setup/knex/config'); +const config = require('../../lib/config'); module.exports = config; diff --git a/server/setup/knex/fixes/fix-20190726150000_shorten_field_column_names.js b/server/setup/knex/fixes/fix-20190726150000_shorten_field_column_names.js new file mode 100644 index 00000000..d83a1937 --- /dev/null +++ b/server/setup/knex/fixes/fix-20190726150000_shorten_field_column_names.js @@ -0,0 +1,69 @@ +'use strict'; + +const config = require('../config'); +const knex = require('../../../lib/knex'); +const shortid = require('shortid'); +const slugify = require('slugify'); + +async function run() { + const lists = await knex('lists'); + for (const list of lists) { + console.log(`Processing list ${list.id}`); + const fields = await knex('custom_fields').whereNotNull('column').where('list', list.id); + + const fieldsMap = new Map(); + const prefixesMap = new Map(); + + for (const field of fields) { + const oldName = field.column; + const newName = ('custom_' + slugify(field.name, '_').substring(0, 32) + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '_'); + const formerPrefix = ('custom_' + slugify(field.name, '_') + '_').toLowerCase().replace(/[^a-z0-9_]/g, ''); + + fieldsMap.set(oldName, newName); + prefixesMap.set(formerPrefix, newName); + + await knex('custom_fields').where('id', field.id).update('column', newName); + + await knex.schema.table('subscription__' + list.id, table => { + table.renameColumn(oldName, newName); + table.renameColumn('source_' + oldName, 'source_' + newName); + }); + } + + + function processRule(rule) { + if (rule.type === 'all' || rule.type === 'some' || rule.type === 'none') { + for (const childRule of rule.rules) { + processRule(childRule); + } + } else { + let newName = fieldsMap.get(rule.column); + if (newName) { + rule.column = newName; + return; + } + + for (const [formerPrefix, newName] of prefixesMap.entries()) { + if (rule.column.startsWith(formerPrefix)) { + rule.column = newName; + return; + } + } + } + } + + const segments = await knex('segments').where('list', list.id); + for (const segment of segments) { + const settings = JSON.parse(segment.settings); + processRule(settings.rootRule); + await knex('segments').where('id', segment.id).update({settings: JSON.stringify(settings)}); + } + } + + await knex('knex_migrations').where('name', '20190726150000_shorten_field_column_names.js').del(); + + console.log('All fixes done'); + process.exit(); +} + +run().catch(err => console.error(err)); \ No newline at end of file diff --git a/server/setup/knex/migrations/20170506102634_v1_to_v2.js b/server/setup/knex/migrations/20170506102634_v1_to_v2.js index d64a40c6..f698e4d3 100644 --- a/server/setup/knex/migrations/20170506102634_v1_to_v2.js +++ b/server/setup/knex/migrations/20170506102634_v1_to_v2.js @@ -11,6 +11,8 @@ const { EntityVals: TriggerEntityVals, EventVals: TriggerEventVals } = require(' const { SubscriptionSource } = require('../../../../shared/lists'); const {DOMParser, XMLSerializer} = require('xmldom'); const log = require('../../../lib/log'); +const shortid = require('shortid'); +const slugify = require('slugify'); const entityTypesAddNamespace = ['list', 'custom_form', 'template', 'campaign', 'report', 'report_template', 'user']; const shareableEntityTypes = ['list', 'custom_form', 'template', 'campaign', 'report', 'report_template', 'namespace', 'send_configuration', 'mosaico_template']; @@ -236,16 +238,54 @@ async function migrateUsers(knex) { }); } +async function shortenFieldColumnNames(knex, list) { + const fields = await knex('custom_fields').whereNotNull('column').where('list', list.id); + + const fieldsMap = new Map(); + + for (const field of fields) { + const oldName = field.column; + const newName = ('custom_' + slugify(field.name, '_').substring(0,32) + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '_'); + + fieldsMap.set(oldName, newName); + + await knex('custom_fields').where('id', field.id).update('column', newName); + + await knex.schema.table('subscription__' + list.id, table => { + table.renameColumn(oldName, newName); + }); + } + + + function processRule(rule) { + if (rule.type === 'all' || rule.type === 'some' || rule.type === 'none') { + for (const childRule of rule.rules) { + processRule(childRule); + } + } else { + rule.column = fieldsMap.get(rule.column) || rule.column /* this is to handle "email" column */; + } + } + + const segments = await knex('segments').where('list', list.id); + for (const segment of segments) { + const settings = JSON.parse(segment.settings); + processRule(settings.rootRule); + await knex('segments').where('id', segment.id).update({settings: JSON.stringify(settings)}); + } +} + async function migrateSubscriptions(knex) { await knex.schema.dropTableIfExists('subscription'); const lists = await knex('lists'); for (const list of lists) { + await shortenFieldColumnNames(knex, list); + await knex.schema.raw('ALTER TABLE `subscription__' + list.id + '` ADD `unsubscribed` timestamp NULL DEFAULT NULL'); await knex.schema.raw('ALTER TABLE `subscription__' + list.id + '` ADD `source_email` int(11) DEFAULT NULL'); await knex.schema.raw('ALTER TABLE `subscription__' + list.id + '` ADD `hash_email` varchar(255) CHARACTER SET ascii'); - const fields = await knex('custom_fields').where('list', list.id); const info = await knex('subscription__' + list.id).columnInfo(); for (const field of fields) { @@ -1251,11 +1291,12 @@ exports.up = (knex, Promise) => (async() => { await migrateCustomFields(knex); log.verbose('Migration', 'Custom fields complete') - await migrateSubscriptions(knex); - await migrateSegments(knex); log.verbose('Migration', 'Segments complete') + await migrateSubscriptions(knex); + log.verbose('Migration', 'Subscriptions complete') + await migrateReports(knex); log.verbose('Migration', 'Reports complete') diff --git a/server/setup/knex/migrations/20190726150000_shorten_field_column_names.js b/server/setup/knex/migrations/20190726150000_shorten_field_column_names.js deleted file mode 100644 index c1fc42eb..00000000 --- a/server/setup/knex/migrations/20190726150000_shorten_field_column_names.js +++ /dev/null @@ -1,21 +0,0 @@ -const shortid = require('shortid'); - -exports.up = (knex, Promise) => (async() => { - const fields = await knex('custom_fields').whereNotNull('column'); - - for (const field of fields) { - const listId = field.list; - const oldName = field.column; - const newName = ('custom_' + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '_'); - - await knex('custom_fields').where('id', field.id).update('column', newName); - - await knex.schema.table('subscription__' + listId, table => { - table.renameColumn(oldName, newName); - table.renameColumn('source_' + oldName, 'source_' + newName); - }); - } -})(); - -exports.down = (knex, Promise) => (async() => { -})(); diff --git a/shared/templates.js b/shared/templates.js index 35d819cf..a4c22fa8 100644 --- a/shared/templates.js +++ b/shared/templates.js @@ -44,28 +44,28 @@ function getMergeTagsForBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) { }; } -function base(text, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) { +function base(text, tagLanguage, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) { const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl); - text = text.split('[URL_BASE]').join(bases.publicBaseUrl); - text = text.split('[TRUSTED_URL_BASE]').join(bases.trustedBaseUrl); - text = text.split('[SANDBOX_URL_BASE]').join(bases.sandboxBaseUrl); - text = text.split('[ENCODED_URL_BASE]').join(encodeURIComponent(bases.publicBaseUrl)); - text = text.split('[ENCODED_TRUSTED_URL_BASE]').join(encodeURIComponent(bases.trustedBaseUrl)); - text = text.split('[ENCODED_SANDBOX_URL_BASE]').join(encodeURIComponent(bases.sandboxBaseUrl)); + text = text.split(renderTag(tagLanguage, 'URL_BASE')).join(bases.publicBaseUrl); + text = text.split(renderTag(tagLanguage, 'TRUSTED_URL_BASE')).join(bases.trustedBaseUrl); + text = text.split(renderTag(tagLanguage, 'SANDBOX_URL_BASE')).join(bases.sandboxBaseUrl); + text = text.split(renderTag(tagLanguage, 'ENCODED_URL_BASE')).join(encodeURIComponent(bases.publicBaseUrl)); + text = text.split(renderTag(tagLanguage, 'ENCODED_TRUSTED_URL_BASE')).join(encodeURIComponent(bases.trustedBaseUrl)); + text = text.split(renderTag(tagLanguage, 'ENCODED_SANDBOX_URL_BASE')).join(encodeURIComponent(bases.sandboxBaseUrl)); return text; } -function unbase(text, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl, treatAllAsPublic = false) { +function unbase(text, tagLanguage, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl, treatAllAsPublic = false) { const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl); - text = text.split(bases.publicBaseUrl).join('[URL_BASE]'); - text = text.split(bases.trustedBaseUrl).join(treatAllAsPublic ? '[URL_BASE]' : '[TRUSTED_URL_BASE]'); - text = text.split(bases.sandboxBaseUrl).join(treatAllAsPublic ? '[URL_BASE]' : '[SANDBOX_URL_BASE]'); - text = text.split(encodeURIComponent(bases.publicBaseUrl)).join('[ENCODED_URL_BASE]'); - text = text.split(encodeURIComponent(bases.trustedBaseUrl)).join(treatAllAsPublic ? '[ENCODED_URL_BASE]' : '[ENCODED_TRUSTED_URL_BASE]'); - text = text.split(encodeURIComponent(bases.sandboxBaseUrl)).join(treatAllAsPublic ? '[ENCODED_URL_BASE]' : '[ENCODED_SANDBOX_URL_BASE]'); + text = text.split(bases.publicBaseUrl).join(renderTag(tagLanguage, 'URL_BASE')); + text = text.split(bases.trustedBaseUrl).join(renderTag(tagLanguage, treatAllAsPublic ? 'URL_BASE' : 'TRUSTED_URL_BASE')); + text = text.split(bases.sandboxBaseUrl).join(renderTag(tagLanguage, treatAllAsPublic ? 'URL_BASE' : 'SANDBOX_URL_BASE')); + text = text.split(encodeURIComponent(bases.publicBaseUrl)).join(renderTag(tagLanguage, 'ENCODED_URL_BASE')); + text = text.split(encodeURIComponent(bases.trustedBaseUrl)).join(renderTag(tagLanguage, treatAllAsPublic ? 'ENCODED_URL_BASE' : 'ENCODED_TRUSTED_URL_BASE')); + text = text.split(encodeURIComponent(bases.sandboxBaseUrl)).join(renderTag(tagLanguage, treatAllAsPublic ? 'ENCODED_URL_BASE' : 'ENCODED_SANDBOX_URL_BASE')); return text; }