Subscribers: router & form helpers

Form:
- add confirm, location & referrer hidden fields
- add script to populate location & referrer
- add helper for creating the email field
- pass through input class and placeholder for email from top level form helper
- rename subscribe_form template & helper as it sounds more natural
- handle success and error cases differently
- improve error message display
- ensure useful data is passed back so that we can show nice messages
- check for honeypot value being filled out
- refactor error handler to set an error and always still render
This commit is contained in:
Hannah Wolfe 2016-04-21 16:37:52 +01:00
parent 74b83766bc
commit 6ef79534e4
11 changed files with 217 additions and 51 deletions

View File

@ -37,8 +37,6 @@
uploadStarted=(action 'uploadStarted')
uploadFinished=(action 'uploadFinished')
uploadSuccess=(action 'uploadSuccess')}}
<span>Ensure the CSV has a column named <code>email</code>.</span>
{{/liquid-if}}
</div>

View File

@ -297,15 +297,22 @@ subscribers = {
var dataToImport = line.split(',');
if (firstLine) {
emailIdx = _.findIndex(dataToImport, function (columnName) {
if (columnName.match(/email/g)) {
return true;
} else {
return false;
}
});
if (dataToImport.length === 1) {
emailIdx = 0;
} else {
emailIdx = _.findIndex(dataToImport, function (columnName) {
if (columnName.match(/email/g)) {
return true;
} else {
return false;
}
});
}
if (emailIdx === -1) {
return reject(new errors.ValidationError('Email column not found'));
return reject(new errors.ValidationError(
'Couldn\'t find your email addresses! Please use a column header which contains the word "email".'
));
}
firstLine = false;
} else if (emailIdx > -1) {

View File

@ -1,23 +1,73 @@
var _ = require('lodash'),
path = require('path'),
config = require('../../config'),
hbs = require('express-hbs'),
router = require('./lib/router'),
// Dirty require
template = require('../../helpers/template');
// Dirty requires
config = require('../../config'),
errors = require('../../errors'),
i18n = require('../../i18n'),
labs = require('../../utils/labs'),
template = require('../../helpers/template'),
utils = require('../../helpers/utils'),
params = ['error', 'success', 'email', 'referrer', 'location'],
/**
* Dirrrrrty script
* <script type="text/javascript">
* document.querySelector('.location').setAttribute('value', window.location.href);
* document.querySelector('.referrer').setAttribute('value', document.referrer);
* </script>
*/
subscribeScript = '<script type="text/javascript">(function(g,h,o,s,t){' +
'h[o](\'.location\')[s]=g.location.href;h[o](\'.referrer\')[s]=h.referrer;' +
'})(window,document,\'querySelector\',\'value\');</script>';
function makeHidden(name) {
return utils.inputTemplate({
type: 'hidden',
name: name,
className: name,
extras: ''
});
}
function subscribeFormHelper(options) {
var data = _.merge({}, options.hash, _.pick(options.data.root, params), {
action: path.join('/', config.paths.subdir, config.routeKeywords.subscribe, '/'),
script: new hbs.handlebars.SafeString(subscribeScript),
hidden: new hbs.handlebars.SafeString(makeHidden('confirm') + makeHidden('location') + makeHidden('referrer'))
});
return template.execute('subscribe_form', data, options);
}
module.exports = {
activate: function activate(ghost) {
var errorMessages = [
i18n.t('warnings.helpers.helperNotAvailable', {helperName: 'subscribe_form'}),
i18n.t('warnings.helpers.apiMustBeEnabled', {helperName: 'subscribe_form', flagName: 'subscribers'}),
i18n.t('warnings.helpers.seeLink', {url: 'http://support.ghost.org/subscribers-beta/'})
];
// Correct way to register a helper from an app
ghost.helpers.register('form_subscribe', function formSubscribeHelper(options) {
var data = _.merge({}, options.hash, {
action: path.join('/', config.paths.subdir, config.routeKeywords.subscribe, '/')
});
return template.execute('form_subscribe', data, options);
ghost.helpers.register('subscribe_form', function labsEnabledHelper() {
if (labs.isSet('subscribers') === true) {
return subscribeFormHelper.apply(this, arguments);
}
errors.logError.apply(this, errorMessages);
return new hbs.handlebars.SafeString('<script>console.error("' + errorMessages.join(' ') + '");</script>');
});
},
setupRoutes: function setupRoutes(blogRouter) {
blogRouter.use('/' + config.routeKeywords.subscribe + '/', router);
blogRouter.use('/' + config.routeKeywords.subscribe + '/', function labsEnabledRouter(req, res, next) {
if (labs.isSet('subscribers') === true) {
return router.apply(this, arguments);
}
next();
});
}
};

View File

@ -1,18 +1,16 @@
var path = require('path'),
express = require('express'),
templates = require('../../../controllers/frontend/templates'),
setResponseContext = require('../../../controllers/frontend/context'),
subscribeRouter = express.Router(),
// Dirty requires
api = require('../../../api'),
subscribeRouter = express.Router();
templates = require('../../../controllers/frontend/templates'),
setResponseContext = require('../../../controllers/frontend/context');
function controller(req, res) {
var defaultView = path.resolve(__dirname, 'views', 'subscribe.hbs'),
paths = templates.getActiveThemePaths(req.app.get('activeTheme')),
data = {};
if (res.error) {
data.error = res.error;
}
data = req.body;
setResponseContext(req, res);
if (paths.hasOwnProperty('subscribe.hbs')) {
@ -22,11 +20,47 @@ function controller(req, res) {
}
}
function errorHandler(error, req, res, next) {
/*jshint unused:false */
if (error.statusCode !== 404) {
res.locals.error = error;
return controller(req, res);
}
next(error);
}
function honeyPot(req, res, next) {
if (!req.body.hasOwnProperty('confirm') || req.body.confirm !== '') {
return next(new Error('Oops, something went wrong!'));
}
// we don't need this anymore
delete req.body.confirm;
next();
}
function handleSource(req, res, next) {
req.body.subscribed_url = req.body.location;
req.body.subscribed_referrer = req.body.referrer;
delete req.body.location;
delete req.body.referrer;
// do something here to get post_id
next();
}
function storeSubscriber(req, res, next) {
return api.subscribers.add({subscribers: [req.body]}, {context: {external: true}}).then(function (result) {
next();
});
req.body.status = 'subscribed';
return api.subscribers.add({subscribers: [req.body]}, {context: {external: true}})
.then(function () {
res.locals.success = true;
next();
})
.catch(function (error) {
next(error);
});
}
// subscribe frontend route
@ -35,9 +69,14 @@ subscribeRouter.route('/')
controller
)
.post(
honeyPot,
handleSource,
storeSubscriber,
controller
);
// configure an error handler just for subscribe problems
subscribeRouter.use(errorHandler);
module.exports = subscribeRouter;
module.exports.controller = controller;

View File

@ -23,24 +23,40 @@
<div class="gh-viewport">
<main class="gh-main" role="main">
<div class="gh-flow">
<header class="gh-flow-head">
<nav class="gh-flow-nav">
<a href="{{@blog.url}}" class="gh-flow-back"><i class="icon-arrow-left"></i> Back</a>
</nav>
</header>
<div class="gh-flow-content-wrap">
<section class="gh-flow-content">
<header>
<h1>Subscribe</h1>
</header>
{{test}}
{{form_subscribe class="gh-signin"}}
{{^if success}}
<header>
<h1>Subscribe to {{@blog.title}}</h1>
</header>
{{subscribe_form
form_class="gh-signin"
input_class="gh-input"
button_class="btn btn-blue btn-block"
placeholder="Your email address"
autofocus="true"
}}
{{else}}
<header>
<h1>Subscribed!</h1>
</header>
<p>
You've successfully subscribed to <em>{{@blog.title}}</em>
with the email address <em>{{email}}</em>.
</p>
{{/if}}
</section>
</div>
</div>
</main>
{{#if error}}
<aside class="gh-notifications">
<article class="gh-notification gh-notification-red">
<div id="gh-notification-content">{{error.message}}</div>
</article>
</aside>
{{/if}}
</div>
</div>
</body>

View File

@ -38,6 +38,7 @@ coreHelpers.url = require('./url');
// Specialist helpers for certain templates
coreHelpers.input_password = require('./input_password');
coreHelpers.input_email = require('./input_email');
coreHelpers.page_url = require('./page_url');
coreHelpers.pageUrl = require('./page_url').deprecated;
@ -97,6 +98,7 @@ registerHelpers = function (adminHbs) {
registerThemeHelper('has', coreHelpers.has);
registerThemeHelper('is', coreHelpers.is);
registerThemeHelper('image', coreHelpers.image);
registerThemeHelper('input_email', coreHelpers.input_email);
registerThemeHelper('input_password', coreHelpers.input_password);
registerThemeHelper('meta_description', coreHelpers.meta_description);
registerThemeHelper('meta_title', coreHelpers.meta_title);

View File

@ -0,0 +1,43 @@
// # Input Email Helper
// Usage: `{{input_email}}`
//
// Password input used on private.hbs for password-protected blogs
//
// We use the name meta_title to match the helper for consistency:
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
var hbs = require('express-hbs'),
utils = require('./utils'),
input_email;
input_email = function (options) {
options = options || {};
options.hash = options.hash || {};
var className = (options.hash.class) ? options.hash.class : 'subscribe-email',
extras = '',
output;
if (options.hash.autofocus) {
extras += 'autofocus="autofocus"';
}
if (options.hash.placeholder) {
extras += ' placeholder="' + options.hash.placeholder + '"';
}
if (options.hash.value) {
extras += ' value="' + options.hash.value + '"';
}
output = utils.inputTemplate({
type: 'email',
name: 'email',
className: className,
extras: extras
});
return new hbs.handlebars.SafeString(output);
};
module.exports = input_email;

View File

@ -1,8 +0,0 @@
<form method="post" action="{{action}}" class="{{class}}">
<div class="form-group">
<span class="input-icon icon-mail">
<input type="email" name="email" class="gh-input">
</span>
</div>
<button class="btn btn-blue btn-block" type="submit">Subscribe</button>
</form>

View File

@ -0,0 +1,15 @@
<form method="post" action="{{action}}" class="{{form_class}}">
{{! This is required for the form to work correctly }}
{{hidden}}
<div class="form-group{{#if error}} error{{/if}}">
{{input_email class=input_class placeholder=placeholder value=email autofocus=autofocus}}
</div>
<button class="{{button_class}}" type="submit">Subscribe</button>
{{! This is used to get extra info about where this subscriber came from }}
{{script}}
</form>
{{#if error}}
<p class="main-error">{{{error.message}}}</p>
{{/if}}

View File

@ -27,6 +27,7 @@ themeHandler = {
// Setup handlebars for the current context (admin or theme)
configHbsForContext: function configHbsForContext(req, res, next) {
var themeData = _.cloneDeep(config.theme),
labsData = _.cloneDeep(config.labs),
blogApp = req.app;
if (req.secure && config.urlSSL) {
@ -38,7 +39,7 @@ themeHandler = {
themeData.posts_per_page = themeData.postsPerPage;
delete themeData.postsPerPage;
hbs.updateTemplateOptions({data: {blog: themeData}});
hbs.updateTemplateOptions({data: {blog: themeData, labs: labsData}});
if (config.paths.themePath && blogApp.get('activeTheme')) {
blogApp.set('views', path.join(config.paths.themePath, blogApp.get('activeTheme')));

View File

@ -450,6 +450,9 @@
"unableToSendEmail": "Ghost is currently unable to send email."
},
"helpers": {
"helperNotAvailable": "The \\{\\{{helperName}\\}\\} helper is not available.",
"apiMustBeEnabled": "The {flagName} labs flag must be enabled if you wish to use the \\{\\{{helperName}\\}\\} helper.",
"seeLink": "See {url}",
"foreach": {
"iteratorNeeded": "Need to pass an iterator to #foreach"
},