@dwp/govuk-casa 8.7.9 → 8.8.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.
Files changed (175) hide show
  1. package/README.md +0 -5
  2. package/dist/casa.d.ts +1 -1
  3. package/dist/casa.js +3 -2
  4. package/dist/casa.js.map +1 -0
  5. package/dist/lib/CasaTemplateLoader.d.ts +1 -7
  6. package/dist/lib/CasaTemplateLoader.js +6 -1
  7. package/dist/lib/CasaTemplateLoader.js.map +1 -0
  8. package/dist/lib/JourneyContext.d.ts +1 -1
  9. package/dist/lib/JourneyContext.js +2 -1
  10. package/dist/lib/JourneyContext.js.map +1 -0
  11. package/dist/lib/MutableRouter.js +1 -0
  12. package/dist/lib/MutableRouter.js.map +1 -0
  13. package/dist/lib/Plan.d.ts +2 -0
  14. package/dist/lib/Plan.js +9 -4
  15. package/dist/lib/Plan.js.map +1 -0
  16. package/dist/lib/ValidationError.js +2 -1
  17. package/dist/lib/ValidationError.js.map +1 -0
  18. package/dist/lib/ValidatorFactory.d.ts +2 -2
  19. package/dist/lib/ValidatorFactory.js +3 -2
  20. package/dist/lib/ValidatorFactory.js.map +1 -0
  21. package/dist/lib/configuration-ingestor.d.ts +1 -1
  22. package/dist/lib/configuration-ingestor.js +2 -1
  23. package/dist/lib/configuration-ingestor.js.map +1 -0
  24. package/dist/lib/configure.js +2 -1
  25. package/dist/lib/configure.js.map +1 -0
  26. package/dist/lib/end-session.js +1 -0
  27. package/dist/lib/end-session.js.map +1 -0
  28. package/dist/lib/field.js +1 -0
  29. package/dist/lib/field.js.map +1 -0
  30. package/dist/lib/index.js +1 -0
  31. package/dist/lib/index.js.map +1 -0
  32. package/dist/lib/logger.js +1 -0
  33. package/dist/lib/logger.js.map +1 -0
  34. package/dist/lib/mount.js +3 -2
  35. package/dist/lib/mount.js.map +1 -0
  36. package/dist/lib/nunjucks-filters.js +28 -1
  37. package/dist/lib/nunjucks-filters.js.map +1 -0
  38. package/dist/lib/nunjucks.js +1 -0
  39. package/dist/lib/nunjucks.js.map +1 -0
  40. package/dist/lib/utils.d.ts +46 -28
  41. package/dist/lib/utils.js +105 -67
  42. package/dist/lib/utils.js.map +1 -0
  43. package/dist/lib/validators/dateObject.js +5 -4
  44. package/dist/lib/validators/dateObject.js.map +1 -0
  45. package/dist/lib/validators/email.js +1 -0
  46. package/dist/lib/validators/email.js.map +1 -0
  47. package/dist/lib/validators/inArray.js +1 -0
  48. package/dist/lib/validators/inArray.js.map +1 -0
  49. package/dist/lib/validators/index.js +1 -0
  50. package/dist/lib/validators/index.js.map +1 -0
  51. package/dist/lib/validators/nino.js +1 -0
  52. package/dist/lib/validators/nino.js.map +1 -0
  53. package/dist/lib/validators/postalAddressObject.d.ts +2 -2
  54. package/dist/lib/validators/postalAddressObject.js +2 -1
  55. package/dist/lib/validators/postalAddressObject.js.map +1 -0
  56. package/dist/lib/validators/regex.js +1 -0
  57. package/dist/lib/validators/regex.js.map +1 -0
  58. package/dist/lib/validators/required.d.ts +1 -1
  59. package/dist/lib/validators/required.js +2 -1
  60. package/dist/lib/validators/required.js.map +1 -0
  61. package/dist/lib/validators/strlen.js +1 -0
  62. package/dist/lib/validators/strlen.js.map +1 -0
  63. package/dist/lib/validators/wordCount.js +1 -0
  64. package/dist/lib/validators/wordCount.js.map +1 -0
  65. package/dist/lib/waypoint-url.js +1 -0
  66. package/dist/lib/waypoint-url.js.map +1 -0
  67. package/dist/middleware/body-parser.js +1 -0
  68. package/dist/middleware/body-parser.js.map +1 -0
  69. package/dist/middleware/csrf.js +1 -0
  70. package/dist/middleware/csrf.js.map +1 -0
  71. package/dist/middleware/data.js +1 -0
  72. package/dist/middleware/data.js.map +1 -0
  73. package/dist/middleware/gather-fields.js +2 -1
  74. package/dist/middleware/gather-fields.js.map +1 -0
  75. package/dist/middleware/i18n.js +1 -0
  76. package/dist/middleware/i18n.js.map +1 -0
  77. package/dist/middleware/post.js +1 -0
  78. package/dist/middleware/post.js.map +1 -0
  79. package/dist/middleware/pre.js +1 -0
  80. package/dist/middleware/pre.js.map +1 -0
  81. package/dist/middleware/progress-journey.js +1 -0
  82. package/dist/middleware/progress-journey.js.map +1 -0
  83. package/dist/middleware/sanitise-fields.js +1 -0
  84. package/dist/middleware/sanitise-fields.js.map +1 -0
  85. package/dist/middleware/serve-first-waypoint.js +1 -0
  86. package/dist/middleware/serve-first-waypoint.js.map +1 -0
  87. package/dist/middleware/session.js +1 -0
  88. package/dist/middleware/session.js.map +1 -0
  89. package/dist/middleware/skip-waypoint.js +1 -0
  90. package/dist/middleware/skip-waypoint.js.map +1 -0
  91. package/dist/middleware/steer-journey.js +1 -0
  92. package/dist/middleware/steer-journey.js.map +1 -0
  93. package/dist/middleware/strip-proxy-path.js +2 -1
  94. package/dist/middleware/strip-proxy-path.js.map +1 -0
  95. package/dist/middleware/validate-fields.js +2 -1
  96. package/dist/middleware/validate-fields.js.map +1 -0
  97. package/dist/mjs/esm-wrapper.js +10 -15
  98. package/dist/routes/ancillary.js +1 -0
  99. package/dist/routes/ancillary.js.map +1 -0
  100. package/dist/routes/journey.js +1 -0
  101. package/dist/routes/journey.js.map +1 -0
  102. package/dist/routes/static.js +1 -0
  103. package/dist/routes/static.js.map +1 -0
  104. package/locales/cy/error.json +1 -1
  105. package/locales/en/error.json +1 -1
  106. package/package.json +20 -19
  107. package/src/casa.js +320 -0
  108. package/src/lib/CasaTemplateLoader.js +104 -0
  109. package/src/lib/JourneyContext.js +783 -0
  110. package/src/lib/MutableRouter.js +310 -0
  111. package/src/lib/Plan.js +624 -0
  112. package/src/lib/ValidationError.js +163 -0
  113. package/src/lib/ValidatorFactory.js +105 -0
  114. package/src/lib/configuration-ingestor.js +457 -0
  115. package/src/lib/configure.js +202 -0
  116. package/src/lib/dirname.cjs +1 -0
  117. package/src/lib/end-session.js +45 -0
  118. package/src/lib/field.js +456 -0
  119. package/src/lib/index.js +33 -0
  120. package/src/lib/logger.js +16 -0
  121. package/src/lib/mount.js +127 -0
  122. package/src/lib/nunjucks-filters.js +150 -0
  123. package/src/lib/nunjucks.js +53 -0
  124. package/src/lib/utils.js +232 -0
  125. package/src/lib/validators/dateObject.js +169 -0
  126. package/src/lib/validators/email.js +55 -0
  127. package/src/lib/validators/inArray.js +81 -0
  128. package/src/lib/validators/index.js +24 -0
  129. package/src/lib/validators/nino.js +57 -0
  130. package/src/lib/validators/postalAddressObject.js +162 -0
  131. package/src/lib/validators/regex.js +48 -0
  132. package/src/lib/validators/required.js +74 -0
  133. package/src/lib/validators/strlen.js +66 -0
  134. package/src/lib/validators/wordCount.js +70 -0
  135. package/src/lib/waypoint-url.js +93 -0
  136. package/src/middleware/body-parser.js +31 -0
  137. package/src/middleware/csrf.js +29 -0
  138. package/src/middleware/data.js +105 -0
  139. package/src/middleware/dirname.cjs +1 -0
  140. package/src/middleware/gather-fields.js +51 -0
  141. package/src/middleware/i18n.js +106 -0
  142. package/src/middleware/post.js +61 -0
  143. package/src/middleware/pre.js +91 -0
  144. package/src/middleware/progress-journey.js +92 -0
  145. package/src/middleware/sanitise-fields.js +58 -0
  146. package/src/middleware/serve-first-waypoint.js +28 -0
  147. package/src/middleware/session.js +129 -0
  148. package/src/middleware/skip-waypoint.js +46 -0
  149. package/src/middleware/steer-journey.js +78 -0
  150. package/src/middleware/strip-proxy-path.js +56 -0
  151. package/src/middleware/validate-fields.js +84 -0
  152. package/src/routes/ancillary.js +29 -0
  153. package/src/routes/dirname.cjs +1 -0
  154. package/src/routes/journey.js +212 -0
  155. package/src/routes/static.js +77 -0
  156. package/views/casa/components/character-count/README.md +10 -0
  157. package/views/casa/components/character-count/template.njk +6 -2
  158. package/views/casa/components/checkboxes/README.md +45 -37
  159. package/views/casa/components/checkboxes/template.njk +8 -7
  160. package/views/casa/components/date-input/README.md +15 -3
  161. package/views/casa/components/date-input/template.njk +6 -4
  162. package/views/casa/components/input/README.md +9 -0
  163. package/views/casa/components/input/template.njk +6 -2
  164. package/views/casa/components/journey-form/README.md +3 -2
  165. package/views/casa/components/postal-address-object/README.md +10 -0
  166. package/views/casa/components/postal-address-object/template.njk +20 -5
  167. package/views/casa/components/radios/README.md +51 -26
  168. package/views/casa/components/radios/template.njk +6 -3
  169. package/views/casa/components/select/README.md +65 -0
  170. package/views/casa/components/select/macro.njk +3 -0
  171. package/views/casa/components/select/template.njk +49 -0
  172. package/views/casa/components/textarea/README.md +9 -0
  173. package/views/casa/components/textarea/template.njk +6 -2
  174. package/views/casa/layouts/journey.njk +1 -1
  175. package/views/casa/layouts/main.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
+ '<': '&lt;',
130
+ '>': '&gt;',
131
+ '"': '&quot;',
132
+ '\'': '&#039;',
133
+ '&': '&amp;',
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
+ }
@@ -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
+ }