@dwp/govuk-casa 8.0.0-alpha1 → 8.0.0
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/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/dist/assets/css/casa-ie8.css +1 -1
- package/dist/assets/css/casa.css +1 -1
- package/dist/casa.d.ts +2 -1
- package/dist/casa.js +3 -1
- package/dist/lib/CasaTemplateLoader.d.ts +11 -3
- package/dist/lib/CasaTemplateLoader.js +38 -2
- package/dist/lib/JourneyContext.d.ts +51 -8
- package/dist/lib/JourneyContext.js +73 -147
- package/dist/lib/MutableRouter.d.ts +19 -19
- package/dist/lib/MutableRouter.js +30 -23
- package/dist/lib/Plan.d.ts +41 -6
- package/dist/lib/Plan.js +84 -17
- package/dist/lib/ValidationError.d.ts +6 -2
- package/dist/lib/ValidationError.js +7 -0
- package/dist/lib/ValidatorFactory.d.ts +72 -13
- package/dist/lib/ValidatorFactory.js +33 -14
- package/dist/lib/configuration-ingestor.d.ts +262 -0
- package/dist/lib/configuration-ingestor.js +464 -0
- package/dist/lib/configure.d.ts +39 -154
- package/dist/lib/configure.js +35 -59
- package/dist/lib/dirname.cjs +1 -1
- package/dist/lib/dirname.d.cts +2 -0
- package/dist/lib/end-session.d.ts +4 -3
- package/dist/lib/end-session.js +30 -8
- package/dist/lib/field.d.ts +53 -55
- package/dist/lib/field.js +96 -54
- package/dist/lib/index.d.ts +14 -0
- package/dist/lib/index.js +54 -0
- package/dist/lib/logger.d.ts +2 -1
- package/dist/lib/logger.js +3 -4
- package/dist/lib/nunjucks-filters.d.ts +11 -11
- package/dist/lib/nunjucks-filters.js +22 -36
- package/dist/lib/nunjucks.d.ts +7 -6
- package/dist/lib/nunjucks.js +6 -6
- package/dist/lib/utils.d.ts +26 -0
- package/dist/lib/utils.js +70 -1
- package/dist/lib/validators/dateObject.js +1 -1
- package/dist/lib/validators/email.js +1 -1
- package/dist/lib/validators/inArray.js +1 -1
- package/dist/lib/validators/index.js +0 -22
- package/dist/lib/validators/postalAddressObject.js +7 -3
- package/dist/lib/validators/required.js +1 -1
- package/dist/lib/waypoint-url.d.ts +13 -7
- package/dist/lib/waypoint-url.js +13 -7
- package/dist/middleware/body-parser.d.ts +2 -1
- package/dist/middleware/body-parser.js +20 -11
- package/dist/middleware/csrf.d.ts +1 -1
- package/dist/middleware/csrf.js +2 -2
- package/dist/middleware/data.d.ts +1 -2
- package/dist/middleware/data.js +19 -15
- package/dist/middleware/dirname.cjs +1 -1
- package/dist/middleware/dirname.d.cts +2 -0
- package/dist/middleware/gather-fields.d.ts +3 -2
- package/dist/middleware/gather-fields.js +14 -7
- package/dist/middleware/i18n.d.ts +1 -1
- package/dist/middleware/i18n.js +16 -11
- package/dist/middleware/post.d.ts +3 -1
- package/dist/middleware/post.js +24 -9
- package/dist/middleware/pre.js +15 -2
- package/dist/middleware/progress-journey.js +15 -17
- package/dist/middleware/sanitise-fields.js +15 -10
- package/dist/middleware/session.d.ts +2 -1
- package/dist/middleware/session.js +65 -52
- package/dist/middleware/skip-waypoint.js +10 -7
- package/dist/middleware/steer-journey.d.ts +3 -2
- package/dist/middleware/steer-journey.js +26 -8
- package/dist/middleware/validate-fields.js +15 -21
- package/dist/mjs/esm-wrapper.js +18 -7
- package/dist/routes/ancillary.d.ts +8 -1
- package/dist/routes/ancillary.js +7 -1
- package/dist/routes/dirname.cjs +1 -1
- package/dist/routes/dirname.d.cts +2 -0
- package/dist/routes/journey.js +20 -24
- package/dist/routes/static.js +10 -9
- package/package.json +41 -22
- package/views/casa/errors/static.njk +11 -0
- package/views/casa/layouts/main.njk +3 -3
package/dist/routes/journey.js
CHANGED
|
@@ -13,26 +13,14 @@ const validate_fields_js_1 = __importDefault(require("../middleware/validate-fie
|
|
|
13
13
|
const progress_journey_js_1 = __importDefault(require("../middleware/progress-journey.js"));
|
|
14
14
|
const waypoint_url_js_1 = __importDefault(require("../lib/waypoint-url.js"));
|
|
15
15
|
const logger_js_1 = __importDefault(require("../lib/logger.js"));
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const waypointPath = `/${waypoint}`;
|
|
19
|
-
const pathMatch = (h) => h.path === undefined || (h.path instanceof RegExp && h.path.test(waypointPath)) || h.path === waypointPath;
|
|
20
|
-
const matchedPageHooks = (pageHooks || []).filter((h) => `journey.${h.hook}` === hookName).map((h) => h.middleware);
|
|
21
|
-
const matchedGlobalHooks = globalHooks.filter((h) => h.hook === hookName).filter(pathMatch).map((h) => h.middleware);
|
|
22
|
-
return [
|
|
23
|
-
...matchedGlobalHooks,
|
|
24
|
-
...matchedPageHooks,
|
|
25
|
-
];
|
|
26
|
-
};
|
|
16
|
+
const utils_js_1 = require("../lib/utils.js");
|
|
17
|
+
const log = (0, logger_js_1.default)('routes:journey');
|
|
27
18
|
const renderMiddlewareFactory = (view, contextFactory) => [
|
|
28
19
|
(req, res, next) => {
|
|
29
20
|
res.render(view, Object.assign({
|
|
30
21
|
// Common template variables for both GET and POST requests
|
|
31
|
-
inEditMode: req.casa.editMode, editOriginUrl: req.casa.editOrigin,
|
|
32
|
-
// editSearchParams: req.editSearchParams,
|
|
33
|
-
activeContextId: req.casa.journeyContext.identity.id }, contextFactory(req)), (err, templateString) => {
|
|
22
|
+
inEditMode: req.casa.editMode, editOriginUrl: req.casa.editOrigin, activeContextId: req.casa.journeyContext.identity.id }, contextFactory(req)), (err, templateString) => {
|
|
34
23
|
if (err) {
|
|
35
|
-
// logger.error(err);
|
|
36
24
|
next(err);
|
|
37
25
|
}
|
|
38
26
|
else {
|
|
@@ -46,7 +34,6 @@ function journeyRouter({ globalHooks, pages, plan, csrfMiddleware, mountUrl, })
|
|
|
46
34
|
const router = new MutableRouter_js_1.default();
|
|
47
35
|
// Special "_" route which handles redirecting the user between sub-apps
|
|
48
36
|
// /app1/_/?refmount=app2&route=prev
|
|
49
|
-
// - will redirect to the
|
|
50
37
|
router.all('/_', (req, res) => {
|
|
51
38
|
var _a, _b;
|
|
52
39
|
const refmount = (_a = req.query) === null || _a === void 0 ? void 0 : _a.refmount;
|
|
@@ -87,25 +74,32 @@ function journeyRouter({ globalHooks, pages, plan, csrfMiddleware, mountUrl, })
|
|
|
87
74
|
...csrfMiddleware,
|
|
88
75
|
];
|
|
89
76
|
pages.forEach((page) => {
|
|
90
|
-
const { waypoint, view, hooks: pageHooks, fields } = page;
|
|
77
|
+
const { waypoint, view, hooks: pageHooks = [], fields } = page;
|
|
91
78
|
const formUrl = (0, waypoint_url_js_1.default)({ mountUrl, waypoint });
|
|
92
79
|
const waypointPath = `/${waypoint}`;
|
|
93
|
-
|
|
80
|
+
let commonWaypointMiddleware = [
|
|
94
81
|
(req, res, next) => {
|
|
95
82
|
req.casa.waypoint = waypoint;
|
|
83
|
+
res.locals.casa.waypoint = waypoint;
|
|
96
84
|
next();
|
|
97
85
|
},
|
|
98
|
-
...(0, skip_waypoint_js_1.default)({ mountUrl, waypoint }),
|
|
99
86
|
];
|
|
100
|
-
|
|
87
|
+
if (plan.isSkippable(waypoint)) {
|
|
88
|
+
log.info(`Configuring "${waypoint}" as a skippable waypoint`);
|
|
89
|
+
commonWaypointMiddleware = [
|
|
90
|
+
...commonWaypointMiddleware,
|
|
91
|
+
...(0, skip_waypoint_js_1.default)({ mountUrl, waypoint }),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
router.get(waypointPath, ...commonMiddleware, ...commonWaypointMiddleware, ...(0, utils_js_1.resolveMiddlewareHooks)('journey.presteer', waypointPath, [...globalHooks, ...pageHooks]), ...(0, steer_journey_js_1.default)({ waypoint, mountUrl, plan }), ...(0, utils_js_1.resolveMiddlewareHooks)('journey.poststeer', waypointPath, [...globalHooks, ...pageHooks]), ...(0, utils_js_1.resolveMiddlewareHooks)('journey.prerender', waypointPath, [...globalHooks, ...pageHooks]), renderMiddlewareFactory(view, (req) => ({
|
|
101
95
|
formUrl,
|
|
102
96
|
formData: req.casa.journeyContext.getDataForPage(waypoint),
|
|
103
97
|
})));
|
|
104
|
-
router.post(waypointPath, ...commonMiddleware, ...commonWaypointMiddleware, ...
|
|
98
|
+
router.post(waypointPath, ...commonMiddleware, ...commonWaypointMiddleware, ...(0, utils_js_1.resolveMiddlewareHooks)('journey.presteer', waypointPath, [...globalHooks, ...pageHooks]), ...(0, steer_journey_js_1.default)({ waypoint, mountUrl, plan }), ...(0, utils_js_1.resolveMiddlewareHooks)('journey.poststeer', waypointPath, [...globalHooks, ...pageHooks]), ...(0, utils_js_1.resolveMiddlewareHooks)('journey.presanitise', waypointPath, [...globalHooks, ...pageHooks]), ...(0, sanitise_fields_js_1.default)({ waypoint, fields }), ...(0, utils_js_1.resolveMiddlewareHooks)('journey.postsanitise', waypointPath, [...globalHooks, ...pageHooks]), ...(0, utils_js_1.resolveMiddlewareHooks)('journey.pregather', waypointPath, [...globalHooks, ...pageHooks]), ...(0, gather_fields_js_1.default)({ waypoint, fields }), ...(0, utils_js_1.resolveMiddlewareHooks)('journey.postgather', waypointPath, [...globalHooks, ...pageHooks]), ...(0, utils_js_1.resolveMiddlewareHooks)('journey.prevalidate', waypointPath, [...globalHooks, ...pageHooks]), ...(0, validate_fields_js_1.default)({ waypoint, fields, mountUrl, plan }), ...(0, utils_js_1.resolveMiddlewareHooks)('journey.postvalidate', waypointPath, [...globalHooks, ...pageHooks]),
|
|
105
99
|
// If there were validation errors, jump out of this route and into the
|
|
106
100
|
// next, where the errors will be rendered
|
|
107
|
-
(req, res, next) => (req.casa.journeyContext.hasValidationErrorsForPage(waypoint) ? next('route') : next()), ...
|
|
108
|
-
router.post(waypointPath, ...
|
|
101
|
+
(req, res, next) => (req.casa.journeyContext.hasValidationErrorsForPage(waypoint) ? next('route') : next()), ...(0, utils_js_1.resolveMiddlewareHooks)('journey.preredirect', waypointPath, [...globalHooks, ...pageHooks]), ...(0, progress_journey_js_1.default)({ waypoint, plan, mountUrl }));
|
|
102
|
+
router.post(waypointPath, ...(0, utils_js_1.resolveMiddlewareHooks)('journey.prerender', waypointPath, [...globalHooks, ...pageHooks]), renderMiddlewareFactory(view, (req) => {
|
|
109
103
|
var _a;
|
|
110
104
|
const errors = (_a = req.casa.journeyContext.getValidationErrorsForPageByField(waypoint)) !== null && _a !== void 0 ? _a : Object.create(null);
|
|
111
105
|
// This is a convenience for the template. The `govukErrorSummary` macro
|
|
@@ -113,9 +107,11 @@ function journeyRouter({ globalHooks, pages, plan, csrfMiddleware, mountUrl, })
|
|
|
113
107
|
// errors in that format.
|
|
114
108
|
// Where there are multiple errors against a particular field, only the
|
|
115
109
|
// first one is shown.
|
|
110
|
+
// Disabling security/detect-object-injection rule because both `errors`
|
|
111
|
+
// and the `k` property are known entities
|
|
116
112
|
const govukErrors = Object.keys(errors).map((k) => ({
|
|
117
113
|
text: req.t(errors[k][0].summary, errors[k][0].variables),
|
|
118
|
-
href: errors[k][0].fieldHref,
|
|
114
|
+
href: errors[k][0].fieldHref, /* eslint-disable-line security/detect-object-injection */
|
|
119
115
|
}));
|
|
120
116
|
return {
|
|
121
117
|
formUrl,
|
package/dist/routes/static.js
CHANGED
|
@@ -3,13 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const express_1 = require("express");
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
7
|
const fs_1 = require("fs");
|
|
8
8
|
const url_1 = require("url");
|
|
9
9
|
const path_1 = require("path");
|
|
10
10
|
const module_1 = require("module");
|
|
11
11
|
const dirname_cjs_1 = __importDefault(require("./dirname.cjs"));
|
|
12
12
|
const MutableRouter_js_1 = __importDefault(require("../lib/MutableRouter.js"));
|
|
13
|
+
const { static: ExpressStatic } = express_1.default; // CommonJS
|
|
13
14
|
const oneDay = 86400000;
|
|
14
15
|
/**
|
|
15
16
|
* @typedef {object} StaticOptions Options to configure static router
|
|
@@ -23,7 +24,7 @@ const oneDay = 86400000;
|
|
|
23
24
|
* @returns {MutableRouter} ExpressJS Router instance
|
|
24
25
|
*/
|
|
25
26
|
function staticRouter({ mountUrl = '/', maxAge = 3600000, }) {
|
|
26
|
-
const
|
|
27
|
+
const router = new MutableRouter_js_1.default();
|
|
27
28
|
const notFoundHandler = (req, res, next) => {
|
|
28
29
|
// Fall through to a general purpose error handler
|
|
29
30
|
next(new Error('404'));
|
|
@@ -56,12 +57,12 @@ function staticRouter({ mountUrl = '/', maxAge = 3600000, }) {
|
|
|
56
57
|
// The static middleware will only server GET/HEAD requests, so we can mount
|
|
57
58
|
// the middleware using `use()` rather than resorting to `get()`
|
|
58
59
|
const govukFrontendDirectory = (0, path_1.resolve)((0, module_1.createRequire)(dirname_cjs_1.default).resolve('govuk-frontend'), '../../');
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
60
|
+
router.use('/govuk/assets/js/all.js', ExpressStatic(`${govukFrontendDirectory}/govuk/all.js`, staticConfig));
|
|
61
|
+
router.use('/govuk/assets', ExpressStatic(`${govukFrontendDirectory}/govuk/assets`, staticConfig));
|
|
62
|
+
router.use('/govuk/assets', notFoundHandler);
|
|
63
|
+
router.use('/casa/assets/css/casa.css', setHeaders, (req, res) => res.send(casaCss));
|
|
64
|
+
router.use('/casa/assets/css/casa-ie8.css', setHeaders, (req, res) => res.send(casaCssIe8));
|
|
65
|
+
router.use('/casa/assets', notFoundHandler);
|
|
66
|
+
return router;
|
|
66
67
|
}
|
|
67
68
|
exports.default = staticRouter;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dwp/govuk-casa",
|
|
3
|
-
"version": "8.0.0
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "A framework for building GOVUK Collect-And-Submit-Applications",
|
|
5
5
|
"main": "dist/casa.js",
|
|
6
6
|
"module": "dist/mjs/casa.js",
|
|
@@ -14,56 +14,75 @@
|
|
|
14
14
|
"locales/**/*",
|
|
15
15
|
"views/**/*"
|
|
16
16
|
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=14.17.2"
|
|
19
|
+
},
|
|
17
20
|
"scripts": {
|
|
18
|
-
"
|
|
21
|
+
"pipeline": "npm run coverage && npm run lint",
|
|
22
|
+
"test:unit": "mocha './tests/**/*.test.js'",
|
|
19
23
|
"test:e2e": "spiderplan --worker-init ./tests/e2e/worker-init.js --language en ./tests/e2e/personas/**/*.yaml",
|
|
24
|
+
"test": "npm run test:unit && npm run test:e2e",
|
|
25
|
+
"lint": "eslint .",
|
|
26
|
+
"coverage": "c8 npm test",
|
|
20
27
|
"build": "npm run build:prepare && npm run build:sources && npm run build:css-assets",
|
|
21
28
|
"build:prepare": "rm -rf dist/* && mkdir -p dist/assets/js/ && mkdir -p dist/assets/css/",
|
|
22
29
|
"build:sources": "tsc -p tsconfig-cjs.json && ./scripts/fixup.sh",
|
|
23
30
|
"build:css-assets": "node scripts/compile-sass.js",
|
|
24
|
-
"prepare": "npm run build"
|
|
31
|
+
"prepare": "npm run build",
|
|
32
|
+
"upgrade-deps": "OD=$(npm outdated --long --parseable); echo \"$OD\" | grep ':devDependencies:' | awk -F: '{ print $4 }' | xargs npm i -DE; echo \"$OD\" | grep ':dependencies:' | awk -F: '{ print $4 }' | xargs npm i -E",
|
|
33
|
+
"standard-version": "node scripts/standard-version.js"
|
|
25
34
|
},
|
|
26
35
|
"keywords": [],
|
|
27
36
|
"author": "DWP Digital",
|
|
28
37
|
"license": "ISC",
|
|
29
38
|
"type": "module",
|
|
30
39
|
"dependencies": {
|
|
31
|
-
"cookie-parser": "1.4.
|
|
40
|
+
"cookie-parser": "1.4.6",
|
|
32
41
|
"csurf": "1.11.0",
|
|
33
|
-
"debug": "4.3.
|
|
42
|
+
"debug": "4.3.3",
|
|
34
43
|
"deepmerge": "4.2.2",
|
|
35
|
-
"express": "4.17.
|
|
44
|
+
"express": "4.17.2",
|
|
36
45
|
"express-session": "1.17.2",
|
|
37
|
-
"govuk-frontend": "
|
|
46
|
+
"govuk-frontend": "4.0.0",
|
|
38
47
|
"graphlib": "2.1.8",
|
|
39
48
|
"helmet": "4.6.0",
|
|
40
|
-
"i18next": "21.
|
|
49
|
+
"i18next": "21.6.3",
|
|
41
50
|
"i18next-http-middleware": "3.1.4",
|
|
42
51
|
"js-yaml": "4.1.0",
|
|
43
52
|
"lodash": "4.17.21",
|
|
44
|
-
"luxon": "2.0
|
|
53
|
+
"luxon": "2.2.0",
|
|
45
54
|
"nunjucks": "3.2.3",
|
|
46
55
|
"uuid": "8.3.2",
|
|
47
|
-
"validator": "^13.
|
|
56
|
+
"validator": "^13.7.0"
|
|
48
57
|
},
|
|
49
58
|
"devDependencies": {
|
|
50
|
-
"@babel/core": "7.
|
|
51
|
-
"@babel/eslint-parser": "7.
|
|
52
|
-
"@babel/preset-env": "7.
|
|
53
|
-
"@
|
|
54
|
-
"@dwp/
|
|
59
|
+
"@babel/core": "7.16.5",
|
|
60
|
+
"@babel/eslint-parser": "7.16.5",
|
|
61
|
+
"@babel/preset-env": "7.16.5",
|
|
62
|
+
"@commitlint/config-conventional": "15.0.0",
|
|
63
|
+
"@dwp/casa-spiderplan": "2.0.0",
|
|
64
|
+
"@dwp/casa-spiderplan-a11y-plugin": "0.1.3",
|
|
65
|
+
"@dwp/casa-spiderplan-zap-plugin": "0.1.1",
|
|
55
66
|
"@dwp/eslint-config-base": "5.0.1",
|
|
56
67
|
"@types/express": "4.17.13",
|
|
57
|
-
"@types/node": "
|
|
68
|
+
"@types/node": "17.0.1",
|
|
69
|
+
"@types/nunjucks": "3.2.0",
|
|
58
70
|
"babel-eslint": "10.1.0",
|
|
71
|
+
"c8": "7.10.0",
|
|
59
72
|
"chai": "4.3.4",
|
|
60
|
-
"commitlint": "
|
|
73
|
+
"commitlint": "15.0.0",
|
|
61
74
|
"eslint": "7.32.0",
|
|
62
75
|
"eslint-plugin-no-unsafe-regex": "1.0.0",
|
|
63
|
-
"eslint-plugin-
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
76
|
+
"eslint-plugin-security": "1.4.0",
|
|
77
|
+
"eslint-plugin-sonarjs": "0.11.0",
|
|
78
|
+
"fast-check": "2.20.0",
|
|
79
|
+
"husky": "7.0.4",
|
|
80
|
+
"mocha": "9.1.3",
|
|
81
|
+
"sass": "1.45.0",
|
|
82
|
+
"sinon": "12.0.1",
|
|
83
|
+
"sinon-chai": "3.7.0",
|
|
84
|
+
"standard-version": "9.3.2",
|
|
85
|
+
"supertest": "6.1.6",
|
|
86
|
+
"typescript": "4.5.4"
|
|
68
87
|
}
|
|
69
88
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{% extends "casa/layouts/main.njk" %}
|
|
2
|
+
|
|
3
|
+
{% block pageTitle %}
|
|
4
|
+
Sorry, there is a problem with the service
|
|
5
|
+
{% endblock %}
|
|
6
|
+
|
|
7
|
+
{% block content %}
|
|
8
|
+
<h1 class="govuk-heading-xl">Sorry, there is a problem with the service</h1>
|
|
9
|
+
|
|
10
|
+
<p class="govuk-body">Try again in a few moments.</p>
|
|
11
|
+
{% endblock %}
|
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
{# Harcoding content for the skip link as t() may not be availble on some error pages #}
|
|
11
11
|
{{ govukSkipLink({
|
|
12
12
|
href: '#main-content',
|
|
13
|
-
text:
|
|
13
|
+
text: "Neidio i'r prif gynnwys" if locale === 'cy' else 'Skip to main content'
|
|
14
14
|
}) }}
|
|
15
15
|
{% endblock %}
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
{% block header %}
|
|
19
19
|
{{ govukHeader({
|
|
20
|
-
assetsPath:
|
|
21
|
-
serviceName: t(
|
|
20
|
+
assetsPath: assetPath + "/images",
|
|
21
|
+
serviceName: t('common:serviceName') if t else '',
|
|
22
22
|
serviceUrl: casa.mountUrl,
|
|
23
23
|
homepageUrl: "https://www.gov.uk/"
|
|
24
24
|
}) }}
|