@montevideo-tech/cmcd-validator
Version:
This is a library created to validate CMCD
1,278 lines (1,206 loc) • 48.6 kB
JavaScript
function _iterableToArrayLimit(arr, i) {
var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"];
if (null != _i) {
var _s,
_e,
_x,
_r,
_arr = [],
_n = !0,
_d = !1;
try {
if (_x = (_i = _i.call(arr)).next, 0 === i) {
if (Object(_i) !== _i) return;
_n = !1;
} else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0);
} catch (err) {
_d = !0, _e = err;
} finally {
try {
if (!_n && null != _i.return && (_r = _i.return(), Object(_r) !== _r)) return;
} finally {
if (_d) throw _e;
}
}
return _arr;
}
}
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
enumerableOnly && (symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
})), keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = null != arguments[i] ? arguments[i] : {};
i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
_defineProperty(target, key, source[key]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
return target;
}
function _defineProperty(obj, key, value) {
key = _toPropertyKey(key);
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
}
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _toPrimitive(input, hint) {
if (typeof input !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || "default");
if (typeof res !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}
function _toPropertyKey(arg) {
var key = _toPrimitive(arg, "string");
return typeof key === "symbol" ? key : String(key);
}
// Unique ID creation requires a high quality random # generator. In the browser we therefore
// require the crypto API and do not support built-in fallback to lower quality random number
// generators (like Math.random()).
let getRandomValues;
const rnds8 = new Uint8Array(16);
function rng() {
// lazy load so that environments that need to polyfill have a chance to do so
if (!getRandomValues) {
// getRandomValues needs to be invoked in a context where "this" is a Crypto implementation.
getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto);
if (!getRandomValues) {
throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported');
}
}
return getRandomValues(rnds8);
}
/**
* Convert array of 16 byte values to UUID string format of the form:
* XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
*/
const byteToHex = [];
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 0x100).toString(16).slice(1));
}
function unsafeStringify(arr, offset = 0) {
// Note: Be careful editing this code! It's been tuned for performance
// and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
}
const randomUUID = typeof crypto !== 'undefined' && crypto.randomUUID && crypto.randomUUID.bind(crypto);
var native = {
randomUUID
};
function v4(options, buf, offset) {
if (native.randomUUID && !buf && !options) {
return native.randomUUID();
}
options = options || {};
const rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
rnds[6] = rnds[6] & 0x0f | 0x40;
rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided
if (buf) {
offset = offset || 0;
for (let i = 0; i < 16; ++i) {
buf[offset + i] = rnds[i];
}
return buf;
}
return unsafeStringify(rnds);
}
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var logger = {exports: {}};
/*!
* js-logger - http://github.com/jonnyreeves/js-logger
* Jonny Reeves, http://jonnyreeves.co.uk/
* js-logger may be freely distributed under the MIT license.
*/
logger.exports;
(function (module) {
(function (global) {
// Top level module for the global, static logger instance.
var Logger = {};
// For those that are at home that are keeping score.
Logger.VERSION = "1.6.1";
// Function which handles all incoming log messages.
var logHandler;
// Map of ContextualLogger instances by name; used by Logger.get() to return the same named instance.
var contextualLoggersByNameMap = {};
// Polyfill for ES5's Function.bind.
var bind = function bind(scope, func) {
return function () {
return func.apply(scope, arguments);
};
};
// Super exciting object merger-matron 9000 adding another 100 bytes to your download.
var merge = function merge() {
var args = arguments,
target = args[0],
key,
i;
for (i = 1; i < args.length; i++) {
for (key in args[i]) {
if (!(key in target) && args[i].hasOwnProperty(key)) {
target[key] = args[i][key];
}
}
}
return target;
};
// Helper to define a logging level object; helps with optimisation.
var defineLogLevel = function defineLogLevel(value, name) {
return {
value: value,
name: name
};
};
// Predefined logging levels.
Logger.TRACE = defineLogLevel(1, 'TRACE');
Logger.DEBUG = defineLogLevel(2, 'DEBUG');
Logger.INFO = defineLogLevel(3, 'INFO');
Logger.TIME = defineLogLevel(4, 'TIME');
Logger.WARN = defineLogLevel(5, 'WARN');
Logger.ERROR = defineLogLevel(8, 'ERROR');
Logger.OFF = defineLogLevel(99, 'OFF');
// Inner class which performs the bulk of the work; ContextualLogger instances can be configured independently
// of each other.
var ContextualLogger = function ContextualLogger(defaultContext) {
this.context = defaultContext;
this.setLevel(defaultContext.filterLevel);
this.log = this.info; // Convenience alias.
};
ContextualLogger.prototype = {
// Changes the current logging level for the logging instance.
setLevel: function setLevel(newLevel) {
// Ensure the supplied Level object looks valid.
if (newLevel && "value" in newLevel) {
this.context.filterLevel = newLevel;
}
},
// Gets the current logging level for the logging instance
getLevel: function getLevel() {
return this.context.filterLevel;
},
// Is the logger configured to output messages at the supplied level?
enabledFor: function enabledFor(lvl) {
var filterLevel = this.context.filterLevel;
return lvl.value >= filterLevel.value;
},
trace: function trace() {
this.invoke(Logger.TRACE, arguments);
},
debug: function debug() {
this.invoke(Logger.DEBUG, arguments);
},
info: function info() {
this.invoke(Logger.INFO, arguments);
},
warn: function warn() {
this.invoke(Logger.WARN, arguments);
},
error: function error() {
this.invoke(Logger.ERROR, arguments);
},
time: function time(label) {
if (typeof label === 'string' && label.length > 0) {
this.invoke(Logger.TIME, [label, 'start']);
}
},
timeEnd: function timeEnd(label) {
if (typeof label === 'string' && label.length > 0) {
this.invoke(Logger.TIME, [label, 'end']);
}
},
// Invokes the logger callback if it's not being filtered.
invoke: function invoke(level, msgArgs) {
if (logHandler && this.enabledFor(level)) {
logHandler(msgArgs, merge({
level: level
}, this.context));
}
}
};
// Protected instance which all calls to the to level `Logger` module will be routed through.
var globalLogger = new ContextualLogger({
filterLevel: Logger.OFF
});
// Configure the global Logger instance.
(function () {
// Shortcut for optimisers.
var L = Logger;
L.enabledFor = bind(globalLogger, globalLogger.enabledFor);
L.trace = bind(globalLogger, globalLogger.trace);
L.debug = bind(globalLogger, globalLogger.debug);
L.time = bind(globalLogger, globalLogger.time);
L.timeEnd = bind(globalLogger, globalLogger.timeEnd);
L.info = bind(globalLogger, globalLogger.info);
L.warn = bind(globalLogger, globalLogger.warn);
L.error = bind(globalLogger, globalLogger.error);
// Don't forget the convenience alias!
L.log = L.info;
})();
// Set the global logging handler. The supplied function should expect two arguments, the first being an arguments
// object with the supplied log messages and the second being a context object which contains a hash of stateful
// parameters which the logging function can consume.
Logger.setHandler = function (func) {
logHandler = func;
};
// Sets the global logging filter level which applies to *all* previously registered, and future Logger instances.
// (note that named loggers (retrieved via `Logger.get`) can be configured independently if required).
Logger.setLevel = function (level) {
// Set the globalLogger's level.
globalLogger.setLevel(level);
// Apply this level to all registered contextual loggers.
for (var key in contextualLoggersByNameMap) {
if (contextualLoggersByNameMap.hasOwnProperty(key)) {
contextualLoggersByNameMap[key].setLevel(level);
}
}
};
// Gets the global logging filter level
Logger.getLevel = function () {
return globalLogger.getLevel();
};
// Retrieve a ContextualLogger instance. Note that named loggers automatically inherit the global logger's level,
// default context and log handler.
Logger.get = function (name) {
// All logger instances are cached so they can be configured ahead of use.
return contextualLoggersByNameMap[name] || (contextualLoggersByNameMap[name] = new ContextualLogger(merge({
name: name
}, globalLogger.context)));
};
// CreateDefaultHandler returns a handler function which can be passed to `Logger.setHandler()` which will
// write to the window's console object (if present); the optional options object can be used to customise the
// formatter used to format each log message.
Logger.createDefaultHandler = function (options) {
options = options || {};
options.formatter = options.formatter || function defaultMessageFormatter(messages, context) {
// Prepend the logger's name to the log message for easy identification.
if (context.name) {
messages.unshift("[" + context.name + "]");
}
};
// Map of timestamps by timer labels used to track `#time` and `#timeEnd()` invocations in environments
// that don't offer a native console method.
var timerStartTimeByLabelMap = {};
// Support for IE8+ (and other, slightly more sane environments)
var invokeConsoleMethod = function invokeConsoleMethod(hdlr, messages) {
Function.prototype.apply.call(hdlr, console, messages);
};
// Check for the presence of a logger.
if (typeof console === "undefined") {
return function () {/* no console */};
}
return function (messages, context) {
// Convert arguments object to Array.
messages = Array.prototype.slice.call(messages);
var hdlr = console.log;
var timerLabel;
if (context.level === Logger.TIME) {
timerLabel = (context.name ? '[' + context.name + '] ' : '') + messages[0];
if (messages[1] === 'start') {
if (console.time) {
console.time(timerLabel);
} else {
timerStartTimeByLabelMap[timerLabel] = new Date().getTime();
}
} else {
if (console.timeEnd) {
console.timeEnd(timerLabel);
} else {
invokeConsoleMethod(hdlr, [timerLabel + ': ' + (new Date().getTime() - timerStartTimeByLabelMap[timerLabel]) + 'ms']);
}
}
} else {
// Delegate through to custom warn/error loggers if present on the console.
if (context.level === Logger.WARN && console.warn) {
hdlr = console.warn;
} else if (context.level === Logger.ERROR && console.error) {
hdlr = console.error;
} else if (context.level === Logger.INFO && console.info) {
hdlr = console.info;
} else if (context.level === Logger.DEBUG && console.debug) {
hdlr = console.debug;
} else if (context.level === Logger.TRACE && console.trace) {
hdlr = console.trace;
}
options.formatter(messages, context);
invokeConsoleMethod(hdlr, messages);
}
};
};
// Configure and example a Default implementation which writes to the `window.console` (if present). The
// `options` hash can be used to configure the default logLevel and provide a custom message formatter.
Logger.useDefaults = function (options) {
Logger.setLevel(options && options.defaultLevel || Logger.DEBUG);
Logger.setHandler(Logger.createDefaultHandler(options));
};
// Createa an alias to useDefaults to avoid reaking a react-hooks rule.
Logger.setDefaults = Logger.useDefaults;
// Export to popular environments boilerplate.
if (module.exports) {
module.exports = Logger;
} else {
Logger._prevLogger = global.Logger;
Logger.noConflict = function () {
global.Logger = Logger._prevLogger;
return Logger;
};
global.Logger = Logger;
}
})(commonjsGlobal);
})(logger);
var loggerExports = logger.exports;
var jsLogger = /*@__PURE__*/getDefaultExportFromCjs(loggerExports);
const cmcdTypes = {
number: 'number',
boolean: 'boolean',
token: 'token',
string: 'string'
};
const keyTypes = {
br: cmcdTypes.number,
bl: cmcdTypes.number,
bs: cmcdTypes.boolean,
cid: cmcdTypes.string,
d: cmcdTypes.number,
dl: cmcdTypes.number,
mtp: cmcdTypes.number,
nor: cmcdTypes.string,
nrr: cmcdTypes.string,
ot: cmcdTypes.token,
pr: cmcdTypes.number,
rtp: cmcdTypes.number,
sf: cmcdTypes.token,
sid: cmcdTypes.string,
st: cmcdTypes.token,
su: cmcdTypes.boolean,
tb: cmcdTypes.number,
v: cmcdTypes.number
};
const errorTypes = {
noCMCDRequest: 'no-CMCD-request',
unknownKey: 'unknown-key',
invalidValue: 'invalid-value',
wrongTypeValue: 'wrong-type-value',
incorrectFormat: 'incorrect-format',
parameterEncoding: 'parameter-encoding',
missingKey: 'missing-key',
invalidHeader: 'invalid-header',
invalidJson: 'invalid-json',
duplicateKey: 'duplicate-key',
duplicatedHeader: 'duplicated-header',
noHeader: 'no-header',
emptyHeader: 'empty-header',
unnecessaryKey: 'unnecessary-key',
wrongCustomType: 'wrong-custom-type',
invalidCustomKey: 'invalid-custom-key',
unknownSpecificKey: 'unknown-specific-key',
wrongCustomHeader: 'wrong-custom-header',
queryMalformed: 'query-malformed',
noAmpersandBetweenRequests: 'no-ampersand-between-requests:'
};
const errorDescription = {
noCMCDRequest: 'No CMCD request found',
unknownKey: 'Key is not part of reserved keys',
invalidValue: 'Value does not meet requirements',
wrongTypeValue: 'Value type is incorrect',
incorrectFormat: 'Format is incorrect',
parameterEncoding: 'Parameter is not encoded',
missingKey: 'Key is missing',
invalidHeader: 'Header is not valid',
invalidJson: 'Json format is not valid',
duplicateKey: 'Key/Keys are not unique',
duplicatedHeader: 'Header is duplicated',
noHeader: 'There is no CMCD headers.',
emptyHeader: 'There is a header with no content',
unnecessaryKey: 'This key must not be sent with the current value',
wrongCustomType: 'The type does not mach with CMCD types',
invalidCustomKey: 'Custom key names MUST carry a hyphenated prefix',
unknownSpecificKey: 'The key does not mach with CMCD keys',
wrongCustomHeader: 'The header does not mach with CMCD headers',
queryMalformed: 'The query is malformed',
noAmpersandBetweenRequests: 'Ampersand required between two or more requests'
};
const cmcdHeader = {
'CMCD-Object': ['br', 'd', 'ot', 'tb'],
'CMCD-Request': ['bl', 'dl', 'mtp', 'nor', 'nrr', 'su'],
'CMCD-Session': ['cid', 'pr', 'sf', 'sid', 'st', 'v'],
'CMCD-Status': ['bs', 'rtp']
};
const warningTypes = {
noAlphabeticalOrder: 'no-alphabetical-order',
valuePr: 'pr-value',
valueV: 'v-value',
noSidReceived: 'no-sid-received',
blWithWrongOtValue: 'bl-with-wrong-ot-value',
noReverseDnsCustomKey: 'customkey-without-reversedns',
specificKeysNotSent: 'specific-keys-not-sent'
};
const warningDescription = {
noAlphabeticalOrder: 'Keys are not arranged alphabetically',
valuePr: 'Should only be sent if not equal to 1.00',
valueV: 'Client should omit this field if the version is 1',
noSidReceived: 'No sid received from CMCD message',
blWithWrongOtValue: 'bl key should only be sent with an object type of a, v or av',
noReverseDnsCustomKey: 'Customkey without Reverse DNS, could be: com.example-customkeyname',
specificKeysNotSent: 'A specific/custom key was set but not sent'
};
const getKeyByValue = (object, value) => Object.keys(object).find(key => object[key] === value);
const createError = (type, requestID, key, value, description) => {
jsLogger.useDefaults({
defaultLevel: jsLogger.TRACE
});
if (!Object.values(errorTypes).includes(type)) {
return -1;
}
const error = getKeyByValue(errorTypes, type);
if (description === undefined) {
jsLogger.info(`${requestID}: Error in '${key}': ${errorDescription[error]}`);
return {
type,
key,
value,
description: errorDescription[error]
};
}
jsLogger.info(`${requestID}: Error in '${key}': ${description}`);
return {
type,
key,
value,
description
};
};
const isStringTokenCorrect = (key, value, errors, extendedkeyTypes, requestID) => {
if ((extendedkeyTypes[key] === cmcdTypes.string || extendedkeyTypes[key] === cmcdTypes.token) && (typeof value === cmcdTypes.boolean || typeof value === cmcdTypes.number)) {
const description = `The value for the key ${key} must be a ${extendedkeyTypes[key]}.`;
errors.push(createError(errorTypes.wrongTypeValue, requestID, key, value, description));
return false;
}
return true;
};
const isBooleanCorrectJson = (key, value, errors, extendedkeyTypes, requestID) => {
if (extendedkeyTypes[key] === cmcdTypes.boolean && typeof value !== cmcdTypes.boolean) {
const description = `The value for the key ${key} must be a boolean.`;
errors.push(createError(errorTypes.wrongTypeValue, requestID, key, value, description));
return false;
}
return true;
};
const isNumberCorrectJson = (key, value, errors, extendedkeyTypes, requestID) => {
if (extendedkeyTypes[key] === cmcdTypes.number && typeof value !== cmcdTypes.number) {
const description = `The value for the key ${key} must be a number`;
errors.push(createError(errorTypes.wrongTypeValue, requestID, key, value, description));
return false;
}
return true;
};
const jsonIsValid = (jsonString, errors, requestID, config, extendedKeyTypes) => {
let valid = true;
const keyvalue = jsonString.split(',');
const keys = [];
// Check if there's double key
keyvalue.forEach(keyVal => {
var _config$specificKey;
const key = keyVal.split(':')[0].replace(/{|"/g, '');
if (config !== null && config !== void 0 && config.specificKey && !((_config$specificKey = config.specificKey) !== null && _config$specificKey !== void 0 && _config$specificKey.includes(key))) {
return;
}
if (keys.includes(key)) {
const description = `The key ${key} is repeated.`;
errors.push(createError(errorTypes.duplicateKey, requestID, key, undefined, description));
valid = false;
}
keys.push(key);
});
try {
const obj = JSON.parse(jsonString);
Object.keys(obj).forEach(key => {
var _config$specificKey2;
if (config !== null && config !== void 0 && config.specificKey && !((_config$specificKey2 = config.specificKey) !== null && _config$specificKey2 !== void 0 && _config$specificKey2.includes(key))) {
return;
}
if (!isBooleanCorrectJson(key, obj[key], errors, extendedKeyTypes, requestID) || !isNumberCorrectJson(key, obj[key], errors, extendedKeyTypes, requestID) || !isStringTokenCorrect(key, obj[key], errors, extendedKeyTypes, requestID)) {
valid = false;
}
});
} catch (error) {
errors.push(createError(errorTypes.invalidJson, requestID));
valid = false;
}
return valid;
};
const checkQuotes = value => value.indexOf('"') === 0 && value.lastIndexOf('"') === value.length - 1;
const isKeyInCorrectHeader = (header, key, errors, extendedcmcdHeader, requestID) => {
if (!extendedcmcdHeader[header].includes(key)) {
const description = `The key ${key} does not go under the header ${header}`;
errors.push(createError(errorTypes.incorrectFormat, requestID, key, description));
return false;
}
return true;
};
const isStringCorrect = (key, value, errors, extendedkeyTypes, requestID) => {
if (extendedkeyTypes[key] === cmcdTypes.string && !checkQuotes(value) || extendedkeyTypes[key] === cmcdTypes.token && checkQuotes(value)) {
const description = `Incorrect format for key: ${key}`;
errors.push(createError(errorTypes.incorrectFormat, requestID, key, value, description));
return false;
}
return true;
};
const isBooleanCorrect = (key, value, errors, extendedkeyTypes, requestID) => {
if (extendedkeyTypes[key] === cmcdTypes.boolean && value === 'true') {
const description = 'If the value is TRUE, the = and the value must be omitted';
errors.push(createError(errorTypes.incorrectFormat, requestID, key, value, description));
return false;
}
if ((typeof value === cmcdTypes.number || typeof value === cmcdTypes.string && value !== 'false') && extendedkeyTypes[key] === cmcdTypes.boolean) {
const description = `The value for the key ${key} must be a boolean.`;
errors.push(createError(errorTypes.wrongTypeValue, requestID, key, value, description));
return false;
}
return true;
};
const isNumberCorrect = (key, value, errors, extendedkeyTypes, requestID) => {
if (extendedkeyTypes[key] === cmcdTypes.number && Number.isNaN(parseInt(value, 10))) {
const description = `The value for the key ${key} must be a number`;
errors.push(createError(errorTypes.incorrectFormat, requestID, key, value, description));
return false;
}
return true;
};
const isSeparetedCorrectly = (keyVal, errors, extendedkeyTypes, requestID) => {
if (keyVal.split('=').length > 2 || keyVal.split('=').length === 1 && extendedkeyTypes[keyVal] !== cmcdTypes.boolean) {
const description = 'key-value pair not separated by =.';
errors.push(createError(errorTypes.incorrectFormat, requestID, keyVal, undefined, description));
return false;
}
return true;
};
const isKeyRepeated = (key, keys, errors, requestID) => {
if (keys.includes(key)) {
const description = `The key ${key} is repeated.`;
errors.push(createError(errorTypes.duplicateKey, requestID, key, undefined, description));
return true;
}
return false;
};
const isHeaderRepeated = (header, headers, errors, requestID) => {
if (headers.includes(header)) {
const description = `This header ${header} is repeated.`;
errors.push(createError(errorTypes.duplicatedHeader, requestID, header, description));
return true;
}
return false;
};
const isHeader = (headers, errors, requestID) => {
if (headers.length === 0) {
const description = 'No headers detected!';
errors.push(createError(errorTypes.noCMCDRequest, requestID, description));
return false;
}
return true;
};
const isEmptyHeader = (keyVal, header, errors, requestID) => {
if (keyVal === '') {
const description = `Empty header detected! Header: ${header}`;
errors.push(createError(errorTypes.emptyHeader, requestID, header, description));
return true;
}
return false;
};
const includesCMCDRequest = (queryString, errors, requestID) => {
if (!queryString.includes('CMCD=')) {
errors.push(createError(errorTypes.noCMCDRequest, requestID));
return false;
}
return true;
};
const isURLMalformed = (queryString, errors, requestID) => {
let valid = false;
try {
// Check if the URL is encoded
if (decodeURI(queryString) === queryString) {
errors.push(createError(errorTypes.parameterEncoding, requestID));
valid = true;
}
} catch (err) {
errors.push(createError(errorTypes.queryMalformed, requestID));
valid = true;
}
return valid;
};
const areRequestsSeparated = (requests, errors, requestID) => {
if (requests[0].length > 0 && requests[0][requests[0].length - 1] !== '&') {
errors.push(createError(errorTypes.noAmpersandBetweenRequests, requestID));
return false;
}
return true;
};
const multipleCMCDReq = (requests, errors, requestID) => {
if (requests.length > 1) {
errors.push(createError(errorTypes.incorrectFormat, requestID));
return true;
}
return false;
};
const queryValidator = (queryString, error, requestID, warnings, config, extendedKeyTypes) => {
// Check if there is a CMCD request in the queryString and Catch if the URL is malformed
if (!includesCMCDRequest(queryString, error, requestID) || isURLMalformed(queryString, error, requestID)) {
return false;
}
const query = queryString.split('?').pop();
const requests = decodeURIComponent(query).split('CMCD=');
// Check if there is another query before CMCD query and is missing a '&' separating them
if (!areRequestsSeparated(requests, error, requestID)) {
return false;
}
// Check if there is more than one CMCD request
requests.shift();
if (multipleCMCDReq(requests, error, requestID)) {
return false;
}
const values = decodeURIComponent(query).split('CMCD=')[1].split('&')[0].split(',');
const keys = [];
let valid = true;
// Check: key/value is separated by =
values.forEach(val => {
if (isSeparetedCorrectly(val, error, extendedKeyTypes, requestID)) {
var _config$specificKey;
const _val$split = val.split('='),
_val$split2 = _slicedToArray(_val$split, 2),
key = _val$split2[0],
value = _val$split2[1];
if (config !== null && config !== void 0 && config.specificKey && !((_config$specificKey = config.specificKey) !== null && _config$specificKey !== void 0 && _config$specificKey.includes(key))) {
return;
}
if (isKeyRepeated(key, keys, error, requestID)) {
valid = false;
}
// Check only the keys in the configuration
// Check: string require ""
if (!isStringCorrect(key, value, error, extendedKeyTypes, requestID) || !isBooleanCorrect(key, value, error, extendedKeyTypes, requestID) || !isNumberCorrect(key, value, error, extendedKeyTypes, requestID)) {
valid = false;
}
keys.push(key);
} else {
valid = false;
}
});
return valid;
};
const createWarning = (type, requestID, key, value) => {
jsLogger.useDefaults({
defaultLevel: jsLogger.TRACE
});
if (!Object.values(warningTypes).includes(type)) {
return -1;
}
const warning = getKeyByValue(warningTypes, type);
jsLogger.info(`${requestID}: Warning in '${key}': ${warningDescription[warning]}`);
return {
type,
key,
value,
description: warningDescription[warning]
};
};
const keySortedAlphabetically = (cmcdKeys, warnings, requestID) => {
// cmcdKeys must be an array
if (JSON.stringify(cmcdKeys) !== JSON.stringify(cmcdKeys.sort())) {
warnings.push(createWarning(warningTypes.noAlphabeticalOrder, requestID));
}
};
const headerValidator = (headerString, errors, requestID, warnings, config, extendedcmcdHeader, extendedKeyTypes, warningFlag = true) => {
const headers = headerString.split('\n');
const cmcdHeaders = [];
const keys = [];
let headerKeys = [];
let valid = true;
// We comment the following eslint error because we don't want to
// return any value in the foreach
// eslint-disable-next-line consistent-return
headers.forEach(element => {
const _element$split = element.split(': '),
_element$split2 = _slicedToArray(_element$split, 2),
header = _element$split2[0],
keysArray = _element$split2[1];
if (!(header in extendedcmcdHeader)) {
return false;
}
if (isHeaderRepeated(header, cmcdHeaders, errors, requestID) || isEmptyHeader(keysArray, header, errors, requestID)) {
valid = false;
return false;
}
keysArray.split(',').forEach(keyVal => {
if (isSeparetedCorrectly(keyVal, errors, extendedKeyTypes, requestID)) {
var _config$specificKey;
const _keyVal$split = keyVal.split('='),
_keyVal$split2 = _slicedToArray(_keyVal$split, 2),
key = _keyVal$split2[0],
value = _keyVal$split2[1];
if (config !== null && config !== void 0 && config.specificKey && !((_config$specificKey = config.specificKey) !== null && _config$specificKey !== void 0 && _config$specificKey.includes(key))) {
return;
}
if (isKeyRepeated(key, keys, errors, requestID) || !isKeyInCorrectHeader(header, key, errors, extendedcmcdHeader, requestID) || !isStringCorrect(key, value, errors, extendedKeyTypes, requestID) || !isBooleanCorrect(key, value, errors, extendedKeyTypes, requestID) || !isNumberCorrect(key, value, errors, extendedKeyTypes, requestID)) {
valid = false;
}
keys.push(key);
headerKeys.push(key);
} else {
valid = false;
}
});
if (warningFlag === true) {
keySortedAlphabetically(headerKeys, warnings, requestID);
headerKeys = [];
}
cmcdHeaders.push(header);
});
if (!isHeader(cmcdHeaders, errors, requestID)) {
return false;
}
return valid;
};
const checkMaxLength = (errors, key, value, requestID) => {
if (value.length > 64) {
const description = `Invalid value for key ${key}. Maximum length is 64 characters.`;
errors.push(createError(errorTypes.invalidValue, requestID, key, value, description));
}
};
const isEncoded = (errors, key, value, requestID) => {
if (decodeURIComponent(value) === value && encodeURIComponent(value) !== value) {
const description = `The key: ${key} must be URLencoded.`;
errors.push(createError(errorTypes.parameterEncoding, requestID, key, value, description));
return false;
}
return true;
};
const isRelativePath = (errors, key, value, requestID) => {
if (/^(http:\/\/|https:\/\/)/.test(decodeURIComponent(value))) {
const description = `The key: ${key} must be a relative path.`;
errors.push(createError(errorTypes.incorrectFormat, requestID, key, value, description));
return false;
}
return true;
};
const checkValidNrrFormat = (errors, key, value, requestID) => {
if (!/^(\d*-\d*)$/.test(value) && !/^\d*-$/.test(value) && !/^-\d*$/.test(value)) {
const description = 'Invalid Nrr format';
errors.push(createError(errorTypes.incorrectFormat, requestID, key, value, description));
}
};
const checkValidValue = (errors, key, value, array, requestID) => {
if (!array.includes(value)) {
const description = `${key} value does not meet the necessary requirements. Must be one of the following values: ${array}.`;
errors.push(createError(errorTypes.invalidValue, requestID, key, value, description));
}
};
const checkRoundToNearest = (errors, key, value, num, unit, requestID) => {
if (value % num !== 0) {
const description = `${key} value is not rounded to the nearest ${num}${unit}.`;
errors.push(createError(errorTypes.invalidValue, requestID, key, value, description));
}
};
const checkIgnoredParameter = (errors, key, value, exep, requestID) => {
if (value === exep) {
const description = `The ${key} key should not be sent if the value is ${exep}`;
errors.push(createError(errorTypes.unnecessaryKey, requestID, key, value, description));
}
};
const isReserved = (errors, requestID, key, value, extendedKeyTypes) => {
if (!(key in extendedKeyTypes)) {
const description = `The key ${key} is not reserved.`;
errors.push(createError(errorTypes.unknownKey, requestID, key, value, description));
}
};
const isPositive = (errors, key, value, requestID) => {
if (keyTypes[key] === cmcdTypes.number && value < 0) {
const description = `The ${key} value must be greater than 0.`;
errors.push(createError(errorTypes.invalidValue, requestID, key, value, description));
}
};
const checkBlKey = (cmcdJson, warnings, key, value, requestID) => {
if (!('ot' in cmcdJson) || !['a', 'v', 'av'].includes(cmcdJson.ot)) {
warnings.push(createWarning(warningTypes.blWithWrongOtValue, requestID, key, value));
}
};
const checkCorrectType = (errors, key, value, requestID) => {
if (typeof value !== keyTypes[key] && keyTypes[key] === cmcdTypes.token && typeof value !== cmcdTypes.string) {
const description = `${key} type is incorrect.`;
errors.push(createError(errorTypes.wrongTypeValue, requestID, key, value, description));
}
};
const checkOtValidValue = (errors, key, value, requestID) => {
checkValidValue(errors, key, value, ['m', 'a', 'v', 'av', 'i', 'c', 'tt', 'k', 'o'], requestID);
};
const checkSfValidValue = (errors, key, value, requestID) => {
checkValidValue(errors, key, value, ['d', 'h', 's', 'o'], requestID);
};
const checkStValidValue = (errors, key, value, requestID) => {
checkValidValue(errors, key, value, ['v', 'l'], requestID);
};
const checkPrValue = (cmcdJson, warnings, key, value, requestID) => {
if (key === 'pr' && value === 1) {
warnings.push(createWarning(warningTypes.valuePr, requestID, key, value));
}
};
const checkVValue = (cmcdJson, warnings, key, value, requestID) => {
if (key === 'v' && cmcdJson.v === 1) {
warnings.push(createWarning(warningTypes.valueV, requestID, key, value));
}
};
const checkSidIsPresent = (cmcdJson, warnings, requestID) => {
if (!('sid' in cmcdJson)) {
warnings.push(createWarning(warningTypes.noSidReceived, requestID));
}
};
// keyValValidator takes as a parameter cmcdJson, which is a javascript object.
// The function iterates through it validating every key value pair.
// eslint-disable-next-line max-len
const keyValValidator = (cmcdJson, errors, requestID, warnings, config, extendedKeyTypes, warningFlag) => {
if (warningFlag === true) {
checkSidIsPresent(cmcdJson, warnings, requestID);
}
// Check if all the specifickey have been received in the request
const keysReceived = Object.keys(cmcdJson);
if (config !== null && config !== void 0 && config.specificKey && warningFlag) {
config.specificKey.forEach(key => {
if (!keysReceived.includes(key)) {
warnings.push(createWarning(warningTypes.specificKeysNotSent, requestID, key));
}
});
}
if (config !== null && config !== void 0 && config.customKey && warningFlag) {
config.customKey.forEach(cKey => {
if (!keysReceived.includes(cKey.key)) {
warnings.push(createWarning(warningTypes.specificKeysNotSent, requestID, cKey.key));
}
});
}
Object.keys(cmcdJson).forEach(key => {
var _config$specificKey;
// Check if we received a configuration
if (config !== null && config !== void 0 && config.specificKey && !((_config$specificKey = config.specificKey) !== null && _config$specificKey !== void 0 && _config$specificKey.includes(key))) {
return;
}
const keyValue = cmcdJson[key];
isReserved(errors, requestID, key, keyValue, extendedKeyTypes);
checkCorrectType(errors, key, keyValue, requestID);
isPositive(errors, key, keyValue, requestID);
switch (key) {
case 'bl':
checkRoundToNearest(errors, key, keyValue, 100, 'ms', requestID);
if (warningFlag === true) {
checkBlKey(cmcdJson, warnings, key, keyValue, requestID);
}
break;
case 'cid':
checkMaxLength(errors, key, keyValue, requestID);
break;
case 'dl':
checkRoundToNearest(errors, key, keyValue, 100, 'ms', requestID);
break;
case 'mtp':
checkIgnoredParameter(errors, key, keyValue, 0, requestID);
checkRoundToNearest(errors, key, keyValue, 100, 'kbps', requestID);
break;
case 'nor':
if (isEncoded(errors, key, keyValue, requestID)) {
isRelativePath(errors, key, keyValue, requestID);
}
break;
case 'nrr':
checkValidNrrFormat(errors, key, keyValue, requestID);
break;
case 'ot':
checkOtValidValue(errors, key, keyValue, requestID);
break;
case 'rtp':
checkRoundToNearest(errors, key, keyValue, 100, 'kbps', requestID);
break;
case 'sf':
checkSfValidValue(errors, key, keyValue, requestID);
break;
case 'sid':
checkMaxLength(errors, key, keyValue, requestID);
break;
case 'st':
checkStValidValue(errors, key, keyValue, requestID);
break;
case 'su':
checkIgnoredParameter(errors, key, keyValue, false, requestID);
break;
case 'bs':
checkIgnoredParameter(errors, key, keyValue, false, requestID);
break;
case 'pr':
if (warningFlag) {
checkPrValue(cmcdJson, warnings, key, keyValue, requestID);
}
break;
case 'v':
if (warningFlag) {
checkVValue(cmcdJson, warnings, key, keyValue, requestID);
}
break;
}
});
};
const parseQueryToJson = (queryString, extendedKeyTypes) => {
const values = decodeURIComponent(queryString).split('CMCD=')[1].split('&')[0].split(',');
const obj = {};
values.forEach(value => {
if (!value.includes('=')) {
obj[value] = true;
} else {
// eslint-disable-next-line prefer-const
let _value$split = value.split('='),
_value$split2 = _slicedToArray(_value$split, 2),
key = _value$split2[0],
val = _value$split2[1];
val = Number.isNaN(Number(val)) ? val.replace(/"/g, '') : Number(val);
if (extendedKeyTypes[key] === 'boolean' && val === 'false') val = false;
obj[key] = val;
}
});
return obj;
};
const parseHeaderToJSON = (headerString, extendedcmcdHeader, extendedKeyTypes) => {
const pairs = headerString.split('\n');
const result = {};
pairs.forEach(pair => {
// eslint-disable-next-line prefer-const
let _pair$split = pair.split(':'),
_pair$split2 = _slicedToArray(_pair$split, 2),
key = _pair$split2[0],
value = _pair$split2[1];
if (!extendedcmcdHeader[key]) {
return;
}
value = value.replace(/ /g, '');
const subPairs = value.split(',');
subPairs.forEach(subPair => {
if (!subPair.includes('=')) {
result[subPair] = true;
} else {
// eslint-disable-next-line prefer-const
let _subPair$split = subPair.split('='),
_subPair$split2 = _slicedToArray(_subPair$split, 2),
subKey = _subPair$split2[0],
subValue = _subPair$split2[1];
subValue = Number.isNaN(Number(subValue)) ? subValue.replace(/"/g, '') : Number(subValue);
if (extendedKeyTypes[subKey] === 'boolean' && subValue === 'false') subValue = false;
result[subKey] = subValue;
}
});
});
return result;
};
const setConfig = (config, errors, requestID, warnings, warningFlag = true) => {
if (!config) {
return [true, keyTypes, cmcdHeader];
}
const customKey = config.customKey,
specificKey = config.specificKey;
let errorsCheck = false;
const extendedKeyTypes = _objectSpread2({}, keyTypes);
const extendedcmcdHeader = _objectSpread2({}, cmcdHeader);
if (customKey) {
const types = Object.values(cmcdTypes);
customKey.forEach(customObj => {
if (!/^[a-zA-Z0-9.]+-[a-zA-Z0-9]+$/.test(customObj.key)) {
errors.push(createError(errorTypes.invalidCustomKey, requestID, customObj.key));
errorsCheck = true;
}
if (!types.includes(customObj.type)) {
errors.push(createError(errorTypes.wrongCustomType, requestID, customObj.key, customObj.type));
errorsCheck = true;
}
if (!/^([a-zA-Z0-9]+\.[a-zA-Z0-9]+)+$/.test(customObj.key.split('-')[0]) && warningFlag === true) {
warnings.push(createWarning(warningTypes.noReverseDnsCustomKey, requestID));
}
if (customObj.headerType && !(customObj.headerType in cmcdHeader)) {
errors.push(createError(errorTypes.invalidHeader, requestID, customObj.key, customObj.type));
errorsCheck = true;
}
if (!errorsCheck) {
extendedKeyTypes[customObj.key] = customObj.type;
if (customObj.headerType) {
extendedcmcdHeader[customObj.headerType].push(customObj.key);
}
}
});
}
if (specificKey) {
const cmcdKeyTypes = Object.keys(extendedKeyTypes);
specificKey.forEach(key => {
if (!cmcdKeyTypes.includes(key)) {
errors.push(createError(errorTypes.unknownSpecificKey, requestID, key));
}
});
}
if (errors.length === 0) {
return [true, extendedKeyTypes, extendedcmcdHeader];
}
return [false, extendedKeyTypes, extendedcmcdHeader];
};
const createOutput = (errors, warnings, rawData, parsedData) => {
const response = {
valid: errors.length === 0,
errors,
warnings,
parsedData,
rawData
};
return response;
};
const CMCDQueryValidator = (query, config, warningFlag = true) => {
const errors = [];
const rawData = query;
const warnings = [];
const requestID = v4();
jsLogger.useDefaults({
defaultLevel: jsLogger.TRACE
});
jsLogger.info(`${requestID}: Started CMCD Query Validation.`);
const _setConfig = setConfig(config, errors, requestID, warnings, warningFlag),
_setConfig2 = _slicedToArray(_setConfig, 2),
validConfig = _setConfig2[0],
extendedKeyTypes = _setConfig2[1];
// check config
jsLogger.info(`${requestID}: Check Configuration.`);
if (!validConfig) {
jsLogger.info(`${requestID}: Configuration not valid.`);
return createOutput(errors, warnings, rawData);
}
// Check query
jsLogger.info(`${requestID}: Validating query format.`);
const valid = queryValidator(query, errors, requestID, warnings, config, extendedKeyTypes);
if (!valid) {
jsLogger.info(`${requestID}: Query not valid.`);
return createOutput(errors, warnings, rawData);
}
jsLogger.info(`${requestID}: Query is valid.`);
// Parsed to json
jsLogger.info(`${requestID}: Parsing query.`);
const parsedData = parseQueryToJson(query, extendedKeyTypes);
if (warningFlag === true) {
keySortedAlphabetically(Object.keys(parsedData), warnings);
}
// Check key value
jsLogger.info(`${requestID}: Validating query keys.`);
keyValValidator(parsedData, errors, requestID, warnings, config, extendedKeyTypes, warningFlag);
return createOutput(errors, warnings, rawData, parsedData);
};
const CMCDJsonValidator = (jsonString, config, warningFlag = true) => {
const errors = [];
const rawData = jsonString;
const warnings = [];
const requestID = v4();
jsLogger.useDefaults({
defaultLevel: jsLogger.TRACE
});
jsLogger.info(`${requestID}: Started CMCD Json Validation.`);
const _setConfig = setConfig(config, errors, requestID, warnings, warningFlag),
_setConfig2 = _slicedToArray(_setConfig, 2),
validConfig = _setConfig2[0],
extendedKeyTypes = _setConfig2[1];
// check config
if (!validConfig) {
jsLogger.info(`${requestID}: Configuration is not valid.`);
return createOutput(errors, warnings, rawData);
}
jsLogger.info(`${requestID}: Configuration es valid.`);
// Check json
jsLogger.info(`${requestID}: Validating Json format.`);
const valid = jsonIsValid(jsonString, errors, requestID, config, extendedKeyTypes);
if (!valid) {
jsLogger.info(`${requestID}: Json not valid.`);
return createOutput(errors, warnings, rawData);
}
jsLogger.info(`${requestID}: Json is valid.`);
const jsonObj = JSON.parse(jsonString);
if (warningFlag === true) {
keySortedAlphabetically(Object.keys(jsonObj), warnings, requestID);
}
// Check key value
jsLogger.info(`${requestID}: Validating Json keys.`);
keyValValidator(jsonObj, errors, requestID, warnings, config, extendedKeyTypes, warningFlag);
return createOutput(errors, warnings, rawData, jsonObj);
};
const CMCDHeaderValidator = (header, config, warningFlag = true) => {
const errors = [];
const rawData = header;
const warnings = [];
const requestID = v4();
jsLogger.useDefaults({
defaultLevel: jsLogger.TRACE
});
jsLogger.info(`${requestID}: Started CMCD Header Validation.`);
const _setConfig = setConfig(config, errors, requestID, warnings, warningFlag),
_setConfig2 = _slicedToArray(_setConfig, 3),
validConfig = _setConfig2[0],
extendedKeyTypes = _setConfig2[1],
extendedcmcdHeader = _setConfig2[2];
// check config
if (!validConfig) {
return createOutput(errors, warnings, rawData);
}
// Check header
jsLogger.info(`${requestID}: Validating header format.`);
const valid = headerValidator(header, errors, requestID, warnings, config, extendedcmcdHeader, extendedKeyTypes, warningFlag);
if (!valid) {
jsLogger.info(`${requestID}: Header not valid.`);
return createOutput(errors, warnings, rawData);
}
jsLogger.info(`${requestID}: Header is valid.`);
// Parsed to json
jsLogger.info(`${requestID}: Parsing header.`);
const parsedData = parseHeaderToJSON(header, extendedcmcdHeader, extendedKeyTypes);
// Check key value
keyValValidator(parsedData, errors, requestID, warnings, config, extendedKeyTypes, warningFlag);
return createOutput(errors, warnings, rawData, parsedData);
};
export { CMCDHeaderValidator, CMCDJsonValidator, CMCDQueryValidator };