@dwp/govuk-casa 8.16.2 → 8.16.3
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/dist/assets/css/casa-ie8.css +1 -1
- package/dist/assets/css/casa.css +1 -1
- package/dist/casa.d.ts +13 -13
- package/dist/casa.js +17 -7
- package/dist/casa.js.map +1 -1
- package/dist/lib/CasaTemplateLoader.d.ts +1 -1
- package/dist/lib/CasaTemplateLoader.js +13 -14
- package/dist/lib/CasaTemplateLoader.js.map +1 -1
- package/dist/lib/JourneyContext.d.ts +10 -4
- package/dist/lib/JourneyContext.js +57 -47
- package/dist/lib/JourneyContext.js.map +1 -1
- package/dist/lib/MutableRouter.d.ts +1 -1
- package/dist/lib/MutableRouter.js +22 -23
- package/dist/lib/MutableRouter.js.map +1 -1
- package/dist/lib/Plan.d.ts +5 -5
- package/dist/lib/Plan.js +49 -36
- package/dist/lib/Plan.js.map +1 -1
- package/dist/lib/ValidationError.d.ts +1 -1
- package/dist/lib/ValidationError.js +9 -9
- package/dist/lib/ValidationError.js.map +1 -1
- package/dist/lib/ValidatorFactory.js +4 -7
- package/dist/lib/ValidatorFactory.js.map +1 -1
- package/dist/lib/configuration-ingestor.d.ts +75 -14
- package/dist/lib/configuration-ingestor.js +156 -64
- package/dist/lib/configuration-ingestor.js.map +1 -1
- package/dist/lib/configure.js +11 -10
- package/dist/lib/configure.js.map +1 -1
- package/dist/lib/constants.js +8 -8
- package/dist/lib/context-id-generators.d.ts +1 -1
- package/dist/lib/context-id-generators.js +7 -4
- package/dist/lib/context-id-generators.js.map +1 -1
- package/dist/lib/end-session.js +2 -2
- package/dist/lib/field.d.ts +6 -6
- package/dist/lib/field.js +15 -21
- package/dist/lib/field.js.map +1 -1
- package/dist/lib/index.d.ts +13 -13
- package/dist/lib/index.js +17 -7
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/logger.js +7 -7
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/mount.js +3 -3
- package/dist/lib/mount.js.map +1 -1
- package/dist/lib/nunjucks-filters.d.ts +5 -1
- package/dist/lib/nunjucks-filters.js +37 -23
- package/dist/lib/nunjucks-filters.js.map +1 -1
- package/dist/lib/nunjucks.d.ts +2 -2
- package/dist/lib/nunjucks.js +6 -7
- package/dist/lib/nunjucks.js.map +1 -1
- package/dist/lib/utils.js +52 -42
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/validators/dateObject.d.ts +3 -3
- package/dist/lib/validators/dateObject.js +44 -37
- package/dist/lib/validators/dateObject.js.map +1 -1
- package/dist/lib/validators/email.d.ts +2 -2
- package/dist/lib/validators/email.js +4 -5
- package/dist/lib/validators/email.js.map +1 -1
- package/dist/lib/validators/inArray.d.ts +2 -2
- package/dist/lib/validators/inArray.js +5 -6
- package/dist/lib/validators/inArray.js.map +1 -1
- package/dist/lib/validators/index.d.ts +10 -10
- package/dist/lib/validators/index.js.map +1 -1
- package/dist/lib/validators/nino.d.ts +2 -2
- package/dist/lib/validators/nino.js +10 -7
- package/dist/lib/validators/nino.js.map +1 -1
- package/dist/lib/validators/postalAddressObject.d.ts +2 -2
- package/dist/lib/validators/postalAddressObject.js +52 -39
- package/dist/lib/validators/postalAddressObject.js.map +1 -1
- package/dist/lib/validators/range.d.ts +2 -2
- package/dist/lib/validators/range.js +6 -7
- package/dist/lib/validators/range.js.map +1 -1
- package/dist/lib/validators/regex.d.ts +2 -2
- package/dist/lib/validators/regex.js +4 -5
- package/dist/lib/validators/regex.js.map +1 -1
- package/dist/lib/validators/required.d.ts +2 -2
- package/dist/lib/validators/required.js +6 -9
- package/dist/lib/validators/required.js.map +1 -1
- package/dist/lib/validators/strlen.d.ts +2 -2
- package/dist/lib/validators/strlen.js +8 -9
- package/dist/lib/validators/strlen.js.map +1 -1
- package/dist/lib/validators/wordCount.d.ts +2 -2
- package/dist/lib/validators/wordCount.js +10 -9
- package/dist/lib/validators/wordCount.js.map +1 -1
- package/dist/lib/waypoint-url.d.ts +4 -4
- package/dist/lib/waypoint-url.js +23 -23
- package/dist/lib/waypoint-url.js.map +1 -1
- package/dist/middleware/body-parser.d.ts +27 -5
- package/dist/middleware/body-parser.js +37 -6
- package/dist/middleware/body-parser.js.map +1 -1
- package/dist/middleware/csrf.d.ts +3 -0
- package/dist/middleware/csrf.js +3 -0
- package/dist/middleware/csrf.js.map +1 -1
- package/dist/middleware/data.d.ts +22 -5
- package/dist/middleware/data.js +37 -7
- package/dist/middleware/data.js.map +1 -1
- package/dist/middleware/gather-fields.d.ts +1 -1
- package/dist/middleware/gather-fields.js +4 -3
- package/dist/middleware/gather-fields.js.map +1 -1
- package/dist/middleware/i18n.d.ts +11 -2
- package/dist/middleware/i18n.js +26 -17
- package/dist/middleware/i18n.js.map +1 -1
- package/dist/middleware/post.d.ts +3 -1
- package/dist/middleware/post.js +35 -18
- package/dist/middleware/post.js.map +1 -1
- package/dist/middleware/pre.d.ts +1 -1
- package/dist/middleware/pre.js +44 -21
- package/dist/middleware/pre.js.map +1 -1
- package/dist/middleware/progress-journey.d.ts +1 -1
- package/dist/middleware/progress-journey.js +5 -5
- package/dist/middleware/progress-journey.js.map +1 -1
- package/dist/middleware/sanitise-fields.d.ts +2 -2
- package/dist/middleware/sanitise-fields.js +13 -11
- package/dist/middleware/sanitise-fields.js.map +1 -1
- package/dist/middleware/serve-first-waypoint.d.ts +1 -1
- package/dist/middleware/serve-first-waypoint.js +6 -4
- package/dist/middleware/serve-first-waypoint.js.map +1 -1
- package/dist/middleware/session.d.ts +27 -8
- package/dist/middleware/session.js +53 -25
- package/dist/middleware/session.js.map +1 -1
- package/dist/middleware/skip-waypoint.d.ts +1 -1
- package/dist/middleware/skip-waypoint.js +3 -3
- package/dist/middleware/skip-waypoint.js.map +1 -1
- package/dist/middleware/steer-journey.d.ts +1 -1
- package/dist/middleware/steer-journey.js +15 -13
- package/dist/middleware/steer-journey.js.map +1 -1
- package/dist/middleware/strip-proxy-path.d.ts +1 -1
- package/dist/middleware/strip-proxy-path.js +3 -3
- package/dist/middleware/strip-proxy-path.js.map +1 -1
- package/dist/middleware/validate-fields.d.ts +2 -2
- package/dist/middleware/validate-fields.js +2 -5
- package/dist/middleware/validate-fields.js.map +1 -1
- package/dist/routes/ancillary.d.ts +2 -2
- package/dist/routes/ancillary.js +3 -3
- package/dist/routes/ancillary.js.map +1 -1
- package/dist/routes/journey.d.ts +1 -1
- package/dist/routes/journey.js +85 -31
- package/dist/routes/journey.js.map +1 -1
- package/dist/routes/static.d.ts +2 -2
- package/dist/routes/static.js +18 -18
- package/dist/routes/static.js.map +1 -1
- package/package.json +33 -36
- package/src/casa.js +13 -13
- package/src/lib/CasaTemplateLoader.js +21 -17
- package/src/lib/JourneyContext.js +118 -79
- package/src/lib/MutableRouter.js +30 -26
- package/src/lib/Plan.js +109 -62
- package/src/lib/ValidationError.js +13 -10
- package/src/lib/ValidatorFactory.js +7 -8
- package/src/lib/configuration-ingestor.js +200 -74
- package/src/lib/configure.js +31 -30
- package/src/lib/constants.js +8 -8
- package/src/lib/context-id-generators.js +39 -38
- package/src/lib/end-session.js +3 -3
- package/src/lib/field.js +48 -32
- package/src/lib/index.js +12 -12
- package/src/lib/logger.js +9 -9
- package/src/lib/mount.js +68 -73
- package/src/lib/nunjucks-filters.js +57 -44
- package/src/lib/nunjucks.js +20 -16
- package/src/lib/utils.js +69 -44
- package/src/lib/validators/dateObject.js +57 -48
- package/src/lib/validators/email.js +8 -9
- package/src/lib/validators/inArray.js +8 -9
- package/src/lib/validators/index.js +11 -11
- package/src/lib/validators/nino.js +25 -12
- package/src/lib/validators/postalAddressObject.js +73 -55
- package/src/lib/validators/range.js +9 -11
- package/src/lib/validators/regex.js +7 -8
- package/src/lib/validators/required.js +13 -14
- package/src/lib/validators/strlen.js +11 -12
- package/src/lib/validators/wordCount.js +17 -12
- package/src/lib/waypoint-url.js +48 -33
- package/src/middleware/body-parser.js +44 -10
- package/src/middleware/csrf.js +4 -1
- package/src/middleware/data.js +62 -25
- package/src/middleware/gather-fields.js +8 -8
- package/src/middleware/i18n.js +49 -39
- package/src/middleware/post.js +47 -21
- package/src/middleware/pre.js +60 -35
- package/src/middleware/progress-journey.js +32 -18
- package/src/middleware/sanitise-fields.js +43 -20
- package/src/middleware/serve-first-waypoint.js +12 -10
- package/src/middleware/session.js +97 -65
- package/src/middleware/skip-waypoint.js +7 -9
- package/src/middleware/steer-journey.js +39 -27
- package/src/middleware/strip-proxy-path.js +8 -7
- package/src/middleware/validate-fields.js +5 -12
- package/src/routes/ancillary.js +4 -6
- package/src/routes/journey.js +158 -78
- package/src/routes/static.js +61 -28
package/src/lib/waypoint-url.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/** @access private */
|
|
7
|
-
const reUrlProtocolExtract = /^url:\/\/(.+)$/i
|
|
7
|
+
const reUrlProtocolExtract = /^url:\/\/(.+)$/i;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Sanitise a waypoint string.
|
|
@@ -13,7 +13,8 @@ const reUrlProtocolExtract = /^url:\/\/(.+)$/i
|
|
|
13
13
|
* @param {string} w Waypoint
|
|
14
14
|
* @returns {string} Sanitised waypoint
|
|
15
15
|
*/
|
|
16
|
-
const sanitiseWaypoint = (w) =>
|
|
16
|
+
const sanitiseWaypoint = (w) =>
|
|
17
|
+
w.replace(/[^/a-z0-9_-]/gi, "").replace(/\/+/g, "/");
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Sanitise a waypoint string, with allowed URL parameters:
|
|
@@ -25,7 +26,7 @@ const sanitiseWaypoint = (w) => w.replace(/[^/a-z0-9_-]/ig, '').replace(/\/+/g,
|
|
|
25
26
|
*/
|
|
26
27
|
const sanitiseWaypointWithAllowedParams = (w) => {
|
|
27
28
|
// Extract URL params
|
|
28
|
-
const parts = w.split(
|
|
29
|
+
const parts = w.split("?");
|
|
29
30
|
if (parts.length !== 2) {
|
|
30
31
|
return sanitiseWaypoint(w);
|
|
31
32
|
}
|
|
@@ -34,14 +35,17 @@ const sanitiseWaypointWithAllowedParams = (w) => {
|
|
|
34
35
|
|
|
35
36
|
// Strip all but those parameters allowed
|
|
36
37
|
const validatedUrlSearchParams = new URLSearchParams();
|
|
37
|
-
for (const pk of [
|
|
38
|
+
for (const pk of ["contextid"]) {
|
|
38
39
|
if (urlSearchParams.has(pk)) {
|
|
39
40
|
validatedUrlSearchParams.set(pk, urlSearchParams.get(pk));
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
return `${sanitiseWaypoint(waypoint)}?${validatedUrlSearchParams.toString()}`.replace(
|
|
44
|
-
|
|
44
|
+
return `${sanitiseWaypoint(waypoint)}?${validatedUrlSearchParams.toString()}`.replace(
|
|
45
|
+
/\?$/,
|
|
46
|
+
"",
|
|
47
|
+
);
|
|
48
|
+
};
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* Generate a URL pointing at a particular waypoint.
|
|
@@ -56,41 +60,49 @@ const sanitiseWaypointWithAllowedParams = (w) => {
|
|
|
56
60
|
* })
|
|
57
61
|
* @memberof module:@dwp/govuk-casa
|
|
58
62
|
* @param {object} obj Options
|
|
59
|
-
* @param {string} [obj.waypoint
|
|
60
|
-
* @param {string} [obj.mountUrl
|
|
63
|
+
* @param {string} [obj.waypoint] Waypoint
|
|
64
|
+
* @param {string} [obj.mountUrl] Mount URL
|
|
61
65
|
* @param {JourneyContext} [obj.journeyContext] JourneyContext
|
|
62
|
-
* @param {boolean} [obj.edit
|
|
66
|
+
* @param {boolean} [obj.edit] Turn edit mode on or off
|
|
63
67
|
* @param {string} [obj.editOrigin] Edit mode original URL
|
|
64
68
|
* @param {boolean} [obj.skipTo] Skip to this waypoint from the current one
|
|
65
|
-
* @param {string} [obj.routeName
|
|
69
|
+
* @param {string} [obj.routeName] Plan route name; next | prev
|
|
66
70
|
* @returns {string} URL
|
|
67
71
|
*/
|
|
68
|
-
export default function waypointUrl(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
export default function waypointUrl(
|
|
73
|
+
{
|
|
74
|
+
waypoint = "",
|
|
75
|
+
mountUrl = "/",
|
|
76
|
+
journeyContext,
|
|
77
|
+
edit = false,
|
|
78
|
+
editOrigin,
|
|
79
|
+
skipTo,
|
|
80
|
+
routeName = "next",
|
|
81
|
+
} = Object.create(null),
|
|
82
|
+
) {
|
|
83
|
+
const url = new URL("https://placeholder.test");
|
|
78
84
|
|
|
79
85
|
// Handle url:// protocol
|
|
80
86
|
// - This will generate a link to the root handler "_" for the given mount path
|
|
81
|
-
if (String(waypoint).substr(0, 7) ===
|
|
87
|
+
if (String(waypoint).substr(0, 7) === "url:///") {
|
|
82
88
|
const m = waypoint.match(reUrlProtocolExtract);
|
|
83
89
|
|
|
84
|
-
const u = new URL(
|
|
90
|
+
const u = new URL(
|
|
91
|
+
sanitiseWaypointWithAllowedParams(m[1]),
|
|
92
|
+
"https://placeholder.test/",
|
|
93
|
+
);
|
|
85
94
|
url.pathname = `${sanitiseWaypoint(u.pathname)}/_/`;
|
|
86
95
|
|
|
87
|
-
url.searchParams.set(
|
|
88
|
-
url.searchParams.set(
|
|
96
|
+
url.searchParams.set("refmount", `url://${mountUrl}`);
|
|
97
|
+
url.searchParams.set("route", routeName);
|
|
89
98
|
for (const [uk, uv] of u.searchParams.entries()) {
|
|
90
99
|
url.searchParams.append(uk, uv);
|
|
91
100
|
}
|
|
92
101
|
} else {
|
|
93
|
-
const u = new URL(
|
|
102
|
+
const u = new URL(
|
|
103
|
+
sanitiseWaypointWithAllowedParams(`${mountUrl}${waypoint}`),
|
|
104
|
+
"https://placeholder.test/",
|
|
105
|
+
);
|
|
94
106
|
url.pathname = u.pathname;
|
|
95
107
|
url.search = u.search;
|
|
96
108
|
}
|
|
@@ -100,26 +112,29 @@ export default function waypointUrl({
|
|
|
100
112
|
// added if the context ID already appears in the url path, i.e. to avoid
|
|
101
113
|
// `/path/1234-abcd/waypoint?contextid=1234-abcd` scenarios
|
|
102
114
|
if (
|
|
103
|
-
journeyContext
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
journeyContext &&
|
|
116
|
+
!journeyContext.isDefault() &&
|
|
117
|
+
journeyContext.identity.id &&
|
|
118
|
+
!mountUrl.includes(`/${journeyContext.identity.id}/`)
|
|
107
119
|
) {
|
|
108
|
-
url.searchParams.set(
|
|
120
|
+
url.searchParams.set("contextid", journeyContext.identity.id);
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
// Attach edit mode flag
|
|
112
124
|
if (edit === true) {
|
|
113
|
-
url.searchParams.set(
|
|
125
|
+
url.searchParams.set("edit", "true");
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
if (edit && editOrigin) {
|
|
117
|
-
url.searchParams.set(
|
|
129
|
+
url.searchParams.set(
|
|
130
|
+
"editorigin",
|
|
131
|
+
sanitiseWaypointWithAllowedParams(editOrigin),
|
|
132
|
+
);
|
|
118
133
|
}
|
|
119
134
|
|
|
120
135
|
// Skipto
|
|
121
136
|
if (skipTo) {
|
|
122
|
-
url.searchParams.set(
|
|
137
|
+
url.searchParams.set("skipto", sanitiseWaypointWithAllowedParams(skipTo));
|
|
123
138
|
}
|
|
124
139
|
|
|
125
140
|
return `${sanitiseWaypoint(url.pathname)}${url.search}`;
|
|
@@ -1,30 +1,64 @@
|
|
|
1
|
-
import { urlencoded as expressBodyParser } from
|
|
1
|
+
import { urlencoded as expressBodyParser } from "express";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import("express").RequestHandler} RequestHandler
|
|
5
|
+
* @access private
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import("express").Request} Request
|
|
10
|
+
* @access private
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {import("express").Response} Response
|
|
15
|
+
* @access private
|
|
16
|
+
*/
|
|
2
17
|
|
|
3
18
|
const rProto = /__proto__/i;
|
|
4
19
|
const rPrototype = /prototype[='"[\]]/i;
|
|
5
20
|
const rConstructor = /constructor[='"[\]]/i;
|
|
6
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Verify request body.
|
|
24
|
+
*
|
|
25
|
+
* @param {Request} req HTTP request
|
|
26
|
+
* @param {Response} res HTTP response
|
|
27
|
+
* @param {Buffer} buf Buffer
|
|
28
|
+
* @param {string} encoding Character encoding
|
|
29
|
+
* @returns {void}
|
|
30
|
+
* @throws {Error} For invalid bodies
|
|
31
|
+
*/
|
|
7
32
|
export function verifyBody(req, res, buf, encoding) {
|
|
8
|
-
const body = decodeURI(buf.toString(encoding)).replace(
|
|
33
|
+
const body = decodeURI(buf.toString(encoding)).replace(
|
|
34
|
+
/[\s\u200B-\u200D\uFEFF]/g,
|
|
35
|
+
"",
|
|
36
|
+
);
|
|
9
37
|
if (rProto.test(body)) {
|
|
10
|
-
throw new Error(
|
|
38
|
+
throw new Error("Request body verification failed (__proto__)");
|
|
11
39
|
}
|
|
12
40
|
if (rPrototype.test(body)) {
|
|
13
|
-
throw new Error(
|
|
41
|
+
throw new Error("Request body verification failed (prototype)");
|
|
14
42
|
}
|
|
15
43
|
if (rConstructor.test(body)) {
|
|
16
|
-
throw new Error(
|
|
44
|
+
throw new Error("Request body verification failed (constructor)");
|
|
17
45
|
}
|
|
18
46
|
}
|
|
19
47
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
48
|
+
/**
|
|
49
|
+
* Body parsing middleware.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} opts Options
|
|
52
|
+
* @param {number} opts.formMaxParams Max number of parameters that should be
|
|
53
|
+
* parsed
|
|
54
|
+
* @param {number} opts.formMaxBytes Max bytes that should be read
|
|
55
|
+
* @returns {RequestHandler[]} Middleware functions
|
|
56
|
+
*/
|
|
57
|
+
export default function bodyParserMiddleware({ formMaxParams, formMaxBytes }) {
|
|
24
58
|
return [
|
|
25
59
|
expressBodyParser({
|
|
26
60
|
extended: true,
|
|
27
|
-
type:
|
|
61
|
+
type: "application/x-www-form-urlencoded",
|
|
28
62
|
inflate: true,
|
|
29
63
|
parameterLimit: formMaxParams,
|
|
30
64
|
limit: formMaxBytes,
|
package/src/middleware/csrf.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { csrfSync } from
|
|
1
|
+
import { csrfSync } from "csrf-sync";
|
|
2
2
|
|
|
3
3
|
// 2 middleware: one to generate the csrf token and check its validity (POST
|
|
4
4
|
// only), and one to provide that token to templates via the `casa.csrfToken`
|
|
5
5
|
// variable.
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
7
10
|
export default function csrfMiddleware() {
|
|
8
11
|
const { csrfSynchronisedProtection } = csrfSync({
|
|
9
12
|
getTokenFromRequest: (req) => req.body._csrf,
|
package/src/middleware/data.js
CHANGED
|
@@ -1,28 +1,57 @@
|
|
|
1
1
|
// Decorates the request with some contextual data about the user's journey
|
|
2
2
|
// through the application. This is used by downstream middleware and templates.
|
|
3
3
|
|
|
4
|
-
import lodash from
|
|
5
|
-
import JourneyContext from
|
|
6
|
-
import { validateUrlPath } from
|
|
7
|
-
import waypointUrl from
|
|
4
|
+
import lodash from "lodash";
|
|
5
|
+
import JourneyContext from "../lib/JourneyContext.js";
|
|
6
|
+
import { validateUrlPath } from "../lib/utils.js";
|
|
7
|
+
import waypointUrl from "../lib/waypoint-url.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import("express").RequestHandler} RequestHandler
|
|
11
|
+
* @access private
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {import("../casa.js").Plan} Plan
|
|
16
|
+
* @access private
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {import("../casa.js").ContextEventHandler} ContextEventHandler
|
|
21
|
+
* @access private
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {import("../casa.js").ContextIdGenerator} ContextIdGenerator
|
|
26
|
+
* @access private
|
|
27
|
+
*/
|
|
8
28
|
|
|
9
29
|
const { has } = lodash;
|
|
10
30
|
|
|
11
31
|
const editOrigin = (req) => {
|
|
12
|
-
if (has(req.query,
|
|
32
|
+
if (has(req.query, "editorigin")) {
|
|
13
33
|
return waypointUrl({ waypoint: req.query.editorigin });
|
|
14
34
|
}
|
|
15
|
-
if (has(req?.body,
|
|
35
|
+
if (has(req?.body, "editorigin")) {
|
|
16
36
|
return waypointUrl({ waypoint: req.body.editorigin });
|
|
17
37
|
}
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
38
|
+
return "";
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Data middleware.
|
|
43
|
+
*
|
|
44
|
+
* Decorates the request with some contextual data about the user's journey
|
|
45
|
+
* through the application. This is used by downstream middleware and
|
|
46
|
+
* templates.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} opts Options
|
|
49
|
+
* @param {Plan} opts.plan CASA Plan
|
|
50
|
+
* @param {ContextEventHandler[]} opts.events Event handlers
|
|
51
|
+
* @param {ContextIdGenerator} opts.contextIdGenerator Content ID generator
|
|
52
|
+
* @returns {RequestHandler[]} Middleware functions
|
|
53
|
+
*/
|
|
54
|
+
export default function dataMiddleware({ plan, events, contextIdGenerator }) {
|
|
26
55
|
return [
|
|
27
56
|
(req, res, next) => {
|
|
28
57
|
/* ------------------------------------------------ Request decorations */
|
|
@@ -36,10 +65,15 @@ export default function dataMiddleware({
|
|
|
36
65
|
|
|
37
66
|
// Current journey context, loaded from session, specified by
|
|
38
67
|
// `contextid` request parameter
|
|
39
|
-
journeyContext:
|
|
68
|
+
journeyContext:
|
|
69
|
+
JourneyContext.extractContextFromRequest(req).addEventListeners(
|
|
70
|
+
events,
|
|
71
|
+
),
|
|
40
72
|
|
|
41
73
|
// Edit mode
|
|
42
|
-
editMode:
|
|
74
|
+
editMode:
|
|
75
|
+
(has(req?.query, "edit") && has(req?.query, "editorigin")) ||
|
|
76
|
+
(has(req?.body, "edit") && has(req?.body, "editorigin")),
|
|
43
77
|
editOrigin: editOrigin(req),
|
|
44
78
|
};
|
|
45
79
|
|
|
@@ -56,7 +90,7 @@ export default function dataMiddleware({
|
|
|
56
90
|
/* ------------------------------------------------- Template variables */
|
|
57
91
|
|
|
58
92
|
// Capture mount URL that will be used in generating all browser URLs
|
|
59
|
-
const mountUrl = validateUrlPath(`${req.baseUrl}/`.replace(/\/+/g,
|
|
93
|
+
const mountUrl = validateUrlPath(`${req.baseUrl}/`.replace(/\/+/g, "/"));
|
|
60
94
|
|
|
61
95
|
// If this CASA app is mounted on a parameterised route, then all of its
|
|
62
96
|
// static assets (served by `staticRouter`) will, by default, be served
|
|
@@ -77,7 +111,9 @@ export default function dataMiddleware({
|
|
|
77
111
|
// Router, the `baseUrl` is different in each case, so we cannot rely
|
|
78
112
|
// on it to be consistent. Hence the need for this property, which will
|
|
79
113
|
// always be the non-parameterised version of the baseUrl.
|
|
80
|
-
const staticMountUrl = validateUrlPath(
|
|
114
|
+
const staticMountUrl = validateUrlPath(
|
|
115
|
+
`${req.unparameterisedBaseUrl}/`.replace(/\/+/g, "/"),
|
|
116
|
+
);
|
|
81
117
|
|
|
82
118
|
// CASA and userland templates
|
|
83
119
|
res.locals.casa = {
|
|
@@ -99,13 +135,14 @@ export default function dataMiddleware({
|
|
|
99
135
|
// the template author does not have to be concerned about the current
|
|
100
136
|
// "state" when generating URLs, but still has the ability to override
|
|
101
137
|
// these curried defaults if needs be.
|
|
102
|
-
res.locals.waypointUrl = (args) =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
138
|
+
res.locals.waypointUrl = (args) =>
|
|
139
|
+
waypointUrl({
|
|
140
|
+
mountUrl,
|
|
141
|
+
journeyContext: req.casa.journeyContext,
|
|
142
|
+
edit: req.casa.editMode,
|
|
143
|
+
editOrigin: req.casa.editOrigin,
|
|
144
|
+
...args,
|
|
145
|
+
});
|
|
109
146
|
|
|
110
147
|
next();
|
|
111
148
|
},
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// - Update the user's journey context with the new data
|
|
4
4
|
// - Remove validation date of JourneyContext so it can re-evaluted
|
|
5
5
|
|
|
6
|
-
import JourneyContext from
|
|
7
|
-
import { REQUEST_PHASE_GATHER } from
|
|
6
|
+
import JourneyContext from "../lib/JourneyContext.js";
|
|
7
|
+
import { REQUEST_PHASE_GATHER } from "../lib/constants.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @access private
|
|
@@ -19,13 +19,10 @@ import { REQUEST_PHASE_GATHER } from '../lib/constants.js';
|
|
|
19
19
|
*
|
|
20
20
|
* @param {object} obj Options
|
|
21
21
|
* @param {string} obj.waypoint Waypoint
|
|
22
|
-
* @param {PageField[]} [obj.fields
|
|
22
|
+
* @param {PageField[]} [obj.fields] Fields
|
|
23
23
|
* @returns {Array} Array of middleware
|
|
24
24
|
*/
|
|
25
|
-
export default ({
|
|
26
|
-
waypoint,
|
|
27
|
-
fields = [],
|
|
28
|
-
}) => [
|
|
25
|
+
export default ({ waypoint, fields = [] }) => [
|
|
29
26
|
(req, res, next) => {
|
|
30
27
|
// Store a copy of the journey context before modifying it. This is useful
|
|
31
28
|
// for any comparison work that may be done in subsequent middleware.
|
|
@@ -39,7 +36,10 @@ export default ({
|
|
|
39
36
|
/* eslint-disable security/detect-object-injection */
|
|
40
37
|
const persistentBody = Object.create(null);
|
|
41
38
|
for (let i = 0, l = fields.length; i < l; i++) {
|
|
42
|
-
if (
|
|
39
|
+
if (
|
|
40
|
+
fields[i].meta.persist &&
|
|
41
|
+
fields[i].getValue(req.body) !== undefined
|
|
42
|
+
) {
|
|
43
43
|
persistentBody[fields[i].name] = fields[i].getValue(req.body);
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/middleware/i18n.js
CHANGED
|
@@ -1,33 +1,38 @@
|
|
|
1
|
-
import { createInstance } from
|
|
2
|
-
import { LanguageDetector, handle } from
|
|
3
|
-
import { resolve, basename } from
|
|
4
|
-
import { existsSync, readFileSync, readdirSync } from
|
|
5
|
-
import deepmerge from
|
|
6
|
-
import
|
|
7
|
-
import logger from
|
|
1
|
+
import { createInstance } from "i18next";
|
|
2
|
+
import { LanguageDetector, handle } from "i18next-http-middleware";
|
|
3
|
+
import { resolve, basename } from "path";
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
5
|
+
import deepmerge from "deepmerge";
|
|
6
|
+
import { load as jsYamlLoad } from "js-yaml";
|
|
7
|
+
import logger from "../lib/logger.js";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import("express").RequestHandler} RequestHandler
|
|
11
|
+
* @access private
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const log = logger("middleware:i18n");
|
|
10
15
|
|
|
11
16
|
const loadJson = (file) => {
|
|
12
17
|
// Strip out newlines (this is a legacy feature which we're keeping for
|
|
13
18
|
// backwards compatibility).
|
|
14
19
|
/* eslint-disable-next-line security/detect-non-literal-fs-filename */
|
|
15
|
-
const json = readFileSync(file,
|
|
16
|
-
return JSON.parse(json.replace(/[\r\n]/g,
|
|
17
|
-
}
|
|
20
|
+
const json = readFileSync(file, "utf8");
|
|
21
|
+
return JSON.parse(json.replace(/[\r\n]/g, ""));
|
|
22
|
+
};
|
|
18
23
|
|
|
19
24
|
/* eslint-disable-next-line security/detect-non-literal-fs-filename */
|
|
20
|
-
const loadYaml = (file) =>
|
|
25
|
+
const loadYaml = (file) => jsYamlLoad(readFileSync(file, "utf8"));
|
|
21
26
|
|
|
22
27
|
const extract = (file) => {
|
|
23
|
-
const ext = /.yaml$/i.test(file) ?
|
|
24
|
-
const data = ext ===
|
|
28
|
+
const ext = /.yaml$/i.test(file) ? ".yaml" : ".json";
|
|
29
|
+
const data = ext === ".yaml" ? loadYaml(file) : loadJson(file);
|
|
25
30
|
|
|
26
31
|
return {
|
|
27
32
|
ns: basename(file, ext),
|
|
28
33
|
data,
|
|
29
34
|
};
|
|
30
|
-
}
|
|
35
|
+
};
|
|
31
36
|
|
|
32
37
|
const loadResources = (languages, directories) => {
|
|
33
38
|
const store = Object.create(null);
|
|
@@ -45,7 +50,7 @@ const loadResources = (languages, directories) => {
|
|
|
45
50
|
return;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
log.info(
|
|
53
|
+
log.info("Loading %s language from %s ...", language, dir);
|
|
49
54
|
/* eslint-disable-next-line security/detect-non-literal-fs-filename */
|
|
50
55
|
readdirSync(dir).forEach((file) => {
|
|
51
56
|
const { ns, data } = extract(resolve(dir, file));
|
|
@@ -61,10 +66,18 @@ const loadResources = (languages, directories) => {
|
|
|
61
66
|
});
|
|
62
67
|
|
|
63
68
|
return store;
|
|
64
|
-
}
|
|
65
|
-
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Internationalisation middleware.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} opts Options
|
|
75
|
+
* @param {string[]} [opts.languages] Language codes
|
|
76
|
+
* @param {string[]} [opts.directories] Source translations directories
|
|
77
|
+
* @returns {RequestHandler[]} Middleware functions
|
|
78
|
+
*/
|
|
66
79
|
export default function i18nMiddleware({
|
|
67
|
-
languages = [
|
|
80
|
+
languages = ["en", "cy"],
|
|
68
81
|
directories = [],
|
|
69
82
|
}) {
|
|
70
83
|
// Load _all_ translations, from all directories into memory.
|
|
@@ -72,32 +85,29 @@ export default function i18nMiddleware({
|
|
|
72
85
|
|
|
73
86
|
// Configure i18next
|
|
74
87
|
const i18nInstance = createInstance();
|
|
75
|
-
i18nInstance
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
});
|
|
88
|
+
i18nInstance.use(LanguageDetector).init({
|
|
89
|
+
initImmediate: false, // because we need synchronous loading
|
|
90
|
+
supportedLngs: languages,
|
|
91
|
+
fallbackLng: false,
|
|
92
|
+
defaultNS: "common",
|
|
93
|
+
// debug: true,
|
|
94
|
+
|
|
95
|
+
// All translation resources
|
|
96
|
+
resources,
|
|
97
|
+
|
|
98
|
+
// LanguageDetector options
|
|
99
|
+
detection: {
|
|
100
|
+
lookupQuerystring: "lang",
|
|
101
|
+
lookupSession: "language",
|
|
102
|
+
order: ["querystring", "session"],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
94
105
|
|
|
95
106
|
// 2 middleware: one to read/set the session language, and one to enhance the
|
|
96
107
|
// req/res objects with i18n features
|
|
97
108
|
return [
|
|
98
109
|
(req, res, next) => {
|
|
99
110
|
if (!req.session.language) {
|
|
100
|
-
/* eslint-disable-next-line prefer-destructuring */
|
|
101
111
|
req.session.language = languages[0];
|
|
102
112
|
}
|
|
103
113
|
if (req?.query.lang && languages.includes(req.query.lang)) {
|
package/src/middleware/post.js
CHANGED
|
@@ -1,61 +1,87 @@
|
|
|
1
1
|
// 2 middleware: one as a fallback 404 handler, one to handle thrown errors
|
|
2
|
-
import logger from
|
|
2
|
+
import logger from "../lib/logger.js";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import("express").RequestHandler} RequestHandler
|
|
6
|
+
* @access private
|
|
7
|
+
*/
|
|
5
8
|
|
|
9
|
+
const log = logger("middleware:post");
|
|
10
|
+
|
|
11
|
+
/** @returns {RequestHandler[]} Middleware functions */
|
|
6
12
|
export default function postMiddleware() {
|
|
7
13
|
return [
|
|
8
14
|
(req, res) => {
|
|
9
|
-
res.status(404).render(
|
|
15
|
+
res.status(404).render("casa/errors/404.njk");
|
|
10
16
|
},
|
|
11
17
|
/* eslint-disable-next-line no-unused-vars */
|
|
12
18
|
(err, req, res, next) => {
|
|
13
19
|
// In some cases, an error may have been thrown before the template assets
|
|
14
20
|
// have had a chance to initialise. So we use a hardcoded template in
|
|
15
21
|
// these cases to ensure the user sees an appropriate message.
|
|
16
|
-
let TEMPLATE =
|
|
22
|
+
let TEMPLATE = "casa/errors/500.njk";
|
|
17
23
|
if (!res.locals.t) {
|
|
18
|
-
res.locals.t = () =>
|
|
24
|
+
res.locals.t = () => "";
|
|
19
25
|
res.locals.casa = {
|
|
20
26
|
...res.locals?.casa,
|
|
21
27
|
mountUrl: `${req.baseUrl}/`,
|
|
22
28
|
};
|
|
23
|
-
TEMPLATE =
|
|
29
|
+
TEMPLATE = "casa/errors/static.njk";
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
// CSRF token is invalid in some way
|
|
27
|
-
if (err?.code ===
|
|
28
|
-
log.info(
|
|
29
|
-
|
|
33
|
+
if (err?.code === "EBADCSRFTOKEN") {
|
|
34
|
+
log.info(
|
|
35
|
+
"CSRF validation has failed. This may be caused by the user submitting a stale form from a previous session [EBADCSRFTOKEN]",
|
|
36
|
+
);
|
|
37
|
+
return res
|
|
38
|
+
.status(403)
|
|
39
|
+
.render(TEMPLATE, { errorCode: "bad_csrf_token", error: err });
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
// Body parsing verification check failed
|
|
33
|
-
if (err?.type ===
|
|
34
|
-
log.info(
|
|
35
|
-
|
|
43
|
+
if (err?.type === "entity.verify.failed") {
|
|
44
|
+
log.info(
|
|
45
|
+
"Body parser verification has failed. This has been caused by the user submitting a payload containing invalid data [entity.verify.failed]",
|
|
46
|
+
);
|
|
47
|
+
return res
|
|
48
|
+
.status(403)
|
|
49
|
+
.render(TEMPLATE, { errorCode: "invalid_payload", error: err });
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
// Too many parameters submitted
|
|
39
|
-
if (err?.type ===
|
|
40
|
-
log.info(
|
|
41
|
-
|
|
53
|
+
if (err?.type === "parameters.too.many") {
|
|
54
|
+
log.info(
|
|
55
|
+
"The request contains more parameters than is currently allowed [parameters.too.many]",
|
|
56
|
+
);
|
|
57
|
+
return res.status(413).render(TEMPLATE, {
|
|
58
|
+
errorCode: "parameter_limit_exceeded",
|
|
59
|
+
error: err,
|
|
60
|
+
});
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
// Overall payload too large
|
|
45
|
-
if (err?.type ===
|
|
46
|
-
log.info(
|
|
47
|
-
|
|
64
|
+
if (err?.type === "entity.too.large") {
|
|
65
|
+
log.info(
|
|
66
|
+
`The request payload is too large. Received ${err.length}b with a maximum of ${err.limit}b [parameters.too.many]`,
|
|
67
|
+
);
|
|
68
|
+
return res
|
|
69
|
+
.status(413)
|
|
70
|
+
.render(TEMPLATE, { errorCode: "payload_size_exceeded", error: err });
|
|
48
71
|
}
|
|
49
72
|
|
|
50
73
|
// Unaccept request method
|
|
51
|
-
if (err?.code ===
|
|
74
|
+
if (err?.code === "unaccepted_request_method") {
|
|
52
75
|
log.info(err.message);
|
|
53
|
-
return res.status(400).render(TEMPLATE, {
|
|
76
|
+
return res.status(400).render(TEMPLATE, {
|
|
77
|
+
errorCode: "unaccepted_request_method",
|
|
78
|
+
error: err,
|
|
79
|
+
});
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
// Unknown error
|
|
57
83
|
log.error(`Unknown error: ${err.message}; stacktrace: ${err.stack}`);
|
|
58
84
|
return res.status(200).render(TEMPLATE, { error: err });
|
|
59
85
|
},
|
|
60
|
-
]
|
|
86
|
+
];
|
|
61
87
|
}
|