hof 20.0.0-beta.2 → 20.0.0-beta.22

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 (59) hide show
  1. package/.github/workflows/automate-publish.yml +1 -1
  2. package/.github/workflows/automate-tag.yml +1 -1
  3. package/.nyc_output/e2fdc3eb-4fd2-47e0-a392-fe5f665776a4.json +1 -0
  4. package/.nyc_output/processinfo/e2fdc3eb-4fd2-47e0-a392-fe5f665776a4.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 +11 -15
  21. package/frontend/govuk-template/build/config.js +1 -1
  22. package/frontend/template-mixins/mixins/template-mixins.js +12 -9
  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 +3 -3
  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/partials/form.html +2 -1
  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 +11 -12
  48. package/sandbox/apps/sandbox/index.js +1 -5
  49. package/sandbox/assets/scss/app.scss +0 -52
  50. package/sandbox/package.json +2 -0
  51. package/sandbox/public/css/app.css +4908 -4965
  52. package/sandbox/public/js/bundle.js +79 -65
  53. package/sandbox/server.js +7 -1
  54. package/sandbox/yarn.lock +39 -564
  55. package/transpiler/lib/write-files.js +1 -2
  56. package/utilities/helpers/index.js +16 -1
  57. package/wizard/index.js +1 -0
  58. package/.nyc_output/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
  59. package/.nyc_output/processinfo/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +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 uuid = 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 = uuid.v1();
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
  }
@@ -128,22 +124,22 @@ module.exports = class Controller extends BaseController {
128
124
  }
129
125
 
130
126
  getFirstFormItem(fields) {
131
- let firstFieldKey
127
+ let firstFieldKey;
132
128
  if (_.size(fields)) {
133
- firstFieldKey = Object.keys(fields)[0]
129
+ firstFieldKey = Object.keys(fields)[0];
134
130
  }
135
131
  return firstFieldKey | 'main-content';
136
132
  }
137
133
 
138
- getHeader(route, lookup, locals){
134
+ getHeader(route, lookup, locals) {
139
135
  return lookup(`pages.${route}.header`, locals);
140
136
  }
141
137
 
142
- getCaptionHeading(route, lookup, locals){
138
+ getCaptionHeading(route, lookup, locals) {
143
139
  return lookup(`pages.${route}.captionHeading`, locals);
144
140
  }
145
141
 
146
- getSubHeading(route, lookup, locals){
142
+ getSubHeading(route, lookup, locals) {
147
143
  return lookup(`pages.${route}.subHeading`, locals);
148
144
  }
149
145
 
@@ -171,14 +167,14 @@ module.exports = class Controller extends BaseController {
171
167
  if (req.form && req.form.options && req.form.options.fields) {
172
168
  const field = req.form.options.fields[key];
173
169
  // get first option for radios
174
- if(field.mixin === 'radio-group') {
175
- req.form.errors[key].errorLinkId = key + "-" + field.options[0];
170
+ if (field.mixin === 'radio-group') {
171
+ req.form.errors[key].errorLinkId = key + '-' + field.options[0];
172
+ // eslint-disable-next-line brace-style
176
173
  }
177
174
  // get first field for date input control
178
175
  else if (field && field.controlType === 'date-input') {
179
176
  req.form.errors[key].errorLinkId = key + '-day';
180
- }
181
- else {
177
+ } else {
182
178
  req.form.errors[key].errorLinkId = key;
183
179
  }
184
180
  }
@@ -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}}',