hof 20.0.0-beta.3 → 20.0.0-beta.30

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. package/.github/workflows/automate-publish.yml +1 -1
  2. package/.github/workflows/automate-tag.yml +1 -1
  3. package/.nyc_output/19d65ff1-2145-4f30-a2cb-ec31dee604f4.json +1 -0
  4. package/.nyc_output/processinfo/19d65ff1-2145-4f30-a2cb-ec31dee604f4.json +1 -0
  5. package/.nyc_output/processinfo/index.json +1 -1
  6. package/README.md +329 -256
  7. package/build/lib/mkdir.js +2 -2
  8. package/components/date/index.js +37 -26
  9. package/components/date/templates/date.html +3 -3
  10. package/components/emailer/index.js +49 -41
  11. package/components/emailer/transports/debug.js +1 -2
  12. package/components/index.js +2 -1
  13. package/components/notify/index.js +60 -0
  14. package/components/notify/notify.js +25 -0
  15. package/components/summary/index.js +18 -0
  16. package/config/hof-defaults.js +5 -3
  17. package/config/rate-limits.js +20 -0
  18. package/config/sanitisation-rules.js +29 -0
  19. package/controller/base-controller.js +26 -8
  20. package/controller/controller.js +23 -17
  21. package/frontend/govuk-template/build/config.js +1 -1
  22. package/frontend/template-mixins/mixins/template-mixins.js +12 -8
  23. package/frontend/template-mixins/partials/forms/checkbox-group.html +12 -3
  24. package/frontend/template-mixins/partials/forms/input-text-date.html +1 -1
  25. package/frontend/template-mixins/partials/forms/input-text-group.html +6 -4
  26. package/frontend/template-mixins/partials/forms/option-group.html +12 -3
  27. package/frontend/template-mixins/partials/forms/select.html +3 -3
  28. package/frontend/template-mixins/partials/forms/textarea-group.html +3 -3
  29. package/frontend/template-mixins/partials/mixins/panel.html +1 -2
  30. package/frontend/template-partials/translations/src/en/errors.json +12 -0
  31. package/frontend/template-partials/views/layout.html +10 -3
  32. package/frontend/template-partials/views/partials/cookie-banner.html +1 -1
  33. package/frontend/template-partials/views/partials/form.html +2 -1
  34. package/frontend/template-partials/views/partials/warn.html +7 -0
  35. package/frontend/template-partials/views/rate-limit-error.html +10 -0
  36. package/frontend/themes/gov-uk/client-js/govuk-cookies.js +43 -44
  37. package/frontend/themes/gov-uk/client-js/index.js +2 -2
  38. package/frontend/themes/gov-uk/client-js/skip-to-main.js +18 -17
  39. package/frontend/themes/gov-uk/styles/govuk.scss +4 -0
  40. package/frontend/themes/gov-uk/styles/modules/_validation.scss +2 -2
  41. package/frontend/toolkit/assets/javascript/form-focus.js +10 -1
  42. package/frontend/toolkit/assets/javascript/validation.js +6 -1
  43. package/index.js +9 -4
  44. package/lib/router.js +2 -1
  45. package/lib/settings.js +9 -8
  46. package/middleware/errors.js +32 -0
  47. package/middleware/index.js +2 -1
  48. package/middleware/rate-limiter.js +98 -0
  49. package/package.json +6 -6
  50. package/sandbox/apps/sandbox/fields.js +12 -12
  51. package/sandbox/apps/sandbox/index.js +1 -5
  52. package/sandbox/assets/scss/app.scss +0 -52
  53. package/sandbox/package.json +2 -0
  54. package/sandbox/public/css/app.css +4908 -4965
  55. package/sandbox/public/js/bundle.js +79 -65
  56. package/sandbox/server.js +7 -1
  57. package/sandbox/yarn.lock +39 -564
  58. package/transpiler/lib/write-files.js +1 -2
  59. package/utilities/helpers/index.js +16 -1
  60. package/wizard/index.js +1 -0
  61. package/.nyc_output/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
  62. package/.nyc_output/processinfo/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
  63. package/.vscode/settings.json +0 -6
  64. package/sandbox/.env +0 -1
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('path');
4
- const mkdirp = require('mkdirp');
4
+ const fs = require('fs');
5
5
 
6
6
  module.exports = file => new Promise((resolve, reject) => {
7
7
  const dir = path.dirname(file);
8
- mkdirp(dir, err => err ? reject(err) : resolve());
8
+ fs.mkdir(dir, {recursive: true}, err => err ? reject(err) : resolve());
9
9
  });
@@ -40,7 +40,7 @@ const conditionalTranslate = (key, translate) => {
40
40
  };
41
41
 
42
42
  const getLegendClassName = field => field && field.legend && field.legend.className || '';
43
- const getNoHeading = field => field && field.noHeading || '';
43
+ const getIsPageHeading = field => field && field.isPageHeading || '';
44
44
 
45
45
  module.exports = (key, opts) => {
46
46
  if (!key) {
@@ -61,6 +61,37 @@ module.exports = (key, opts) => {
61
61
  dayOptional = true;
62
62
  }
63
63
 
64
+ // take the 3 date parts, padding or defaulting
65
+ // to '01' if applic, then create a date value in the
66
+ // format YYYY-MM-DD. Save to req.body for processing
67
+ const preProcess = (req, res, next) => {
68
+ const parts = getParts(req.body, fields, key);
69
+ if (_.some(parts, part => part !== '')) {
70
+ if (dayOptional && parts.day === '') {
71
+ parts.day = '01';
72
+ } else {
73
+ parts.day = pad(parts.day);
74
+ }
75
+ if (monthOptional && parts.month === '') {
76
+ parts.month = '01';
77
+ } else {
78
+ parts.month = pad(parts.month);
79
+ }
80
+ req.body[key] = `${parts.year}-${parts.month}-${parts.day}`;
81
+ }
82
+ next();
83
+ };
84
+
85
+ // defaultFormatters on the base controller replace '--' with '-' on the process step.
86
+ // This ensures having the correct number of hyphens, so values do not jump from year to month.
87
+ // This should only be done on a partially completed date field otherwise the validation messages break.
88
+ const postProcess = (req, res, next) => {
89
+ const value = req.form.values[key];
90
+ if (value) {
91
+ req.form.values[key] = req.body[key];
92
+ }
93
+ next();
94
+ };
64
95
  // if date field is included in errorValues, extend
65
96
  // errorValues with the individual components
66
97
  const preGetErrors = (req, res, next) => {
@@ -114,9 +145,9 @@ module.exports = (key, opts) => {
114
145
  const legend = conditionalTranslate(`fields.${key}.legend`, req.translate);
115
146
  const hint = conditionalTranslate(`fields.${key}.hint`, req.translate);
116
147
  const legendClassName = getLegendClassName(options);
117
- const noHeading = getNoHeading(options);
148
+ const isPageHeading = getIsPageHeading(options);
118
149
  const error = req.form.errors && req.form.errors[key];
119
- res.render(template, { key, legend, legendClassName, noHeading, hint, error }, (err, html) => {
150
+ res.render(template, { key, legend, legendClassName, isPageHeading, hint, error }, (err, html) => {
120
151
  if (err) {
121
152
  next(err);
122
153
  } else {
@@ -127,35 +158,15 @@ module.exports = (key, opts) => {
127
158
  });
128
159
  };
129
160
 
130
- // take the 3 date parts, padding or defaulting
131
- // to '01' if applic, then create a date value in the
132
- // format YYYY-MM-DD. Save to req.body for processing
133
- const preProcess = (req, res, next) => {
134
- const parts = getParts(req.body, fields, key);
135
- if (_.some(parts, part => part !== '')) {
136
- if (dayOptional && parts.day === '') {
137
- parts.day = '01';
138
- } else {
139
- parts.day = pad(parts.day);
140
- }
141
- if (monthOptional && parts.month === '') {
142
- parts.month = '01';
143
- } else {
144
- parts.month = pad(parts.month);
145
- }
146
- req.body[key] = `${parts.year}-${parts.month}-${parts.day}`;
147
- }
148
- next();
149
- };
150
-
151
161
  // return config extended with hooks
152
162
  return Object.assign({}, options, {
153
163
  hooks: {
164
+ 'pre-process': preProcess,
165
+ 'post-process': postProcess,
154
166
  'pre-getErrors': preGetErrors,
155
167
  'post-getErrors': postGetErrors,
156
168
  'post-getValues': postGetValues,
157
- 'pre-render': preRender,
158
- 'pre-process': preProcess
169
+ 'pre-render': preRender
159
170
  }
160
171
  });
161
172
  };
@@ -1,9 +1,9 @@
1
1
  <div class="govuk-form-group {{#error}}govuk-form-group--error{{/error}}">
2
2
  <fieldset id="{{key}}-group" class="govuk-fieldset{{#className}} {{className}}{{/className}}" role="group">
3
- <legend class="govuk-fieldset__legend {{^noHeading}}govuk-fieldset__legend--l{{/noHeading}}{{#legendClassName}} {{legendClassName}}{{/legendClassName}}">
4
- {{^noHeading}}<h1 class="govuk-fieldset__heading">{{/noHeading}}
3
+ <legend class="govuk-fieldset__legend {{#isPageHeading}}govuk-fieldset__legend--l{{/isPageHeading}}{{#legendClassName}} {{legendClassName}}{{/legendClassName}}">
4
+ {{#isPageHeading}}<h1 class="govuk-fieldset__heading">{{/isPageHeading}}
5
5
  {{legend}}
6
- {{^noHeading}}</h1>{{/noHeading}}
6
+ {{#isPageHeading}}</h1>{{/isPageHeading}}
7
7
  </legend>
8
8
  {{#hint}}
9
9
  <span id="{{key}}-hint" class="govuk-hint">{{hint}}</span>
@@ -17,48 +17,56 @@ module.exports = config => {
17
17
  }
18
18
 
19
19
  return superclass => class EmailBehaviour extends superclass {
20
- successHandler(req, res, callback) {
21
- Promise.resolve()
22
- .then(() => {
23
- debug(`Loading email template from ${config.template}`);
24
- return new Promise((resolve, reject) => {
25
- fs.readFile(config.template, (err, template) => err ? reject(err) : resolve(template.toString('utf8')));
20
+ async successHandler(req, res, next) {
21
+ req.sessionModel.unset('nodemailer-error');
22
+
23
+ try {
24
+ debug(`Loading email template from ${config.template}`);
25
+
26
+ const template = await new Promise((resolve, reject) => {
27
+ return fs.readFile(config.template, (err, resolvedTemplate) => {
28
+ return err ? reject(err) : resolve(resolvedTemplate.toString('utf8'));
26
29
  });
27
- })
28
- .then(template => {
29
- debug('Rendering email content');
30
- const data = config.parse(req.sessionModel.toJSON(), req.translate);
31
- return Hogan.compile(template).render(data);
32
- })
33
- .then(body => {
34
- debug('Building email settings');
35
- const settings = { body };
36
-
37
- if (typeof config.recipient === 'function') {
38
- settings.recipient = config.recipient(req.sessionModel.toJSON());
39
- } else {
40
- settings.recipient = req.sessionModel.get(config.recipient) || config.recipient;
41
- }
42
- if (typeof settings.recipient !== 'string' || !settings.recipient.includes('@')) {
43
- throw new Error('hof-behaviour-emailer: invalid recipient');
44
- }
45
-
46
- if (typeof config.subject === 'function') {
47
- settings.subject = config.subject(req.sessionModel.toJSON(), req.translate);
48
- } else {
49
- settings.subject = config.subject;
50
- }
51
-
52
- return settings;
53
- })
54
- .then(settings => {
55
- debug('Sending email', settings);
56
- return emailer.send(settings);
57
- })
58
- .then(() => {
59
- debug('Email sent successfully');
60
- super.successHandler(req, res, callback);
61
- }, callback);
30
+ });
31
+
32
+ debug('Rendering email content');
33
+
34
+ const data = config.parse(req.sessionModel.toJSON(), req.translate);
35
+
36
+ debug('Building email settings');
37
+
38
+ const settings = { body: Hogan.compile(template).render(data) };
39
+
40
+ if (typeof config.recipient === 'function') {
41
+ settings.recipient = config.recipient(req.sessionModel.toJSON());
42
+ } else {
43
+ settings.recipient = req.sessionModel.get(config.recipient) || config.recipient;
44
+ }
45
+ if (typeof settings.recipient !== 'string' || !settings.recipient.includes('@')) {
46
+ return next(new Error('hof-behaviour-emailer: invalid recipient'));
47
+ }
48
+
49
+ if (typeof config.subject === 'function') {
50
+ settings.subject = config.subject(req.sessionModel.toJSON(), req.translate);
51
+ } else {
52
+ settings.subject = config.subject;
53
+ }
54
+
55
+ debug('Sending email', settings);
56
+
57
+ await emailer.send(settings);
58
+
59
+ debug('Email sent successfully');
60
+
61
+ return super.successHandler(req, res, next);
62
+ } catch (e) {
63
+ if (config.emailerFallback) {
64
+ req.log('error', e.message || e);
65
+ req.sessionModel.set('nodemailer-error', true);
66
+ return super.successHandler(req, res, next);
67
+ }
68
+ return next(e);
69
+ }
62
70
  }
63
71
  };
64
72
  };
@@ -4,7 +4,6 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const cp = require('child_process');
7
- const mkdirp = require('mkdirp');
8
7
 
9
8
  const mimes = {
10
9
  '.gif': 'image/gif',
@@ -12,7 +11,7 @@ const mimes = {
12
11
  };
13
12
 
14
13
  const mkdir = dir => new Promise((resolve, reject) => {
15
- mkdirp(dir, err => err ? reject(err) : resolve());
14
+ fs.mkdir(dir, {recursive: true}, err => err ? reject(err) : resolve());
16
15
  });
17
16
 
18
17
  const cidToBase64 = (h, attachments) => {
@@ -5,5 +5,6 @@ module.exports = {
5
5
  clearSession: require('./clear-session'),
6
6
  date: require('./date'),
7
7
  emailer: require('./emailer'),
8
- summary: require('./summary')
8
+ summary: require('./summary'),
9
+ notify: require('./notify')
9
10
  };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const Notify = require('./notify');
4
+ const Hogan = require('hogan.js');
5
+ const fs = require('fs');
6
+
7
+ module.exports = config => {
8
+ const notify = new Notify(config);
9
+ config.parse = config.parse || (data => data);
10
+
11
+ if (!config.recipient) {
12
+ throw new Error('Email recipient must be defined');
13
+ }
14
+ if (typeof config.template !== 'string') {
15
+ throw new Error('Email template must be defined');
16
+ }
17
+
18
+ return superclass => class NotifyBehaviour extends superclass {
19
+ successHandler(req, res, next) {
20
+ Promise.resolve()
21
+ .then(() => {
22
+ return new Promise((resolve, reject) => {
23
+ fs.readFile(config.template, (err, template) => err ? reject(err) : resolve(template.toString('utf8')));
24
+ });
25
+ })
26
+ .then(template => {
27
+ const data = config.parse(req.sessionModel.toJSON(), req.translate);
28
+ return Hogan.compile(template).render(data);
29
+ })
30
+ .then(body => {
31
+ const settings = { body };
32
+
33
+ if (typeof config.recipient === 'function') {
34
+ settings.recipient = config.recipient(req.sessionModel.toJSON());
35
+ } else {
36
+ settings.recipient = req.sessionModel.get(config.recipient) || config.recipient;
37
+ }
38
+ if (typeof settings.recipient !== 'string' || !settings.recipient.includes('@')) {
39
+ throw new Error('hof-behaviour-emailer: invalid recipient');
40
+ }
41
+
42
+ if (typeof config.subject === 'function') {
43
+ settings.subject = config.subject(req.sessionModel.toJSON(), req.translate);
44
+ } else {
45
+ settings.subject = config.subject;
46
+ }
47
+
48
+ return settings;
49
+ })
50
+ .then(settings => {
51
+ return notify.send(settings);
52
+ })
53
+ .then(() => {
54
+ super.successHandler(req, res, next);
55
+ }, next);
56
+ }
57
+ };
58
+ };
59
+
60
+ module.exports.Notify = Notify;
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+ const NotifyClient = require('notifications-node-client').NotifyClient;
3
+ const {v4: uuidv4} = require('uuid');
4
+
5
+ module.exports = class Notify {
6
+ constructor(opts) {
7
+ const options = opts || {};
8
+ this.options = options;
9
+ this.notifyClient = new NotifyClient(options.notifyApiKey);
10
+ this.notifyTemplate = options.notifyTemplate;
11
+ }
12
+
13
+ send(email) {
14
+ const reference = uuidv4();
15
+
16
+ return this.notifyClient.sendEmail(this.notifyTemplate, email.recipient, {
17
+ personalisation: {
18
+ 'email-subject': email.subject,
19
+ 'email-body': email.body
20
+ },
21
+ reference });
22
+ }
23
+ };
24
+
25
+ module.exports.NotifyClient = NotifyClient;
@@ -1,6 +1,9 @@
1
1
 
2
2
  'use strict';
3
3
 
4
+ const config = require('../../config/rate-limits');
5
+ const rateLimiter = require('../../middleware/rate-limiter');
6
+
4
7
  const concat = (x, y) => x.concat(y);
5
8
  const flatMap = (f, xs) => xs.map(f).reduce(concat, []);
6
9
 
@@ -215,4 +218,19 @@ module.exports = SuperClass => class extends SuperClass {
215
218
  rows
216
219
  });
217
220
  }
221
+
222
+ validate(req, res, next) {
223
+ if (!config.rateLimits.submissions.active) {
224
+ return super.validate(req, res, next);
225
+ }
226
+ // how do we stop this ballsing up our tests??????
227
+ const options = Object.assign({}, config, { logger: req });
228
+
229
+ return rateLimiter(options, 'submissions')(req, res, err => {
230
+ if (err) {
231
+ return next(err);
232
+ }
233
+ return super.validate(req, res, next);
234
+ });
235
+ }
218
236
  };
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
  /* eslint no-process-env: "off" */
3
+ const rateLimits = require('./rate-limits');
3
4
 
4
5
  const defaults = {
5
6
  appName: process.env.APP_NAME || 'HOF Application',
@@ -19,7 +20,7 @@ const defaults = {
19
20
  host: process.env.HOST || '0.0.0.0',
20
21
  port: process.env.PORT || '8080',
21
22
  env: process.env.NODE_ENV || 'development',
22
- gaTagId: process.env.GA_TAG,
23
+ gaTagId: process.env.GA_TAG || 'Test-GA-Tag',
23
24
  ga4TagId: process.env.GA_4_TAG,
24
25
  gaCrossDomainTrackingTagId: process.env.GDS_CROSS_DOMAIN_GA_TAG,
25
26
  loglevel: process.env.LOG_LEVEL || 'info',
@@ -31,7 +32,8 @@ const defaults = {
31
32
  session: {
32
33
  ttl: process.env.SESSION_TTL || 1800,
33
34
  secret: process.env.SESSION_SECRET || 'changethis',
34
- name: process.env.SESSION_NAME || 'hod.sid'
35
+ name: process.env.SESSION_NAME || 'hod.sid',
36
+ sanitiseInputs: false
35
37
  },
36
38
  apis: {
37
39
  pdfConverter: process.env.PDF_CONVERTER_URL
@@ -39,4 +41,4 @@ const defaults = {
39
41
  serveStatic: process.env.SERVE_STATIC_FILES !== 'false'
40
42
  };
41
43
 
42
- module.exports = defaults;
44
+ module.exports = Object.assign({}, defaults, rateLimits);
@@ -0,0 +1,20 @@
1
+
2
+ module.exports = {
3
+ rateLimits: {
4
+ env: process.env.NODE_ENV,
5
+ requests: {
6
+ active: false,
7
+ windowSizeInMinutes: 5,
8
+ maxWindowRequestCount: 100,
9
+ windowLogIntervalInMinutes: 1,
10
+ errCode: 'DDOS_RATE_LIMIT'
11
+ },
12
+ submissions: {
13
+ active: false,
14
+ windowSizeInMinutes: 10,
15
+ maxWindowRequestCount: 1,
16
+ windowLogIntervalInMinutes: 1,
17
+ errCode: 'SUBMISSION_RATE_LIMIT'
18
+ }
19
+ }
20
+ };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+ /* eslint no-process-env: "off" */
3
+
4
+ const sanitisationBlacklistArray = {
5
+ // Input will be sanitised using the below rules
6
+ // The key is what we're sanitising out
7
+ // The regex is the rule we used to find them (note some dictate repeating characters)
8
+ // And the replace is what we're replacing that pattern with. Usually nothing sometimes a
9
+ // single character or sometimes a single character followed by a "-"
10
+ '/*': { regex: '\/\\*', replace: '-' },
11
+ '*/': { regex: '\\*\\/', replace: '-' },
12
+ '|': { regex: '\\|', replace: '-' },
13
+ '&&': { regex: '&&+', replace: '&' },
14
+ '@@': { regex: '@@+', replace: '@' },
15
+ '/..;/': { regex: '/\\.\\.;/', replace: '-' }, // Purposely input before ".." as they conflict
16
+ // '..': { regex: '\\.\\.+', replace: '.' }, // Agreed to disable this rule for now unless its specifically required
17
+ '/etc/passwd': { regex: '\/etc\/passwd', replace: '-' },
18
+ 'c:\\': { regex: 'c:\\\\', replace: '-' },
19
+ 'cmd.exe': { regex: 'cmd\\.exe', replace: '-' },
20
+ '<': { regex: '<', replace: '<-' },
21
+ '>': { regex: '>', replace: '>-' },
22
+ '[': { regex: '\\[+', replace: '[-' },
23
+ ']': { regex: '\\]+', replace: ']-' },
24
+ '~': { regex: '~', replace: '~-' },
25
+ '&#': { regex: '&#', replace: '-' },
26
+ '%U': { regex: '%U', replace: '-' }
27
+ };
28
+
29
+ module.exports = sanitisationBlacklistArray;
@@ -8,6 +8,8 @@ const debug = require('debug')('hmpo:form');
8
8
  const dataFormatter = require('./formatting');
9
9
  const dataValidator = require('./validation');
10
10
  const ErrorClass = require('./validation-error');
11
+ const Helpers = require('../utilities').helpers;
12
+ const sanitisationBlacklistArray = require('../config/sanitisation-rules');
11
13
 
12
14
  module.exports = class BaseController extends EventEmitter {
13
15
  constructor(options) {
@@ -69,6 +71,7 @@ module.exports = class BaseController extends EventEmitter {
69
71
  this._configure.bind(this),
70
72
  this._process.bind(this),
71
73
  this._validate.bind(this),
74
+ this._sanitize.bind(this),
72
75
  this._getHistoricalValues.bind(this),
73
76
  this.saveValues.bind(this),
74
77
  this.successHandler.bind(this),
@@ -162,6 +165,28 @@ module.exports = class BaseController extends EventEmitter {
162
165
  return validator(key, req.form.values[key], req.form.values, emptyValue);
163
166
  }
164
167
 
168
+ _sanitize(req, res, callback) {
169
+ // Sanitisation could be disabled in the config
170
+ if(!this.options.sanitiseInputs) return callback();
171
+
172
+ // If we don't have any data, no need to progress
173
+ if(!_.isEmpty(req.form.values)) {
174
+ Object.keys(req.form.values).forEach(function (property, propertyIndex) {
175
+ // If it's not a string, don't sanitise it
176
+ if(_.isString(req.form.values[property])) {
177
+ // For each property in our form data
178
+ Object.keys(sanitisationBlacklistArray).forEach(function (blacklisted, blacklistedIndex) {
179
+ const blacklistedDetail = sanitisationBlacklistArray[blacklisted];
180
+ const regexQuery = new RegExp(blacklistedDetail.regex, 'gi');
181
+ // Will perform the required replace based on our passed in regex and the replace string
182
+ req.form.values[property] = req.form.values[property].replace(regexQuery, blacklistedDetail.replace);
183
+ });
184
+ }
185
+ });
186
+ }
187
+ return callback();
188
+ }
189
+
165
190
  _process(req, res, callback) {
166
191
  req.form.values = req.form.values || {};
167
192
  const formatter = dataFormatter(
@@ -213,16 +238,9 @@ module.exports = class BaseController extends EventEmitter {
213
238
  }
214
239
 
215
240
  _getForkTarget(req, res) {
216
- function evalCondition(condition) {
217
- return _.isFunction(condition) ?
218
- condition(req, res) :
219
- condition.value === (req.form.values[condition.field] ||
220
- (req.form.historicalValues && req.form.historicalValues[condition.field]));
221
- }
222
-
223
241
  // If a fork condition is met, its target supercedes the next property
224
242
  return req.form.options.forks.reduce((result, value) =>
225
- evalCondition(value.condition) ?
243
+ Helpers.isFieldValueInPageOrSessionValid(req, res, value.condition) ?
226
244
  value.target :
227
245
  result
228
246
  , req.form.options.next);
@@ -4,6 +4,7 @@ const _ = require('lodash');
4
4
  const i18nLookup = require('i18n-lookup');
5
5
  const Mustache = require('mustache');
6
6
  const BaseController = require('./base-controller');
7
+ const Helpers = require('../utilities').helpers;
7
8
 
8
9
  const omitField = (field, req) => field.useWhen && (typeof field.useWhen === 'string'
9
10
  ? req.sessionModel.get(field.useWhen) !== 'true'
@@ -54,12 +55,7 @@ module.exports = class Controller extends BaseController {
54
55
 
55
56
  // If a form condition is met, its target supercedes the next property
56
57
  next = _.reduce(forks, (result, value) => {
57
- const evalCondition = condition => _.isFunction(condition) ?
58
- condition(req, res) :
59
- condition.value === (req.form.values[condition.field] ||
60
- (req.form.historicalValues && req.form.historicalValues[condition.field]));
61
-
62
- if (evalCondition(value.condition)) {
58
+ if (Helpers.isFieldValueInPageOrSessionValid(req, res, value.condition)) {
63
59
  if (value.continueOnEdit) {
64
60
  req.form.options.continueOnEdit = true;
65
61
  }
@@ -119,6 +115,7 @@ module.exports = class Controller extends BaseController {
119
115
  title: this.getTitle(route, lookup, req.form.options.fields, res.locals),
120
116
  header: this.getHeader(route, lookup, res.locals),
121
117
  captionHeading: this.getCaptionHeading(route, lookup, res.locals),
118
+ warning: this.getWarning(route, lookup, res.locals),
122
119
  subHeading: this.getSubHeading(route, lookup, res.locals),
123
120
  intro: this.getIntro(route, lookup, res.locals),
124
121
  backLink: this.getBackLink(req, res),
@@ -128,25 +125,29 @@ module.exports = class Controller extends BaseController {
128
125
  }
129
126
 
130
127
  getFirstFormItem(fields) {
131
- let firstFieldKey
128
+ let firstFieldKey;
132
129
  if (_.size(fields)) {
133
- firstFieldKey = Object.keys(fields)[0]
130
+ firstFieldKey = Object.keys(fields)[0];
134
131
  }
135
132
  return firstFieldKey | 'main-content';
136
133
  }
137
134
 
138
- getHeader(route, lookup, locals){
135
+ getHeader(route, lookup, locals) {
139
136
  return lookup(`pages.${route}.header`, locals);
140
137
  }
141
138
 
142
- getCaptionHeading(route, lookup, locals){
139
+ getCaptionHeading(route, lookup, locals) {
143
140
  return lookup(`pages.${route}.captionHeading`, locals);
144
141
  }
145
142
 
146
- getSubHeading(route, lookup, locals){
143
+ getSubHeading(route, lookup, locals) {
147
144
  return lookup(`pages.${route}.subHeading`, locals);
148
145
  }
149
146
 
147
+ getWarning(route, lookup, locals) {
148
+ return lookup(`pages.${route}.warning`, locals);
149
+ }
150
+
150
151
  getTitle(route, lookup, fields, locals) {
151
152
  let fieldName = '';
152
153
  if (_.size(fields)) {
@@ -170,15 +171,20 @@ module.exports = class Controller extends BaseController {
170
171
  Object.keys(req.form.errors).forEach(key => {
171
172
  if (req.form && req.form.options && req.form.options.fields) {
172
173
  const field = req.form.options.fields[key];
173
- // get first option for radios
174
- if(field.mixin === 'radio-group') {
175
- req.form.errors[key].errorLinkId = key + "-" + field.options[0];
174
+ // get first option for radios and checkbox
175
+ if (field.mixin === 'radio-group' || field.mixin === 'checkbox-group') {
176
+ // get first option for radios and checkbox where there is a toggle
177
+ if(typeof field.options[0] === 'object') {
178
+ req.form.errors[key].errorLinkId = key + '-' + field.options[0].value;
179
+ } else {
180
+ req.form.errors[key].errorLinkId = key + '-' + field.options[0];
181
+ }
182
+ // eslint-disable-next-line brace-style
176
183
  }
177
184
  // get first field for date input control
178
- else if (field && field.controlType === 'date-input') {
185
+ else if (field && field.mixin === 'input-date') {
179
186
  req.form.errors[key].errorLinkId = key + '-day';
180
- }
181
- else {
187
+ } else {
182
188
  req.form.errors[key].errorLinkId = key;
183
189
  }
184
190
  }
@@ -4,7 +4,7 @@ module.exports = {
4
4
  htmlLang: '{{htmlLang}}',
5
5
  assetPath: '{{govukAssetPath}}',
6
6
  afterHeader: '{{$afterHeader}}{{/afterHeader}}',
7
- bodyClasses: '{{$bodyClasses}}{{/bodyClasses}}',
7
+ bodyClasses: '{{$bodyClasses}}{{/bodyClasses}}',
8
8
  bodyStart: '{{$bodyStart}}{{/bodyStart}}',
9
9
  bodyEnd: '{{$bodyEnd}}{{/bodyEnd}}',
10
10
  content: '{{$main}}{{/main}}',