diff --git a/TODO.md b/TODO.md index 873ce9a2..a82d38c7 100644 --- a/TODO.md +++ b/TODO.md @@ -1,15 +1,15 @@ ### Front page - Some dashboard -### Deletion -- Check/delete dependencies - ### Templates - Add MJML template editor - Include GrapeJS with MJML support - CKEditor to sandbox - Add Files support to CKEditor +### Message delivery +- Better integration with ZoneMTA to allow multiple send configurations (with different DKIM) against one ZoneMTA instance via different HTTP configuration of ZoneMTA. This may need an extension of ZoneMTA to provide some header entry that identifies the campaign. + ### Campaigns - Statistics for a sent campaign - List of sent RSS campaigns (?) diff --git a/app-builder.js b/app-builder.js index 0aa1c5d2..ad6e81a2 100644 --- a/app-builder.js +++ b/app-builder.js @@ -218,13 +218,12 @@ function createApp(appType) { useWith404Fallback('/subscription', subscription); useWith404Fallback('/links', links); useWith404Fallback('/archive', archive); + useWith404Fallback('/files', files); } - if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) { - // Regular endpoints - useWith404Fallback('/files', files); - useWith404Fallback('/mosaico', mosaico.getRouter(appType)); + useWith404Fallback('/mosaico', mosaico.getRouter(appType)); + if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) { if (config.reports && config.reports.enabled === true) { useWith404Fallback('/reports', reports); } @@ -266,7 +265,7 @@ function createApp(appType) { app.use('/', index.getRouter(appType)); // Error handlers - if (app.get('env') === 'development') { + if (app.get('env') === 'development' || app.get('env') === 'test') { // development error handler // will print stacktrace app.use((err, req, res, next) => { diff --git a/client/src/lib/mosaico.js b/client/src/lib/mosaico.js index cd4c53f9..a76b8707 100644 --- a/client/src/lib/mosaico.js +++ b/client/src/lib/mosaico.js @@ -8,6 +8,7 @@ import styles from "./mosaico.scss"; import {UntrustedContentHost, parentRPC} from './untrusted'; import {Icon} from "./bootstrap-components"; import { + getPublicUrl, getSandboxUrl, getTrustedUrl } from "./urls"; @@ -105,12 +106,13 @@ export class MosaicoSandbox extends Component { } async exportState(method, params) { - const sandboxUrlBase = getSandboxUrl(); const trustedUrlBase = getTrustedUrl(); + const sandboxUrlBase = getSandboxUrl(); + const publicUrlBase = getPublicUrl(); return { - html: unbase(this.viewModel.exportHTML(), trustedUrlBase, sandboxUrlBase, true), - model: unbase(this.viewModel.exportJSON(), trustedUrlBase, sandboxUrlBase), - metadata: unbase(this.viewModel.exportMetadata(), trustedUrlBase, sandboxUrlBase) + html: unbase(this.viewModel.exportHTML(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true), + model: unbase(this.viewModel.exportJSON(), trustedUrlBase, sandboxUrlBase, publicUrlBase), + metadata: unbase(this.viewModel.exportMetadata(), trustedUrlBase, sandboxUrlBase, publicUrlBase) }; } @@ -155,10 +157,11 @@ export class MosaicoSandbox extends Component { strings: window.mosaicoLanguageStrings }; - const sandboxUrlBase = getSandboxUrl(); const trustedUrlBase = getTrustedUrl(); - const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, trustedUrlBase, sandboxUrlBase)); - const model = this.props.initialModel && JSON.parse(base(this.props.initialModel, trustedUrlBase, sandboxUrlBase)); + 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 template = this.props.templateId ? getSandboxUrl(`mosaico/templates/${this.props.templateId}/index.html`) : this.props.templatePath; const allPlugins = plugins.concat(window.mosaicoPlugins); diff --git a/lib/campaign-sender.js b/lib/campaign-sender.js index e144a8da..38c3b0f4 100644 --- a/lib/campaign-sender.js +++ b/lib/campaign-sender.js @@ -51,7 +51,7 @@ class CampaignSender { } if (campaign.source === CampaignSource.TEMPLATE) { - this.template = templates.getByIdTx(tx, contextHelpers.getAdminContext(), this.campaign.data.sourceTemplate, false); + this.template = await templates.getByIdTx(tx, contextHelpers.getAdminContext(), this.campaign.data.sourceTemplate, false); } const attachments = await files.listTx(tx, contextHelpers.getAdminContext(), 'campaign', 'attachment', this.campaign.id); @@ -111,7 +111,7 @@ class CampaignSender { if (replaceDataImgs) { // replace data: images with embedded attachments html = html.replace(/(]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => { - let cid = shortid.generate() + '-attachments@' + campaign.address.split('@').pop(); + const cid = shortid.generate() + '-attachments@' + campaign.address.split('@').pop(); attachments.push({ path: dataUri, cid diff --git a/models/fields.js b/models/fields.js index 65c1aaee..2f6a3a40 100644 --- a/models/fields.js +++ b/models/fields.js @@ -14,6 +14,8 @@ const { formatDate, formatBirthday, parseDate, parseBirthday } = require('../sha const { getFieldColumn } = require('../shared/lists'); const { cleanupFromPost } = require('../lib/helpers'); const Handlebars = require('handlebars'); +const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls'); +const { getMergeTagsForBases } = require('../shared/templates'); const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']); @@ -228,7 +230,7 @@ fieldTypes['date'] = { getHbsType: field => 'typeDate' + field.settings.dateFormat.charAt(0).toUpperCase() + field.settings.dateFormat.slice(1), forHbs: (field, value) => formatDate(field.settings.dateFormat, value), parsePostValue: (field, value) => parseDate(field.settings.dateFormat, value), - render: (field, value) => value !== null && value.trim() !== '' ? formatDate(field.settings.dateFormat, value) : '' + render: (field, value) => value !== null ? formatDate(field.settings.dateFormat, value) : '' }; fieldTypes['birthday'] = { @@ -243,7 +245,7 @@ fieldTypes['birthday'] = { getHbsType: field => 'typeBirthday' + field.settings.dateFormat.charAt(0).toUpperCase() + field.settings.dateFormat.slice(1), forHbs: (field, value) => formatBirthday(field.settings.dateFormat, value), parsePostValue: (field, value) => parseBirthday(field.settings.dateFormat, value), - render: (field, value) => value !== null && value.trim() !== '' ? formatBirthday(field.settings.dateFormat, value) : '' + render: (field, value) => value !== null ? formatBirthday(field.settings.dateFormat, value) : '' }; const groupedTypes = Object.keys(fieldTypes).filter(key => fieldTypes[key].grouped); @@ -694,7 +696,8 @@ async function forHbs(context, listId, subscription) { // assumes grouped subscr function getMergeTags(fieldsGrouped, subscription) { // assumes grouped subscription const mergeTags = { - 'EMAIL': subscription.email + 'EMAIL': subscription.email, + ...getMergeTagsForBases(getTrustedUrl(), getSandboxUrl(), getPublicUrl()) }; for (const fld of fieldsGrouped) { diff --git a/models/files.js b/models/files.js index 628fc71b..232481c1 100644 --- a/models/files.js +++ b/models/files.js @@ -8,7 +8,7 @@ const fs = require('fs-extra-promise'); const path = require('path'); const interoperableErrors = require('../shared/interoperable-errors'); const entitySettings = require('../lib/entity-settings'); -const {getTrustedUrl} = require('../lib/urls'); +const {getPublicUrl} = require('../lib/urls'); const crypto = require('crypto'); const bluebird = require('bluebird'); @@ -29,7 +29,7 @@ function getFilePath(type, subType, entityId, filename) { } function getFileUrl(context, type, subType, entityId, filename) { - return getTrustedUrl(`files/${type}/${subType}/${entityId}/${filename}`, context) + return getPublicUrl(`files/${type}/${subType}/${entityId}/${filename}`, context) } function getFilesTable(type, subType) { @@ -109,7 +109,7 @@ async function getFileByFilename(context, type, subType, entityId, name) { } async function getFileByUrl(context, url) { - const urlPrefix = getTrustedUrl('files/', context); + const urlPrefix = getPublicUrl('files/', context); if (url.startsWith(urlPrefix)) { const path = url.substring(urlPrefix.length); const pathElem = path.split('/'); diff --git a/routes/archive.js b/routes/archive.js index 05cf96b9..5e77de78 100644 --- a/routes/archive.js +++ b/routes/archive.js @@ -15,16 +15,14 @@ router.get('/:campaign/:list/:subscription', (req, res, next) => { res.render('partials/tracking-scripts', { layout: 'archive/layout-raw' }, (err, scripts) => { - console.log(scripts); - console.log(err); if (err) { return next(err); } - html = scripts ? html.replace(/<\/body\b/i, match => scripts + match) : html; + const htmlWithScripts = scripts ? html.replace(/<\/body\b/i, match => scripts + match) : html; res.render('archive/view', { layout: 'archive/layout-raw', - message: html + message: htmlWithScripts }); }); diff --git a/routes/mosaico.js b/routes/mosaico.js index a3709047..6625ea25 100644 --- a/routes/mosaico.js +++ b/routes/mosaico.js @@ -23,7 +23,7 @@ const mosaicoTemplates = require('../models/mosaico-templates'); const contextHelpers = require('../lib/context-helpers'); const interoperableErrors = require('../shared/interoperable-errors'); -const { getTrustedUrl, getSandboxUrl } = require('../lib/urls'); +const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls'); const { base } = require('../shared/templates'); const { AppType } = require('../shared/app'); @@ -134,7 +134,7 @@ 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))); + res.send(base(tmpl.data.html, 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. @@ -153,7 +153,7 @@ function getRouter(appType) { }); // This is a fallback to versafix-1 if the block thumbnail is not defined by the template - router.use('/templates/:mosaicoTemplateId/edres', express.static(path.join(__dirname, '..', 'client', 'public', 'mosaico', 'templates', 'versafix-1', 'edres'))); + router.use('/templates/:mosaicoTemplateId/edres', express.static(path.join(__dirname, '..', 'client', 'static', 'mosaico', 'templates', 'versafix-1', 'edres'))); fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file'); @@ -185,7 +185,7 @@ function getRouter(appType) { if (config.language && config.language !== 'en') { const lang = config.language.split('_')[0]; try { - const file = path.join(__dirname, '..', 'client', 'public', 'mosaico', 'lang', 'mosaico-' + lang + '.json'); + const file = path.join(__dirname, '..', 'client', 'static', 'mosaico', 'lang', 'mosaico-' + lang + '.json'); languageStrings = await fsReadFile(file, 'utf8'); } catch (err) { } @@ -205,7 +205,8 @@ function getRouter(appType) { }); }); - } else { + } else if (appType === AppType.TRUSTED || appType === AppType.PUBLIC) { // Mosaico editor loads the images from TRUSTED endpoint. This is hard to change because the index.html has to come from TRUSTED. + // So we serve /mosaico/img under both endpoints. There is no harm in it. router.getAsync('/img', async (req, res) => { const method = req.query.method; const params = req.query.params; @@ -214,6 +215,7 @@ function getRouter(appType) { // FIXME - cache the generated files !!! + if (method === 'placeholder') { width = sanitizeSize(width, 1, 2048, 600, false); height = sanitizeSize(height, 1, 2048, 300, false); @@ -228,7 +230,7 @@ function getRouter(appType) { const mosaicoLegacyUrlPrefix = getTrustedUrl(`mosaico/uploads/`); if (url.startsWith(mosaicoLegacyUrlPrefix)) { - filePath = path.join(__dirname, '..', 'client', 'public' , 'mosaico', 'uploads', url.substring(mosaicoLegacyUrlPrefix.length)); + filePath = path.join(__dirname, '..', 'client', 'static' , 'mosaico', 'uploads', url.substring(mosaicoLegacyUrlPrefix.length)); } else { const file = await files.getFileByUrl(contextHelpers.getAdminContext(), url); filePath = file.path; diff --git a/shared/templates.js b/shared/templates.js index 181fbf7d..cc086710 100644 --- a/shared/templates.js +++ b/shared/templates.js @@ -1,6 +1,6 @@ 'use strict'; -function base(text, trustedBaseUrl, sandboxBaseUrl) { +function _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) { if (trustedBaseUrl.endsWith('/')) { trustedBaseUrl = trustedBaseUrl.substring(0, trustedBaseUrl.length - 1); } @@ -9,32 +9,54 @@ function base(text, trustedBaseUrl, sandboxBaseUrl) { sandboxBaseUrl = sandboxBaseUrl.substring(0, sandboxBaseUrl.length - 1); } - 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)); + if (publicBaseUrl.endsWith('/')) { + publicBaseUrl = publicBaseUrl.substring(0, publicBaseUrl.length - 1); + } + + return {trustedBaseUrl, sandboxBaseUrl, publicBaseUrl}; +} + +function getMergeTagsForBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) { + const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl); + + return { + URL_BASE: bases.publicBaseUrl, + TRUSTED_URL_BASE: bases.trustedBaseUrl, + SANDBOX_URL_BASE: bases.sandboxBaseUrl, + ENCODED_URL_BASE: encodeURIComponent(bases.publicBaseUrl), + ENCODED_TRUSTED_URL_BASE: encodeURIComponent(bases.trustedBaseUrl), + ENCODED_SANDBOX_URL_BASE: encodeURIComponent(bases.sandboxBaseUrl) + }; +} + +function base(text, 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)); return text; } -function unbase(text, trustedBaseUrl, sandboxBaseUrl, treatSandboxAsTrusted = false) { - if (trustedBaseUrl.endsWith('/')) { - trustedBaseUrl = trustedBaseUrl.substring(0, trustedBaseUrl.length - 1); - } +function unbase(text, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl, treatAllAsPublic = false) { + const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl); - if (sandboxBaseUrl.endsWith('/')) { - sandboxBaseUrl = sandboxBaseUrl.substring(0, sandboxBaseUrl.length - 1); - } - - 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]'); + 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]'); return text; } module.exports = { base, - unbase + unbase, + getMergeTagsForBases }; \ No newline at end of file diff --git a/shared/urls.js b/shared/urls.js index 79c85efe..2f7da329 100644 --- a/shared/urls.js +++ b/shared/urls.js @@ -1,6 +1,6 @@ 'use strict'; -const anonymousRestrictedAccessToken = 'public'; +const anonymousRestrictedAccessToken = 'anonymous'; module.exports = { anonymousRestrictedAccessToken