@dwp/govuk-casa 8.7.12 → 8.9.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/dist/casa.d.ts +12 -1
- package/dist/casa.js +10 -2
- package/dist/casa.js.map +1 -0
- package/dist/lib/CasaTemplateLoader.js +1 -0
- package/dist/lib/CasaTemplateLoader.js.map +1 -0
- package/dist/lib/JourneyContext.d.ts +12 -3
- package/dist/lib/JourneyContext.js +20 -5
- package/dist/lib/JourneyContext.js.map +1 -0
- package/dist/lib/MutableRouter.js +1 -0
- package/dist/lib/MutableRouter.js.map +1 -0
- package/dist/lib/Plan.d.ts +1 -1
- package/dist/lib/Plan.js +2 -5
- package/dist/lib/Plan.js.map +1 -0
- package/dist/lib/ValidationError.js +1 -0
- package/dist/lib/ValidationError.js.map +1 -0
- package/dist/lib/ValidatorFactory.d.ts +2 -2
- package/dist/lib/ValidatorFactory.js +3 -2
- package/dist/lib/ValidatorFactory.js.map +1 -0
- package/dist/lib/configuration-ingestor.js +1 -0
- package/dist/lib/configuration-ingestor.js.map +1 -0
- package/dist/lib/configure.js +2 -1
- package/dist/lib/configure.js.map +1 -0
- package/dist/lib/constants.d.ts +9 -0
- package/dist/lib/constants.js +13 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/end-session.js +1 -0
- package/dist/lib/end-session.js.map +1 -0
- package/dist/lib/field.js +1 -0
- package/dist/lib/field.js.map +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logger.js +1 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/mount.js +3 -2
- package/dist/lib/mount.js.map +1 -0
- package/dist/lib/nunjucks-filters.js +1 -0
- package/dist/lib/nunjucks-filters.js.map +1 -0
- package/dist/lib/nunjucks.js +1 -0
- package/dist/lib/nunjucks.js.map +1 -0
- package/dist/lib/utils.d.ts +45 -27
- package/dist/lib/utils.js +105 -67
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/validators/dateObject.js +4 -3
- package/dist/lib/validators/dateObject.js.map +1 -0
- package/dist/lib/validators/email.js +1 -0
- package/dist/lib/validators/email.js.map +1 -0
- package/dist/lib/validators/inArray.js +1 -0
- package/dist/lib/validators/inArray.js.map +1 -0
- package/dist/lib/validators/index.js +1 -0
- package/dist/lib/validators/index.js.map +1 -0
- package/dist/lib/validators/nino.js +1 -0
- package/dist/lib/validators/nino.js.map +1 -0
- package/dist/lib/validators/postalAddressObject.d.ts +2 -2
- package/dist/lib/validators/postalAddressObject.js +2 -1
- package/dist/lib/validators/postalAddressObject.js.map +1 -0
- package/dist/lib/validators/regex.js +1 -0
- package/dist/lib/validators/regex.js.map +1 -0
- package/dist/lib/validators/required.js +1 -0
- package/dist/lib/validators/required.js.map +1 -0
- package/dist/lib/validators/strlen.js +1 -0
- package/dist/lib/validators/strlen.js.map +1 -0
- package/dist/lib/validators/wordCount.js +1 -0
- package/dist/lib/validators/wordCount.js.map +1 -0
- package/dist/lib/waypoint-url.js +40 -9
- package/dist/lib/waypoint-url.js.map +1 -0
- package/dist/middleware/body-parser.js +1 -0
- package/dist/middleware/body-parser.js.map +1 -0
- package/dist/middleware/csrf.js +1 -0
- package/dist/middleware/csrf.js.map +1 -0
- package/dist/middleware/data.js +1 -0
- package/dist/middleware/data.js.map +1 -0
- package/dist/middleware/gather-fields.js +10 -2
- package/dist/middleware/gather-fields.js.map +1 -0
- package/dist/middleware/i18n.js +1 -0
- package/dist/middleware/i18n.js.map +1 -0
- package/dist/middleware/post.js +1 -0
- package/dist/middleware/post.js.map +1 -0
- package/dist/middleware/pre.js +1 -0
- package/dist/middleware/pre.js.map +1 -0
- package/dist/middleware/progress-journey.js +7 -2
- package/dist/middleware/progress-journey.js.map +1 -0
- package/dist/middleware/sanitise-fields.js +1 -0
- package/dist/middleware/sanitise-fields.js.map +1 -0
- package/dist/middleware/serve-first-waypoint.js +1 -0
- package/dist/middleware/serve-first-waypoint.js.map +1 -0
- package/dist/middleware/session.js +1 -0
- package/dist/middleware/session.js.map +1 -0
- package/dist/middleware/skip-waypoint.js +1 -0
- package/dist/middleware/skip-waypoint.js.map +1 -0
- package/dist/middleware/steer-journey.js +2 -0
- package/dist/middleware/steer-journey.js.map +1 -0
- package/dist/middleware/strip-proxy-path.js +1 -0
- package/dist/middleware/strip-proxy-path.js.map +1 -0
- package/dist/middleware/validate-fields.js +7 -1
- package/dist/middleware/validate-fields.js.map +1 -0
- package/dist/mjs/esm-wrapper.js +11 -15
- package/dist/routes/ancillary.js +1 -0
- package/dist/routes/ancillary.js.map +1 -0
- package/dist/routes/journey.js +1 -0
- package/dist/routes/journey.js.map +1 -0
- package/dist/routes/static.js +1 -0
- package/dist/routes/static.js.map +1 -0
- package/locales/cy/error.json +1 -1
- package/locales/en/error.json +1 -1
- package/package.json +17 -16
- package/src/casa.js +330 -0
- package/src/lib/CasaTemplateLoader.js +104 -0
- package/src/lib/JourneyContext.js +797 -0
- package/src/lib/MutableRouter.js +310 -0
- package/src/lib/Plan.js +619 -0
- package/src/lib/ValidationError.js +163 -0
- package/src/lib/ValidatorFactory.js +105 -0
- package/src/lib/configuration-ingestor.js +457 -0
- package/src/lib/configure.js +202 -0
- package/src/lib/constants.js +9 -0
- package/src/lib/dirname.cjs +1 -0
- package/src/lib/end-session.js +45 -0
- package/src/lib/field.js +456 -0
- package/src/lib/index.js +33 -0
- package/src/lib/logger.js +16 -0
- package/src/lib/mount.js +127 -0
- package/src/lib/nunjucks-filters.js +150 -0
- package/src/lib/nunjucks.js +53 -0
- package/src/lib/utils.js +232 -0
- package/src/lib/validators/dateObject.js +169 -0
- package/src/lib/validators/email.js +55 -0
- package/src/lib/validators/inArray.js +81 -0
- package/src/lib/validators/index.js +24 -0
- package/src/lib/validators/nino.js +57 -0
- package/src/lib/validators/postalAddressObject.js +162 -0
- package/src/lib/validators/regex.js +48 -0
- package/src/lib/validators/required.js +74 -0
- package/src/lib/validators/strlen.js +66 -0
- package/src/lib/validators/wordCount.js +70 -0
- package/src/lib/waypoint-url.js +126 -0
- package/src/middleware/body-parser.js +31 -0
- package/src/middleware/csrf.js +29 -0
- package/src/middleware/data.js +105 -0
- package/src/middleware/dirname.cjs +1 -0
- package/src/middleware/gather-fields.js +58 -0
- package/src/middleware/i18n.js +106 -0
- package/src/middleware/post.js +61 -0
- package/src/middleware/pre.js +91 -0
- package/src/middleware/progress-journey.js +96 -0
- package/src/middleware/sanitise-fields.js +58 -0
- package/src/middleware/serve-first-waypoint.js +28 -0
- package/src/middleware/session.js +129 -0
- package/src/middleware/skip-waypoint.js +46 -0
- package/src/middleware/steer-journey.js +79 -0
- package/src/middleware/strip-proxy-path.js +56 -0
- package/src/middleware/validate-fields.js +89 -0
- package/src/routes/ancillary.js +29 -0
- package/src/routes/dirname.cjs +1 -0
- package/src/routes/journey.js +212 -0
- package/src/routes/static.js +77 -0
- package/views/casa/components/character-count/README.md +10 -0
- package/views/casa/components/character-count/template.njk +6 -2
- package/views/casa/components/checkboxes/README.md +43 -34
- package/views/casa/components/checkboxes/template.njk +8 -7
- package/views/casa/components/date-input/README.md +11 -1
- package/views/casa/components/date-input/template.njk +6 -4
- package/views/casa/components/input/README.md +9 -0
- package/views/casa/components/input/template.njk +6 -2
- package/views/casa/components/postal-address-object/README.md +10 -0
- package/views/casa/components/postal-address-object/template.njk +20 -5
- package/views/casa/components/radios/README.md +49 -24
- package/views/casa/components/radios/template.njk +6 -3
- package/views/casa/components/select/README.md +65 -0
- package/views/casa/components/select/macro.njk +3 -0
- package/views/casa/components/select/template.njk +49 -0
- package/views/casa/components/textarea/README.md +9 -0
- package/views/casa/components/textarea/template.njk +6 -2
- package/views/casa/layouts/journey.njk +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import merge from 'deepmerge';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
import nunjucks from 'nunjucks';
|
|
4
|
+
|
|
5
|
+
const { all: deepmergeAll } = merge;
|
|
6
|
+
|
|
7
|
+
// Arrays will be merged such that elements at the same index will be merged
|
|
8
|
+
// into each other
|
|
9
|
+
// ref: https://www.npmjs.com/package/deepmerge
|
|
10
|
+
|
|
11
|
+
const combineMerge = (target, source, options) => {
|
|
12
|
+
const destination = target.slice()
|
|
13
|
+
|
|
14
|
+
source.forEach((item, index) => {
|
|
15
|
+
// ESLint disabled as `index` is only an integer
|
|
16
|
+
/* eslint-disable security/detect-object-injection */
|
|
17
|
+
if (typeof destination[index] === 'undefined') {
|
|
18
|
+
destination[index] = options.cloneUnlessOtherwiseSpecified(item, options)
|
|
19
|
+
} else if (options.isMergeableObject(item)) {
|
|
20
|
+
destination[index] = merge(target[index], item, options)
|
|
21
|
+
} else if (target.indexOf(item) === -1) {
|
|
22
|
+
destination.push(item)
|
|
23
|
+
}
|
|
24
|
+
/* eslint-enable security/detect-object-injection */
|
|
25
|
+
})
|
|
26
|
+
return destination
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Allows objects to be deepmerged and retain their type, without becoming [object Object]
|
|
30
|
+
// ref: https://github.com/jonschlinkert/is-plain-object/blob/master/is-plain-object.js
|
|
31
|
+
|
|
32
|
+
function isObject(o) {
|
|
33
|
+
return Object.prototype.toString.call(o) === '[object Object]';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isPlainObjectOrArray(o) {
|
|
37
|
+
if (Array.isArray(o)) {
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
if (isObject(o) === false) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
const ctor = o.constructor;
|
|
44
|
+
if (ctor === undefined) {
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
const prot = ctor.prototype;
|
|
48
|
+
if (isObject(prot) === false) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
52
|
+
if (prot.hasOwnProperty('isPrototypeOf') === false) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mergeObjects(...objects) {
|
|
59
|
+
return deepmergeAll([Object.create(null), ...objects], { arrayMerge: combineMerge, isMergeableObject: isPlainObjectOrArray });
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Determine whether a value exists in a list.
|
|
63
|
+
*
|
|
64
|
+
* @memberof NunjucksFilters
|
|
65
|
+
* @param {any[]} source List of items to search
|
|
66
|
+
* @param {any} search Item to search within the `source`
|
|
67
|
+
* @returns {boolean} True if the search item was found
|
|
68
|
+
*/
|
|
69
|
+
function includes(source = [], search = '') {
|
|
70
|
+
return source.includes(search);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Format a given date.
|
|
75
|
+
*
|
|
76
|
+
* Requires NodeJS >= 14 to make use of bundled date locale data.
|
|
77
|
+
*
|
|
78
|
+
* `date` may be any of the following types:
|
|
79
|
+
* object - {dd:'', mm:'', yyyy:''}
|
|
80
|
+
*
|
|
81
|
+
* @memberof NunjucksFilters
|
|
82
|
+
* @param {object} date Date
|
|
83
|
+
* @param {string} date.dd Day
|
|
84
|
+
* @param {string} date.mm Month
|
|
85
|
+
* @param {string} date.yyyy Year
|
|
86
|
+
* @param {object} [config] Options
|
|
87
|
+
* @param {string} [config.locale] Locale (default 'en')
|
|
88
|
+
* @param {string} [config.format] Format (default 'd MMMM yyyy')
|
|
89
|
+
* @returns {string} Formatted date
|
|
90
|
+
*/
|
|
91
|
+
function formatDateObject(date, config = {}) {
|
|
92
|
+
const { locale = 'en', format = 'd MMMM yyyy' } = config;
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
Object.prototype.toString.call(date) === '[object Object]'
|
|
96
|
+
&& 'yyyy' in date
|
|
97
|
+
&& 'mm' in date
|
|
98
|
+
&& 'dd' in date
|
|
99
|
+
) {
|
|
100
|
+
return DateTime.fromObject({
|
|
101
|
+
year: Math.max(0, parseInt(date.yyyy, 10)),
|
|
102
|
+
month: Math.max(0, parseInt(date.mm, 10)),
|
|
103
|
+
day: Math.max(1, parseInt(date.dd, 10)),
|
|
104
|
+
}).setLocale(locale).toFormat(format);
|
|
105
|
+
}
|
|
106
|
+
return 'INVALID DATE OBJECT';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Attribute values will be HTML/attribute escaped.
|
|
111
|
+
*
|
|
112
|
+
* Example:
|
|
113
|
+
* Given: {class: 'basic', 'data-ga': 3}
|
|
114
|
+
* Output: class="basic" data-ga="3"
|
|
115
|
+
*
|
|
116
|
+
* @memberof NunjucksFilters
|
|
117
|
+
* @param {object} attrsObject Attributes object (in name:value pairs)
|
|
118
|
+
* @returns {string} Formatted
|
|
119
|
+
*/
|
|
120
|
+
function renderAsAttributes(attrsObject) {
|
|
121
|
+
const attrsList = [];
|
|
122
|
+
if (typeof attrsObject === 'object') {
|
|
123
|
+
Object.keys(attrsObject).forEach((key) => {
|
|
124
|
+
// ESLint disable as `attrsObject` is dev-controlled, `Object.keys()` has
|
|
125
|
+
// been used (to get "own" properties) and `m` is one of the characters
|
|
126
|
+
// found by the regex.
|
|
127
|
+
/* eslint-disable security/detect-object-injection */
|
|
128
|
+
const value = String(attrsObject[key]).replace(/[<>"'&]/g, (m) => ({
|
|
129
|
+
'<': '<',
|
|
130
|
+
'>': '>',
|
|
131
|
+
'"': '"',
|
|
132
|
+
'\'': ''',
|
|
133
|
+
'&': '&',
|
|
134
|
+
}[m]));
|
|
135
|
+
/* eslint-enable security/detect-object-injection */
|
|
136
|
+
attrsList.push(`${key}="${value}"`);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return new nunjucks.runtime.SafeString(attrsList.join(' '));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @namespace NunjucksFilters
|
|
144
|
+
*/
|
|
145
|
+
export {
|
|
146
|
+
mergeObjects,
|
|
147
|
+
includes,
|
|
148
|
+
formatDateObject,
|
|
149
|
+
renderAsAttributes,
|
|
150
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { Environment } from 'nunjucks';
|
|
4
|
+
import dirname from './dirname.cjs';
|
|
5
|
+
import CasaTemplateLoader from './CasaTemplateLoader.js';
|
|
6
|
+
import {
|
|
7
|
+
mergeObjects, includes, renderAsAttributes, formatDateObject,
|
|
8
|
+
} from './nunjucks-filters.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} NunjucksOptions
|
|
12
|
+
* @property {string[]} [views=[]] Template file directories (optional, default [])
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a Nunjucks environment.
|
|
17
|
+
*
|
|
18
|
+
* @access private
|
|
19
|
+
* @param {NunjucksOptions} options Nunjucks options
|
|
20
|
+
* @returns {Environment} Nunjucks Environment instance
|
|
21
|
+
*/
|
|
22
|
+
export default function nunjucksConfig({
|
|
23
|
+
views = [],
|
|
24
|
+
}) {
|
|
25
|
+
// Prepare a single Nunjucks environment for all responses to use. Note that
|
|
26
|
+
// we cannot prepare response-specific global functions/filters if we use a
|
|
27
|
+
// single environment, but the performance gains of doing so are significant.
|
|
28
|
+
const loader = new CasaTemplateLoader(views, {
|
|
29
|
+
watch: false,
|
|
30
|
+
noCache: false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const env = new Environment(loader, {
|
|
34
|
+
autoescape: true,
|
|
35
|
+
throwOnUndefined: false,
|
|
36
|
+
trimBlocks: false,
|
|
37
|
+
lstripBlocks: false,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Enhancement to expose loader functions
|
|
41
|
+
env.modifyBlock = loader.modifyBlock.bind(loader);
|
|
42
|
+
|
|
43
|
+
// Globals
|
|
44
|
+
// These can't be modified once set. But they can be overridden by res.locals.
|
|
45
|
+
env.addGlobal('casaVersion', JSON.parse(readFileSync(resolve(dirname, '../../package.json'))).version);
|
|
46
|
+
|
|
47
|
+
env.addGlobal('mergeObjects', mergeObjects);
|
|
48
|
+
env.addGlobal('includes', includes);
|
|
49
|
+
env.addGlobal('formatDateObject', formatDateObject);
|
|
50
|
+
env.addGlobal('renderAsAttributes', renderAsAttributes);
|
|
51
|
+
|
|
52
|
+
return env;
|
|
53
|
+
}
|
package/src/lib/utils.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @access private
|
|
3
|
+
* @typedef {import('../casa').GlobalHook | import('../casa').PageHook} Hook
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Determine if value is empty. Recurse over objects.
|
|
8
|
+
*
|
|
9
|
+
* @access private
|
|
10
|
+
* @param {any} val Value to check
|
|
11
|
+
* @returns {boolean} True if the object is empty
|
|
12
|
+
*/
|
|
13
|
+
export function isEmpty(val) {
|
|
14
|
+
if (
|
|
15
|
+
val === null
|
|
16
|
+
|| typeof val === 'undefined'
|
|
17
|
+
|| (typeof val === 'string' && val === '')
|
|
18
|
+
) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(val) || typeof val === 'object') {
|
|
22
|
+
// ESLint disabled as `k` is an "own property" (thanks to `Object.keys()`)
|
|
23
|
+
/* eslint-disable-next-line security/detect-object-injection */
|
|
24
|
+
return Object.keys(val).filter((k) => !isEmpty(val[k])).length === 0;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Test is a value can be stringified (numbers or strings)
|
|
31
|
+
*
|
|
32
|
+
* @access private
|
|
33
|
+
* @param {any} value Item to test
|
|
34
|
+
* @returns {boolean} Whether the value is stringable or not
|
|
35
|
+
*/
|
|
36
|
+
export function isStringable(value) {
|
|
37
|
+
return typeof value === 'string' || typeof value === 'number';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract the middleware functions that are relevant for the given hook and
|
|
42
|
+
* path.
|
|
43
|
+
*
|
|
44
|
+
* @access private
|
|
45
|
+
* @param {string} hookName Hook name (including scope prefix)
|
|
46
|
+
* @param {string} path URL path to match (relative to mountUrl)
|
|
47
|
+
* @param {Hook[]} hooks Hooks to be applied at the page level
|
|
48
|
+
* @returns {Function[]} An array of middleware that should be applied
|
|
49
|
+
*/
|
|
50
|
+
export function resolveMiddlewareHooks(hookName, path, hooks = []) {
|
|
51
|
+
/* eslint-disable-next-line max-len */
|
|
52
|
+
const pathMatch = (h) => h.path === undefined || (h.path instanceof RegExp && h.path.test(path)) || h.path === path;
|
|
53
|
+
return hooks.filter((h) => h.hook === hookName).filter(pathMatch).map((h) => h.middleware);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Coerce an input to a string.
|
|
58
|
+
*
|
|
59
|
+
* @access private
|
|
60
|
+
* @param {any} input Input to be stringified
|
|
61
|
+
* @param {string} fallback Fallback to use if input can't be stringified
|
|
62
|
+
* @returns {string} The stringified input
|
|
63
|
+
*/
|
|
64
|
+
export function stringifyInput(input, fallback) {
|
|
65
|
+
// Not using param defaults here as the fallback may be explicitly "undefined"
|
|
66
|
+
const fb = arguments.length === 2 && (isStringable(fallback) || fallback === undefined) ? fallback : '';
|
|
67
|
+
return isStringable(input) ? String(input) : fb;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Strip whitespace from a string.
|
|
72
|
+
*
|
|
73
|
+
* @access private
|
|
74
|
+
* @param {string} value value to be stripped of whitespace
|
|
75
|
+
* @param {object} options overrides for the default whitespace replacements
|
|
76
|
+
* @returns {string} value stripped of white space
|
|
77
|
+
* @throws {TypeError}
|
|
78
|
+
*/
|
|
79
|
+
export function stripWhitespace(value, options) {
|
|
80
|
+
const opts = {
|
|
81
|
+
leading: '',
|
|
82
|
+
trailing: '',
|
|
83
|
+
nested: ' ',
|
|
84
|
+
...options,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (typeof value !== 'string') {
|
|
88
|
+
throw new TypeError('value must be a string');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof opts.leading !== 'string') {
|
|
92
|
+
throw new TypeError('leading must be a string');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (typeof opts.trailing !== 'string') {
|
|
96
|
+
throw new TypeError('trailing must be a string');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (typeof opts.nested !== 'string') {
|
|
100
|
+
throw new TypeError('nested must be a string');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return value
|
|
104
|
+
.replace(/^\s+/, opts.leading)
|
|
105
|
+
.replace(/\s+$/, opts.trailing)
|
|
106
|
+
.replace(/\s+/g, opts.nested);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ------------------------------------------------ validation / sanitisation */
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Checks if the given string can be used as an object key.
|
|
113
|
+
*
|
|
114
|
+
* @access private
|
|
115
|
+
* @param {string} key Proposed Object key
|
|
116
|
+
* @returns {string} Same key if it's valid
|
|
117
|
+
* @throws {Error} if proposed key is an invalid keyword
|
|
118
|
+
*/
|
|
119
|
+
export function notProto(key) {
|
|
120
|
+
if (['__proto__', 'constructor', 'prototype'].includes(String(key).toLowerCase())) {
|
|
121
|
+
throw new Error('Attempt to use prototype key disallowed');
|
|
122
|
+
}
|
|
123
|
+
return key;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validate a hook name.
|
|
128
|
+
*
|
|
129
|
+
* @access private
|
|
130
|
+
* @param {string} hookName Hook name
|
|
131
|
+
* @returns {void}
|
|
132
|
+
* @throws {TypeError}
|
|
133
|
+
* @throws {SyntaxError}
|
|
134
|
+
*/
|
|
135
|
+
export function validateHookName(hookName) {
|
|
136
|
+
if (typeof hookName !== 'string') {
|
|
137
|
+
throw new TypeError('Hook name must be a string');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!hookName.length) {
|
|
141
|
+
throw new SyntaxError('Hook name must not be empty');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!hookName.match(/^([a-z_]+\.|)[a-z_]+$/i)) {
|
|
145
|
+
throw new SyntaxError('Hook name must match either <scope>.<hookname> or <hookname> formats');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validate a hook path.
|
|
151
|
+
*
|
|
152
|
+
* @access private
|
|
153
|
+
* @param {string} path URL path
|
|
154
|
+
* @returns {void}
|
|
155
|
+
* @throws {TypeError}
|
|
156
|
+
*/
|
|
157
|
+
export function validateHookPath(path) {
|
|
158
|
+
if (typeof path !== 'string' && !(path instanceof RegExp)) {
|
|
159
|
+
throw new TypeError('Hook path must be a string or RegExp');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Validate a URL path.
|
|
165
|
+
*
|
|
166
|
+
* @access private
|
|
167
|
+
* @param {string} path URL path
|
|
168
|
+
* @returns {string} Same string, if valid
|
|
169
|
+
* @throws {TypeError}
|
|
170
|
+
* @throws {SyntaxError}
|
|
171
|
+
*/
|
|
172
|
+
export function validateUrlPath(path) {
|
|
173
|
+
if (typeof path !== 'string') {
|
|
174
|
+
throw new TypeError('URL path must be a string');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (path.match(/[^/a-z0-9_-]/)) {
|
|
178
|
+
throw new SyntaxError('URL path must contain only a-z, 0-9, -, _ and / characters');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (path.match(/\/{2,}/)) {
|
|
182
|
+
throw new SyntaxError('URL path must not contain consecutive /');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return path;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Validate a template name.
|
|
190
|
+
*
|
|
191
|
+
* @access private
|
|
192
|
+
* @param {string} view Template name
|
|
193
|
+
* @returns {void}
|
|
194
|
+
* @throws {TypeError}
|
|
195
|
+
* @throws {SyntaxError}
|
|
196
|
+
*/
|
|
197
|
+
export function validateView(view) {
|
|
198
|
+
if (typeof view !== 'string') {
|
|
199
|
+
throw new TypeError('View must be a string');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!view.length) {
|
|
203
|
+
throw new SyntaxError('View must not be empty');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!view.match(/^[a-z0-9/_-]+\.njk$/i)) {
|
|
207
|
+
throw new SyntaxError('View must contain only a-z, 0-9, -, _ and / characters, and end in .njk');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validate a waypoint.
|
|
213
|
+
*
|
|
214
|
+
* @access private
|
|
215
|
+
* @param {string} waypoint Waypoint
|
|
216
|
+
* @returns {void}
|
|
217
|
+
* @throws {TypeError}
|
|
218
|
+
* @throws {SyntaxError}
|
|
219
|
+
*/
|
|
220
|
+
export function validateWaypoint(waypoint) {
|
|
221
|
+
if (typeof waypoint !== 'string') {
|
|
222
|
+
throw new TypeError('Waypoint must be a string');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!waypoint.length) {
|
|
226
|
+
throw new SyntaxError('Waypoint must not be empty');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (waypoint.match(/[^/a-z0-9_-]/)) {
|
|
230
|
+
throw new SyntaxError('Waypoint must contain only a-z, 0-9, -, _ and / characters');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/* eslint-disable class-methods-use-this */
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
import lodash from 'lodash';
|
|
4
|
+
import ValidationError from '../ValidationError.js';
|
|
5
|
+
import ValidatorFactory from '../ValidatorFactory.js';
|
|
6
|
+
import { stringifyInput, stripWhitespace } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
const { isPlainObject } = lodash;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @access private
|
|
12
|
+
* @typedef {import('../../casa').ErrorMessageConfig} ErrorMessageConfig
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} DateObjectConfigOptions
|
|
17
|
+
* @property {ErrorMessageConfig} errorMsg Error message config
|
|
18
|
+
* @property {object} [afterOffsetFromNow] Offset from now
|
|
19
|
+
* @property {ErrorMessageConfig} [errorMsgAfterOffset] Error if date is after this offset
|
|
20
|
+
* @property {object} [beforeOffsetFromNow] Offset from now
|
|
21
|
+
* @property {ErrorMessageConfig} [errorMsgBeforeOffset] Error if date is before this offset
|
|
22
|
+
* @property {boolean} [allowMonthNames=false] Allow "Jan", "January", etc
|
|
23
|
+
* @property {boolean} [allowSingleDigitDay=false] Allow "1" rather than "01"
|
|
24
|
+
* @property {boolean} [allowSingleDigitMonth=false] Allow "1" rather than "01"
|
|
25
|
+
* @property {DateTime} [now=false] Override the notion of "now" (useful for testing)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Date object format:
|
|
30
|
+
* {
|
|
31
|
+
* dd: <string>,
|
|
32
|
+
* mm: <string>,
|
|
33
|
+
* yyyy: <string>
|
|
34
|
+
* }.
|
|
35
|
+
*
|
|
36
|
+
* Note that the time part will be zero'ed, as we are only interested in the
|
|
37
|
+
* date component (minimum day resolution).
|
|
38
|
+
*
|
|
39
|
+
* See {@link DateObjectConfigOptions} for `make()` options.
|
|
40
|
+
*
|
|
41
|
+
* @memberof Validators
|
|
42
|
+
* @augments ValidatorFactory
|
|
43
|
+
*/
|
|
44
|
+
export default class DateObject extends ValidatorFactory {
|
|
45
|
+
/** @property {string} name Validator name ("dateObject") */
|
|
46
|
+
name = 'dateObject';
|
|
47
|
+
|
|
48
|
+
validate(value, dataContext = {}) {
|
|
49
|
+
const config = {
|
|
50
|
+
errorMsg: {
|
|
51
|
+
inline: 'validation:rule.dateObject.inline',
|
|
52
|
+
summary: 'validation:rule.dateObject.summary',
|
|
53
|
+
},
|
|
54
|
+
errorMsgAfterOffset: {
|
|
55
|
+
inline: 'validation:rule.dateObject.afterOffset.inline',
|
|
56
|
+
summary: 'validation:rule.dateObject.afterOffset.summary',
|
|
57
|
+
},
|
|
58
|
+
errorMsgBeforeOffset: {
|
|
59
|
+
inline: 'validation:rule.dateObject.beforeOffset.inline',
|
|
60
|
+
summary: 'validation:rule.dateObject.beforeOffset.summary',
|
|
61
|
+
},
|
|
62
|
+
now: DateTime.local(),
|
|
63
|
+
allowSingleDigitDay: false,
|
|
64
|
+
allowSingleDigitMonth: false,
|
|
65
|
+
allowMonthNames: false,
|
|
66
|
+
afterOffsetFromNow: undefined,
|
|
67
|
+
beforeOffsetFromNow: undefined,
|
|
68
|
+
...this.config,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let valid = false;
|
|
72
|
+
let { errorMsg } = config;
|
|
73
|
+
let luxonDate;
|
|
74
|
+
const NOW = config.now.startOf('day');
|
|
75
|
+
|
|
76
|
+
// Accepted formats
|
|
77
|
+
let formats = ['dd-MM-yyyy'];
|
|
78
|
+
const formatTests = [{
|
|
79
|
+
flags: [config.allowSingleDigitDay],
|
|
80
|
+
formats: ['d-MM-yyyy'],
|
|
81
|
+
}, {
|
|
82
|
+
flags: [config.allowSingleDigitDay, config.allowSingleDigitMonth],
|
|
83
|
+
formats: ['d-M-yyyy'],
|
|
84
|
+
}, {
|
|
85
|
+
flags: [config.allowSingleDigitDay, config.allowMonthNames],
|
|
86
|
+
formats: ['d-MMM-yyyy', 'd-MMMM-yyyy'],
|
|
87
|
+
}, {
|
|
88
|
+
flags: [config.allowSingleDigitMonth],
|
|
89
|
+
formats: ['dd-M-yyyy'],
|
|
90
|
+
}, {
|
|
91
|
+
flags: [config.allowMonthNames],
|
|
92
|
+
formats: ['dd-MMM-yyyy', 'dd-MMMM-yyyy'],
|
|
93
|
+
}];
|
|
94
|
+
formatTests.forEach((test) => {
|
|
95
|
+
if (test.flags.every((v) => v === true)) {
|
|
96
|
+
formats = [...formats, ...test.formats];
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (typeof value === 'object') {
|
|
101
|
+
formats.find((format) => {
|
|
102
|
+
luxonDate = DateTime.fromFormat(
|
|
103
|
+
[value.dd, value.mm, value.yyyy].join('-'),
|
|
104
|
+
format,
|
|
105
|
+
).startOf('day');
|
|
106
|
+
|
|
107
|
+
valid = luxonDate.isValid;
|
|
108
|
+
|
|
109
|
+
return valid;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (luxonDate) {
|
|
113
|
+
// Check date is after the specified duration from now.
|
|
114
|
+
// Need to use UTC() otherwise DST shifts can affect the calculated offset
|
|
115
|
+
if (config.afterOffsetFromNow) {
|
|
116
|
+
const offsetDate = NOW.plus(config.afterOffsetFromNow).startOf('day');
|
|
117
|
+
|
|
118
|
+
if (luxonDate <= offsetDate) {
|
|
119
|
+
valid = false;
|
|
120
|
+
errorMsg = config.errorMsgAfterOffset;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check date is before the specified duration from now
|
|
125
|
+
// Need to use UTC() otherwise DST shifts can affect the calculated offset
|
|
126
|
+
if (config.beforeOffsetFromNow) {
|
|
127
|
+
const offsetDate = NOW.plus(config.beforeOffsetFromNow).startOf('day');
|
|
128
|
+
|
|
129
|
+
if (luxonDate >= offsetDate) {
|
|
130
|
+
valid = false;
|
|
131
|
+
errorMsg = config.errorMsgBeforeOffset;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check presence of each object component (dd, mm, yyyy) in order to log
|
|
137
|
+
// which specific parts are in error
|
|
138
|
+
errorMsg.focusSuffix = [];
|
|
139
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'dd') || !value.dd) {
|
|
140
|
+
errorMsg.focusSuffix.push('[dd]');
|
|
141
|
+
}
|
|
142
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'mm') || !value.mm) {
|
|
143
|
+
errorMsg.focusSuffix.push('[mm]');
|
|
144
|
+
}
|
|
145
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'yyyy') || !value.yyyy) {
|
|
146
|
+
errorMsg.focusSuffix.push('[yyyy]');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// If the date is invalid, but not specific parts have been highlighted in
|
|
150
|
+
// error, then highlight all inputs, focusing on the [dd] first
|
|
151
|
+
if (!valid && !errorMsg.focusSuffix.length) {
|
|
152
|
+
errorMsg.focusSuffix = ['[dd]', '[mm]', '[yyyy]'];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return valid ? [] : [ValidationError.make({ errorMsg, dataContext })];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
sanitise(value) {
|
|
160
|
+
if (value !== undefined) {
|
|
161
|
+
return isPlainObject(value) ? {
|
|
162
|
+
dd: stripWhitespace(stringifyInput(value.dd)),
|
|
163
|
+
mm: stripWhitespace(stringifyInput(value.mm)),
|
|
164
|
+
yyyy: stripWhitespace(stringifyInput(value.yyyy)),
|
|
165
|
+
} : Object.create(null);
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* eslint-disable class-methods-use-this */
|
|
2
|
+
import validatorPkg from 'validator';
|
|
3
|
+
import ValidationError from '../ValidationError.js';
|
|
4
|
+
import ValidatorFactory from '../ValidatorFactory.js';
|
|
5
|
+
import { stringifyInput } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
const { isEmail } = validatorPkg; // CommonJS
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @access private
|
|
11
|
+
* @typedef {import('../../casa').ErrorMessageConfig} ErrorMessageConfig
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} EmailConfigOptions
|
|
16
|
+
* @property {ErrorMessageConfig} errorMsg Error message config
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Email address.
|
|
21
|
+
*
|
|
22
|
+
* This is not an exhaustive validation, and is permissive.
|
|
23
|
+
*
|
|
24
|
+
* See {@link EmailConfigOptions} for `make()` options.
|
|
25
|
+
*
|
|
26
|
+
* @memberof Validators
|
|
27
|
+
* @augments ValidatorFactory
|
|
28
|
+
*/
|
|
29
|
+
export default class Email extends ValidatorFactory {
|
|
30
|
+
/** @property {string} name Validator name ("email") */
|
|
31
|
+
name = 'email';
|
|
32
|
+
|
|
33
|
+
validate(value, dataContext = {}) {
|
|
34
|
+
let isValid;
|
|
35
|
+
try {
|
|
36
|
+
isValid = isEmail(value);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
isValid = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const errorMsg = this.config.errorMsg || {
|
|
42
|
+
summary: 'validation:rule.email.summary',
|
|
43
|
+
inline: 'validation:rule.email.inline',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return isValid ? [] : [ValidationError.make({ errorMsg, dataContext })];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
sanitise(value) {
|
|
50
|
+
if (value !== undefined) {
|
|
51
|
+
return stringifyInput(value);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|