hof 20.0.0-beta.3 → 20.0.0-beta.31
Sign up to get free protection for your applications and to get access to all the features.
- package/.github/workflows/automate-publish.yml +1 -1
- package/.github/workflows/automate-tag.yml +1 -1
- package/README.md +329 -256
- package/build/lib/mkdir.js +2 -2
- package/components/date/index.js +37 -26
- package/components/date/templates/date.html +3 -3
- package/components/emailer/index.js +49 -41
- package/components/emailer/transports/debug.js +1 -2
- package/components/index.js +2 -1
- package/components/notify/index.js +60 -0
- package/components/notify/notify.js +25 -0
- package/components/summary/index.js +18 -0
- package/config/hof-defaults.js +5 -3
- package/config/rate-limits.js +20 -0
- package/config/sanitisation-rules.js +29 -0
- package/controller/base-controller.js +26 -8
- package/controller/controller.js +23 -17
- package/frontend/govuk-template/build/config.js +1 -1
- package/frontend/template-mixins/mixins/template-mixins.js +12 -8
- package/frontend/template-mixins/partials/forms/checkbox-group.html +13 -4
- package/frontend/template-mixins/partials/forms/input-text-date.html +1 -1
- package/frontend/template-mixins/partials/forms/input-text-group.html +6 -4
- package/frontend/template-mixins/partials/forms/option-group.html +12 -3
- package/frontend/template-mixins/partials/forms/select.html +3 -3
- package/frontend/template-mixins/partials/forms/textarea-group.html +3 -3
- package/frontend/template-mixins/partials/mixins/panel.html +1 -2
- package/frontend/template-partials/translations/src/en/errors.json +12 -0
- package/frontend/template-partials/views/layout.html +10 -3
- package/frontend/template-partials/views/partials/cookie-banner.html +1 -1
- package/frontend/template-partials/views/partials/form.html +2 -1
- package/frontend/template-partials/views/partials/warn.html +7 -0
- package/frontend/template-partials/views/rate-limit-error.html +10 -0
- package/frontend/themes/gov-uk/client-js/govuk-cookies.js +43 -44
- package/frontend/themes/gov-uk/client-js/index.js +2 -2
- package/frontend/themes/gov-uk/client-js/skip-to-main.js +18 -17
- package/frontend/themes/gov-uk/styles/govuk.scss +4 -0
- package/frontend/themes/gov-uk/styles/modules/_validation.scss +2 -2
- package/frontend/toolkit/assets/javascript/form-focus.js +10 -1
- package/frontend/toolkit/assets/javascript/validation.js +6 -1
- package/index.js +9 -4
- package/lib/router.js +2 -1
- package/lib/settings.js +9 -8
- package/middleware/errors.js +32 -0
- package/middleware/index.js +2 -1
- package/middleware/rate-limiter.js +98 -0
- package/package.json +6 -6
- package/sandbox/apps/sandbox/fields.js +12 -12
- package/sandbox/apps/sandbox/index.js +1 -5
- package/sandbox/apps/sandbox/translations/en/default.json +24 -0
- package/sandbox/assets/scss/app.scss +0 -52
- package/sandbox/package.json +2 -0
- package/sandbox/public/css/app.css +4908 -4965
- package/sandbox/public/js/bundle.js +79 -65
- package/sandbox/server.js +7 -1
- package/sandbox/yarn.lock +39 -564
- package/transpiler/lib/write-files.js +1 -2
- package/utilities/helpers/index.js +16 -1
- package/wizard/index.js +1 -0
- package/.nyc_output/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
- package/.nyc_output/processinfo/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
- package/.vscode/settings.json +0 -6
- package/sandbox/.env +0 -1
package/build/lib/mkdir.js
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
'use strict';
|
2
2
|
|
3
3
|
const path = require('path');
|
4
|
-
const
|
4
|
+
const fs = require('fs');
|
5
5
|
|
6
6
|
module.exports = file => new Promise((resolve, reject) => {
|
7
7
|
const dir = path.dirname(file);
|
8
|
-
|
8
|
+
fs.mkdir(dir, {recursive: true}, err => err ? reject(err) : resolve());
|
9
9
|
});
|
package/components/date/index.js
CHANGED
@@ -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
|
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
|
148
|
+
const isPageHeading = getIsPageHeading(options);
|
118
149
|
const error = req.form.errors && req.form.errors[key];
|
119
|
-
res.render(template, { key, legend, legendClassName,
|
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 {{
|
4
|
-
{{
|
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
|
-
{{
|
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,
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
.
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
.
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
14
|
+
fs.mkdir(dir, {recursive: true}, err => err ? reject(err) : resolve());
|
16
15
|
});
|
17
16
|
|
18
17
|
const cidToBase64 = (h, attachments) => {
|
package/components/index.js
CHANGED
@@ -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
|
};
|
package/config/hof-defaults.js
CHANGED
@@ -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
|
-
|
243
|
+
Helpers.isFieldValueInPageOrSessionValid(req, res, value.condition) ?
|
226
244
|
value.target :
|
227
245
|
result
|
228
246
|
, req.form.options.next);
|
package/controller/controller.js
CHANGED
@@ -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
|
-
|
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
|
-
|
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.
|
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}}',
|