diff --git a/lib/models/campaigns.js b/lib/models/campaigns.js index 2282c68f..f7e4fbec 100644 --- a/lib/models/campaigns.js +++ b/lib/models/campaigns.js @@ -12,6 +12,7 @@ let feed = require('../feed'); let log = require('npmlog'); let mailer = require('../mailer'); let caches = require('../caches'); +let humanize = require('humanize'); let allowedKeys = ['description', 'from', 'address', 'subject', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'tracking_disabled']; @@ -362,6 +363,120 @@ module.exports.get = (id, withSegment, callback) => { }); }; +module.exports.getAttachments = (campaign, callback) => { + campaign = Number(campaign) || 0; + + if (campaign < 1) { + return callback(new Error('Missing Campaign ID')); + } + + db.getConnection((err, connection) => { + if (err) { + return callback(err); + } + connection.query('SELECT `id`, `filename`, `size`, `created` FROM `attachments` WHERE `campaign`=?', [campaign], (err, rows) => { + connection.release(); + if (err) { + return callback(err); + } + + if (!rows || !rows.length) { + return callback(null, []); + } + + let attachments = rows.map((row, i) => { + row = tools.convertKeys(row); + row.index = i + 1; + row.size = humanize.filesize(Number(row.size) || 0); + return row; + }); + return callback(null, attachments); + }); + }); +}; + +module.exports.addAttachment = (id, attachment, callback) => { + + let size = attachment.content ? attachment.content.length : 0; + + if (!size) { + return callback(new Error('Emtpy or too large attahcment')); + } + db.getConnection((err, connection) => { + if (err) { + return callback(err); + } + + let keys = ['campaign', 'size']; + let values = [id, size]; + + Object.keys(attachment).forEach(key => { + let value; + if (Buffer.isBuffer(attachment[key])) { + value = attachment[key]; + } else { + value = typeof attachment[key] === 'number' ? attachment[key] : (attachment[key] || '').toString().trim(); + } + + key = tools.toDbKey(key); + keys.push(key); + values.push(value); + }); + + let query = 'INSERT INTO `attachments` (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(',') + ')'; + connection.query(query, values, (err, result) => { + connection.release(); + if (err) { + return callback(err); + } + + let attachmentId = result && result.insertId || false; + return callback(null, attachmentId); + }); + }); +}; + +module.exports.deleteAttachment = (id, attachment, callback) => { + db.getConnection((err, connection) => { + if (err) { + return callback(err); + } + + let query = 'DELETE FROM `attachments` WHERE `id`=? AND `campaign`=? LIMIT 1'; + connection.query(query, [attachment, id], (err, result) => { + connection.release(); + if (err) { + return callback(err); + } + + let deleted = result && result.affectedRows || false; + return callback(null, deleted); + }); + }); +}; + +module.exports.getAttachment = (id, attachment, callback) => { + db.getConnection((err, connection) => { + if (err) { + return callback(err); + } + + let query = 'SELECT `filename`, `content_type`, `content` FROM `attachments` WHERE `id`=? AND `campaign`=? LIMIT 1'; + connection.query(query, [attachment, id], (err, rows) => { + connection.release(); + if (err) { + return callback(err); + } + if (!rows || !rows.length) { + return callback(null, false); + } + + let attachment = tools.convertKeys(rows[0]); + return callback(null, attachment); + }); + }); +}; + module.exports.getLinks = (id, linkId, callback) => { if (!callback && typeof linkId === 'function') { callback = linkId; diff --git a/meta.json b/meta.json index eba8a2e7..a7ea836e 100644 --- a/meta.json +++ b/meta.json @@ -1,3 +1,3 @@ { - "schemaVersion": 18 + "schemaVersion": 19 } diff --git a/routes/campaigns.js b/routes/campaigns.js index 5d44ab4a..8b6ed325 100644 --- a/routes/campaigns.js +++ b/routes/campaigns.js @@ -12,6 +12,11 @@ let tools = require('../lib/tools'); let striptags = require('striptags'); let passport = require('../lib/passport'); let htmlescape = require('escape-html'); +let multer = require('multer'); +let uploadStorage = multer.memoryStorage(); +let uploads = multer({ + storage: uploadStorage +}); router.all('/*', (req, res, next) => { if (!req.user) { @@ -120,121 +125,129 @@ router.get('/edit/:id', passport.csrfProtection, (req, res, next) => { return res.redirect('/campaigns'); } - settings.list(['disableWysiwyg'], (err, configItems) => { + campaigns.getAttachments(campaign.id, (err, attachments) => { if (err) { return next(err); } + campaign.attachments = attachments; - lists.quicklist((err, listItems) => { + settings.list(['disableWysiwyg'], (err, configItems) => { if (err) { - req.flash('danger', err.message || err); - return res.redirect('/'); + return next(err); } - if (Number(campaign.list)) { - listItems.forEach(list => { - list.segments.forEach(segment => { - if (segment.id === campaign.segment) { - segment.selected = true; - } - }); - if (list.id === campaign.list && !campaign.segment) { - list.selected = true; - } - }); - } - - campaign.csrfToken = req.csrfToken(); - campaign.listItems = listItems; - campaign.useEditor = true; - - campaign.disableWysiwyg = configItems.disableWysiwyg; - campaign.showGeneral = req.query.tab === 'general' || !req.query.tab; - campaign.showTemplate = req.query.tab === 'template'; - - let view; - switch (campaign.type) { - case 4: //triggered - view = 'campaigns/edit-triggered'; - break; - case 2: //rss - view = 'campaigns/edit-rss'; - break; - case 1: - default: - view = 'campaigns/edit'; - } - - let getList = (listId, callback) => { - lists.get(listId, (err, list) => { - if (err) { - return callback(err); - } - if (!list) { - list = { - id: listId - }; - } - - fields.list(list.id, (err, fieldList) => { - if (err && !fieldList) { - fieldList = []; - } - - let mergeTags = [ - // keep indentation - { - key: 'LINK_UNSUBSCRIBE', - value: 'URL that points to the preferences page of the subscriber' - }, { - key: 'LINK_PREFERENCES', - value: 'URL that points to the unsubscribe page' - }, { - key: 'LINK_BROWSER', - value: 'URL to preview the message in a browser' - }, { - key: 'EMAIL', - value: 'Email address' - }, { - key: 'FIRST_NAME', - value: 'First name' - }, { - key: 'LAST_NAME', - value: 'Last name' - }, { - key: 'FULL_NAME', - value: 'Full name (first and last name combined)' - }, { - key: 'SUBSCRIPTION_ID', - value: 'Unique ID that identifies the recipient' - }, { - key: 'LIST_ID', - value: 'Unique ID that identifies the list used for this campaign' - }, { - key: 'CAMPAIGN_ID', - value: 'Unique ID that identifies current campaign' - } - ]; - - fieldList.forEach(field => { - mergeTags.push({ - key: field.key, - value: field.name - }); - }); - - return callback(null, list, mergeTags); - }); - }); - }; - - getList(campaign.list, (err, list, mergeTags) => { + lists.quicklist((err, listItems) => { if (err) { req.flash('danger', err.message || err); return res.redirect('/'); } - campaign.mergeTags = mergeTags; - res.render(view, campaign); + + if (Number(campaign.list)) { + listItems.forEach(list => { + list.segments.forEach(segment => { + if (segment.id === campaign.segment) { + segment.selected = true; + } + }); + if (list.id === campaign.list && !campaign.segment) { + list.selected = true; + } + }); + } + + campaign.csrfToken = req.csrfToken(); + campaign.listItems = listItems; + campaign.useEditor = true; + + campaign.disableWysiwyg = configItems.disableWysiwyg; + campaign.showGeneral = req.query.tab === 'general' || !req.query.tab; + campaign.showTemplate = req.query.tab === 'template'; + campaign.showAttachments = req.query.tab === 'attachments'; + + let view; + switch (campaign.type) { + case 4: //triggered + view = 'campaigns/edit-triggered'; + break; + case 2: //rss + view = 'campaigns/edit-rss'; + break; + case 1: + default: + view = 'campaigns/edit'; + } + + let getList = (listId, callback) => { + lists.get(listId, (err, list) => { + if (err) { + return callback(err); + } + if (!list) { + list = { + id: listId + }; + } + + fields.list(list.id, (err, fieldList) => { + if (err && !fieldList) { + fieldList = []; + } + + let mergeTags = [ + // keep indentation + { + key: 'LINK_UNSUBSCRIBE', + value: 'URL that points to the preferences page of the subscriber' + }, { + key: 'LINK_PREFERENCES', + value: 'URL that points to the unsubscribe page' + }, { + key: 'LINK_BROWSER', + value: 'URL to preview the message in a browser' + }, { + key: 'EMAIL', + value: 'Email address' + }, { + key: 'FIRST_NAME', + value: 'First name' + }, { + key: 'LAST_NAME', + value: 'Last name' + }, { + key: 'FULL_NAME', + value: 'Full name (first and last name combined)' + }, { + key: 'SUBSCRIPTION_ID', + value: 'Unique ID that identifies the recipient' + }, { + key: 'LIST_ID', + value: 'Unique ID that identifies the list used for this campaign' + }, { + key: 'CAMPAIGN_ID', + value: 'Unique ID that identifies current campaign' + } + ]; + + fieldList.forEach(field => { + mergeTags.push({ + key: field.key, + value: field.name + }); + }); + + return callback(null, list, mergeTags); + }); + }); + }; + + getList(campaign.list, (err, list, mergeTags) => { + if (err) { + req.flash('danger', err.message || err); + return res.redirect('/'); + } + campaign.mergeTags = mergeTags; + res.render(view, campaign); + }); }); }); }); @@ -762,4 +775,80 @@ router.post('/inactivate', passport.parseForm, passport.csrfProtection, (req, re }); }); +router.post('/attachment', uploads.single('attachment'), passport.parseForm, passport.csrfProtection, (req, res) => { + campaigns.get(req.body.id, false, (err, campaign) => { + if (err || !campaign) { + req.flash('danger', err && err.message || err || 'Could not find campaign with specified ID'); + return res.redirect('/campaigns'); + } + campaigns.addAttachment(campaign.id, { + filename: req.file.originalname, + contentType: req.file.mimetype, + content: req.file.buffer + }, (err, attachmentId) => { + if (err) { + req.flash('danger', err && err.message || err); + } else if (attachmentId) { + req.flash('success', 'Attachment uploaded'); + } else { + req.flash('info', 'Could not store attachment'); + } + return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments'); + }); + }); +}); + +router.post('/attachment/delete', passport.parseForm, passport.csrfProtection, (req, res) => { + campaigns.get(req.body.id, false, (err, campaign) => { + if (err || !campaign) { + req.flash('danger', err && err.message || err || 'Could not find campaign with specified ID'); + return res.redirect('/campaigns'); + } + campaigns.deleteAttachment(campaign.id, Number(req.body.attachment), (err, deleted) => { + if (err) { + req.flash('danger', err && err.message || err); + } else if (deleted) { + req.flash('success', 'Attachment deleted'); + } else { + req.flash('info', 'Could not delete attachment'); + } + return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments'); + }); + }); +}); + +router.post('/attachment/download', passport.parseForm, passport.csrfProtection, (req, res) => { + campaigns.get(req.body.id, false, (err, campaign) => { + if (err || !campaign) { + req.flash('danger', err && err.message || err || 'Could not find campaign with specified ID'); + return res.redirect('/campaigns'); + } + campaigns.getAttachment(campaign.id, Number(req.body.attachment), (err, attachment) => { + if (err) { + req.flash('danger', err && err.message || err); + return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments'); + } else if (!attachment) { + req.flash('success', 'Attachment uploaded'); + return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments'); + } + + res.set('Content-Disposition', 'attachment; filename="' + encodeURIComponent(attachment.filename).replace(/['()]/g, escape) + '"'); + res.set('Content-Type', attachment.contentType); + res.send(attachment.content); + }); + }); +}); + +router.get('/attachment/:campaign', passport.csrfProtection, (req, res) => { + campaigns.get(req.params.campaign, false, (err, campaign) => { + if (err || !campaign) { + req.flash('danger', err && err.message || err || 'Could not find campaign with specified ID'); + return res.redirect('/campaigns'); + } + campaign.csrfToken = req.csrfToken(); + res.render('campaigns/upload-attachment', campaign); + }); +}); + + module.exports = router; diff --git a/setup/sql/upgrade-00019.sql b/setup/sql/upgrade-00019.sql new file mode 100644 index 00000000..1c9a78fa --- /dev/null +++ b/setup/sql/upgrade-00019.sql @@ -0,0 +1,21 @@ +# Header section +# Define incrementing schema version number +SET @schema_version = '19'; + +CREATE TABLE `attachments` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `campaign` int(11) unsigned NOT NULL, + `filename` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '', + `content_type` varchar(100) CHARACTER SET ascii NOT NULL DEFAULT '', + `content` longblob, + `size` int(11) NOT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `campaign` (`campaign`), + CONSTRAINT `attachments_ibfk_1` FOREIGN KEY (`campaign`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +# Footer section +LOCK TABLES `settings` WRITE; +INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version; +UNLOCK TABLES; diff --git a/views/campaigns/edit.hbs b/views/campaigns/edit.hbs index 25812383..a33173ba 100644 --- a/views/campaigns/edit.hbs +++ b/views/campaigns/edit.hbs @@ -17,16 +17,29 @@ +{{#each attachments}} +
+ + + +
+
+ + + +
+{{/each}} +
-
@@ -109,7 +122,6 @@
-

@@ -187,17 +199,78 @@ {{/if}}
-
- -
+
-
-
-
- +

+ + +
+ + Attachments + + +
+ + + + + + + + + {{#if attachments}} + {{#each attachments}} + + + + + + + {{/each}} + {{else}} + + + + {{/if}} + +
+ # + + File + + Size + +   +
+ {{index}} + + + + {{size}} + + +
+ No data available in table +
+
+ +
+
+ +
+ +
+ +
+
+
+ +
+
-
diff --git a/views/campaigns/upload-attachment.hbs b/views/campaigns/upload-attachment.hbs new file mode 100644 index 00000000..b535e3c1 --- /dev/null +++ b/views/campaigns/upload-attachment.hbs @@ -0,0 +1,31 @@ + + +

Edit Campaign View campaign

+ +
+ +
+ + +
+ +
+ +
+
+ +
+
+ +
+
+