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.
- package/.github/workflows/automate-publish.yml +1 -1
- package/.github/workflows/automate-tag.yml +1 -1
- package/.nyc_output/e2fdc3eb-4fd2-47e0-a392-fe5f665776a4.json +1 -0
- package/.nyc_output/processinfo/e2fdc3eb-4fd2-47e0-a392-fe5f665776a4.json +1 -0
- package/.nyc_output/processinfo/index.json +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 +11 -15
- package/frontend/govuk-template/build/config.js +1 -1
- package/frontend/template-mixins/mixins/template-mixins.js +12 -9
- package/frontend/template-mixins/partials/forms/checkbox-group.html +12 -3
- package/frontend/template-mixins/partials/forms/input-text-date.html +1 -1
- package/frontend/template-mixins/partials/forms/input-text-group.html +3 -3
- 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/partials/form.html +2 -1
- 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 +11 -12
- package/sandbox/apps/sandbox/index.js +1 -5
- 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/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 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
|
};
|
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
|
}
|
@@ -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 +
|
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}}',
|