hof 20.0.0-beta.3 → 20.0.0-beta.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.github/workflows/automate-publish.yml +1 -1
  2. package/.github/workflows/automate-tag.yml +1 -1
  3. package/README.md +329 -256
  4. package/build/lib/mkdir.js +2 -2
  5. package/components/date/index.js +37 -26
  6. package/components/date/templates/date.html +3 -3
  7. package/components/emailer/index.js +49 -41
  8. package/components/emailer/transports/debug.js +1 -2
  9. package/components/index.js +2 -1
  10. package/components/notify/index.js +60 -0
  11. package/components/notify/notify.js +25 -0
  12. package/components/summary/index.js +18 -0
  13. package/config/hof-defaults.js +5 -3
  14. package/config/rate-limits.js +20 -0
  15. package/config/sanitisation-rules.js +29 -0
  16. package/controller/base-controller.js +26 -8
  17. package/controller/controller.js +23 -17
  18. package/frontend/govuk-template/build/config.js +1 -1
  19. package/frontend/template-mixins/mixins/template-mixins.js +12 -8
  20. package/frontend/template-mixins/partials/forms/checkbox-group.html +13 -4
  21. package/frontend/template-mixins/partials/forms/input-text-date.html +1 -1
  22. package/frontend/template-mixins/partials/forms/input-text-group.html +6 -4
  23. package/frontend/template-mixins/partials/forms/option-group.html +12 -3
  24. package/frontend/template-mixins/partials/forms/select.html +3 -3
  25. package/frontend/template-mixins/partials/forms/textarea-group.html +3 -3
  26. package/frontend/template-mixins/partials/mixins/panel.html +1 -2
  27. package/frontend/template-partials/translations/src/en/errors.json +12 -0
  28. package/frontend/template-partials/views/layout.html +10 -3
  29. package/frontend/template-partials/views/partials/cookie-banner.html +1 -1
  30. package/frontend/template-partials/views/partials/form.html +2 -1
  31. package/frontend/template-partials/views/partials/warn.html +7 -0
  32. package/frontend/template-partials/views/rate-limit-error.html +10 -0
  33. package/frontend/themes/gov-uk/client-js/govuk-cookies.js +43 -44
  34. package/frontend/themes/gov-uk/client-js/index.js +2 -2
  35. package/frontend/themes/gov-uk/client-js/skip-to-main.js +18 -17
  36. package/frontend/themes/gov-uk/styles/govuk.scss +4 -0
  37. package/frontend/themes/gov-uk/styles/modules/_validation.scss +2 -2
  38. package/frontend/toolkit/assets/javascript/form-focus.js +10 -1
  39. package/frontend/toolkit/assets/javascript/validation.js +6 -1
  40. package/index.js +9 -4
  41. package/lib/router.js +2 -1
  42. package/lib/settings.js +9 -8
  43. package/middleware/errors.js +32 -0
  44. package/middleware/index.js +2 -1
  45. package/middleware/rate-limiter.js +98 -0
  46. package/package.json +6 -6
  47. package/sandbox/apps/sandbox/fields.js +12 -12
  48. package/sandbox/apps/sandbox/index.js +1 -5
  49. package/sandbox/apps/sandbox/translations/en/default.json +24 -0
  50. package/sandbox/assets/scss/app.scss +0 -52
  51. package/sandbox/package.json +2 -0
  52. package/sandbox/public/css/app.css +4908 -4965
  53. package/sandbox/public/js/bundle.js +79 -65
  54. package/sandbox/server.js +7 -1
  55. package/sandbox/yarn.lock +39 -564
  56. package/transpiler/lib/write-files.js +1 -2
  57. package/utilities/helpers/index.js +16 -1
  58. package/wizard/index.js +1 -0
  59. package/.nyc_output/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
  60. package/.nyc_output/processinfo/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
  61. package/.nyc_output/processinfo/index.json +0 -1
  62. package/.vscode/settings.json +0 -6
  63. 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}}',