@dwp/govuk-casa
Version:
A framework for building GOVUK Collect-And-Submit-Applications
238 lines • 7.4 kB
JavaScript
;
/**
* @typedef {import("../casa").GlobalHook | import("../casa").PageHook} Hook
* @access private
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.isEmpty = isEmpty;
exports.isStringable = isStringable;
exports.resolveMiddlewareHooks = resolveMiddlewareHooks;
exports.stringifyInput = stringifyInput;
exports.coerceInputToInteger = coerceInputToInteger;
exports.stripWhitespace = stripWhitespace;
exports.notProto = notProto;
exports.validateHookName = validateHookName;
exports.validateHookPath = validateHookPath;
exports.validateUrlPath = validateUrlPath;
exports.validateView = validateView;
exports.validateWaypoint = validateWaypoint;
/**
* Determine if value is empty. Recurse over objects.
*
* @param {any} val Value to check
* @returns {boolean} True if the object is empty
* @access private
*/
function isEmpty(val) {
if (val === null ||
typeof val === "undefined" ||
(typeof val === "string" && val === "")) {
return true;
}
if (Array.isArray(val) || typeof val === "object") {
// ESLint disabled as `k` is an "own property" (thanks to `Object.keys()`)
/* eslint-disable-next-line security/detect-object-injection */
return Object.keys(val).filter((k) => !isEmpty(val[k])).length === 0;
}
return false;
}
/**
* Test is a value can be stringified (numbers or strings)
*
* @param {any} value Item to test
* @returns {boolean} Whether the value is stringable or not
* @access private
*/
function isStringable(value) {
return typeof value === "string" || typeof value === "number";
}
/**
* Extract the middleware functions that are relevant for the given hook and
* path.
*
* @param {string} hookName Hook name (including scope prefix)
* @param {string} path URL path to match (relative to mountUrl)
* @param {Hook[]} hooks Hooks to be applied at the page level
* @returns {Function[]} An array of middleware that should be applied
* @access private
*/
function resolveMiddlewareHooks(hookName, path, hooks = []) {
const pathMatch = (h) => h.path === undefined ||
(h.path instanceof RegExp && h.path.test(path)) ||
h.path === path;
return hooks
.filter((h) => h.hook === hookName)
.filter(pathMatch)
.map((h) => h.middleware);
}
/**
* Coerce an input to a string.
*
* @param {any} input Input to be stringified
* @param {string} fallback Fallback to use if input can't be stringified
* @returns {string} The stringified input
* @access private
*/
function stringifyInput(input, fallback) {
// Not using param defaults here as the fallback may be explicitly "undefined"
const fb = arguments.length === 2 && (isStringable(fallback) || fallback === undefined)
? fallback
: "";
return isStringable(input) ? String(input) : fb;
}
/**
* Coerce an input to an integer.
*
* @param {any} input Input to be coerced.
* @returns {number | undefined} The number as an integer or `undefined`.
*/
function coerceInputToInteger(input) {
return Number.isNaN(Number(input)) ? undefined : Math.floor(Number(input));
}
/**
* Strip whitespace from a string.
*
* @param {string} value Value to be stripped of whitespace
* @param {object} options Overrides for the default whitespace replacements
* @returns {string} Value stripped of white space
* @throws {TypeError}
* @access private
*/
function stripWhitespace(value, options) {
const opts = {
leading: "",
trailing: "",
nested: " ",
...options,
};
if (typeof value !== "string") {
throw new TypeError("value must be a string");
}
if (typeof opts.leading !== "string") {
throw new TypeError("leading must be a string");
}
if (typeof opts.trailing !== "string") {
throw new TypeError("trailing must be a string");
}
if (typeof opts.nested !== "string") {
throw new TypeError("nested must be a string");
}
// This approach avoids using `/s+$/` regex, which triggers the
// `sonarjs/slow-regex` eslint rule
let newValue = value.replace(/^\s+/, opts.leading);
if (newValue.match(/\s$/)) {
newValue = `${newValue.trimEnd()}${opts.trailing}`;
}
newValue = newValue.replace(/\s+/g, opts.nested);
return newValue;
}
/* ------------------------------------------------ validation / sanitisation */
/**
* Checks if the given string can be used as an object key.
*
* @param {string} key Proposed Object key
* @returns {string} Same key if it's valid
* @throws {Error} If proposed key is an invalid keyword
* @access private
*/
function notProto(key) {
if (["__proto__", "constructor", "prototype"].includes(String(key).toLowerCase())) {
throw new Error("Attempt to use prototype key disallowed");
}
return key;
}
/**
* Validate a hook name.
*
* @param {string} hookName Hook name
* @returns {void}
* @throws {TypeError}
* @throws {SyntaxError}
* @access private
*/
function validateHookName(hookName) {
if (typeof hookName !== "string") {
throw new TypeError("Hook name must be a string");
}
if (!hookName.length) {
throw new SyntaxError("Hook name must not be empty");
}
if (!hookName.match(/^([a-z_]+\.|)[a-z_]+$/i)) {
throw new SyntaxError("Hook name must match either <scope>.<hookname> or <hookname> formats");
}
}
/**
* Validate a hook path.
*
* @param {string} path URL path
* @returns {void}
* @throws {TypeError}
* @access private
*/
function validateHookPath(path) {
if (typeof path !== "string" && !(path instanceof RegExp)) {
throw new TypeError("Hook path must be a string or RegExp");
}
}
/**
* Validate a URL path.
*
* @param {string} path URL path
* @returns {string} Same string, if valid
* @throws {TypeError}
* @throws {SyntaxError}
* @access private
*/
function validateUrlPath(path) {
if (typeof path !== "string") {
throw new TypeError("URL path must be a string");
}
if (path.match(/[^/a-z0-9_-]/)) {
throw new SyntaxError("URL path must contain only a-z, 0-9, -, _ and / characters");
}
if (path.match(/\/{2,}/)) {
throw new SyntaxError("URL path must not contain consecutive /");
}
return path;
}
/**
* Validate a template name.
*
* @param {string} view Template name
* @returns {void}
* @throws {TypeError}
* @throws {SyntaxError}
* @access private
*/
function validateView(view) {
if (typeof view !== "string") {
throw new TypeError("View must be a string");
}
if (!view.length) {
throw new SyntaxError("View must not be empty");
}
if (!view.match(/^[a-z0-9/_-]+\.njk$/i)) {
throw new SyntaxError("View must contain only a-z, 0-9, -, _ and / characters, and end in .njk");
}
}
/**
* Validate a waypoint.
*
* @param {string} waypoint Waypoint
* @returns {void}
* @throws {TypeError}
* @throws {SyntaxError}
* @access private
*/
function validateWaypoint(waypoint) {
if (typeof waypoint !== "string") {
throw new TypeError("Waypoint must be a string");
}
if (!waypoint.length) {
throw new SyntaxError("Waypoint must not be empty");
}
if (waypoint.match(/[^/a-z0-9_-]/)) {
throw new SyntaxError("Waypoint must contain only a-z, 0-9, -, _ and / characters");
}
}
//# sourceMappingURL=utils.js.map