2020-05-19 11:43:42 +03:00

1578 lines
44 KiB
JavaScript

/*
* This module is intended to be executed both on client side and server side.
* No error should be thrown. (soft error handling)
*/
(function () {
var root = {};
// Dependencies --------------------------------------------------------------
root.async = (typeof require === 'function') ? require('async') : window.async;
if (typeof root.async !== 'object') {
throw new Error('Module async is required (https://github.com/caolan/async)');
}
var async = root.async;
function _extend(origin, add) {
if (!add || typeof add !== 'object') {
return origin;
}
var keys = Object.keys(add);
var i = keys.length;
while (i--) {
origin[keys[i]] = add[keys[i]];
}
return origin;
}
function _merge() {
var ret = {};
var args = Array.prototype.slice.call(arguments);
var keys = null;
var i = null;
args.forEach(function (arg) {
if (arg && arg.constructor === Object) {
keys = Object.keys(arg);
i = keys.length;
while (i--) {
ret[keys[i]] = arg[keys[i]];
}
}
});
return ret;
}
// Customisable class (Base class) -------------------------------------------
// Use with operation "new" to extend Validation and Sanitization themselves,
// not their prototype. In other words, constructor shall be call to extend
// those functions, instead of being in their constructor, like this:
// _extend(Validation, new Customisable);
function Customisable() {
this.custom = {};
this.extend = function (custom) {
return _extend(this.custom, custom);
};
this.reset = function () {
this.custom = {};
};
this.remove = function (fields) {
if (!_typeIs.array(fields)) {
fields = [fields];
}
fields.forEach(function (field) {
delete this.custom[field];
}, this);
};
}
// Inspection class (Base class) ---------------------------------------------
// Use to extend Validation and Sanitization prototypes. Inspection
// constructor shall be called in derived class constructor.
function Inspection(schema, custom) {
var _stack = ['@'];
this._schema = schema;
this._custom = {};
if (custom != null) {
for (var key in custom) {
if (Object.prototype.hasOwnProperty.call(custom, key)) {
this._custom['$' + key] = custom[key];
}
}
}
this._getDepth = function () {
return _stack.length;
};
this._dumpStack = function () {
return _stack.map(function (i) {return i.replace(/^\[/g, '\u001b\u001c\u001d\u001e');})
.join('.').replace(/\.\u001b\u001c\u001d\u001e/g, '[');
};
this._deeperObject = function (name) {
_stack.push((/^[a-z$_][a-z0-9$_]*$/i).test(name) ? name : '["' + name + '"]');
return this;
};
this._deeperArray = function (i) {
_stack.push('[' + i + ']');
return this;
};
this._back = function () {
_stack.pop();
return this;
};
}
// Simple types --------------------------------------------------------------
// If the property is not defined or is not in this list:
var _typeIs = {
"function": function (element) {
return typeof element === 'function';
},
"string": function (element) {
return typeof element === 'string';
},
"number": function (element) {
return typeof element === 'number' && !isNaN(element);
},
"integer": function (element) {
return typeof element === 'number' && element % 1 === 0;
},
"NaN": function (element) {
return typeof element === 'number' && isNaN(element);
},
"boolean": function (element) {
return typeof element === 'boolean';
},
"null": function (element) {
return element === null;
},
"date": function (element) {
return element != null && element instanceof Date;
},
"object": function (element) {
return element != null && element.constructor === Object;
},
"array": function (element) {
return element != null && element.constructor === Array;
},
"any": function (element) {
return true;
}
};
function _simpleType(type, candidate) {
if (typeof type == 'function') {
return candidate instanceof type;
}
type = type in _typeIs ? type : 'any';
return _typeIs[type](candidate);
}
function _realType(candidate) {
for (var i in _typeIs) {
if (_simpleType(i, candidate)) {
if (i !== 'any') { return i; }
return 'an instance of ' + candidate.constructor.name;
}
}
}
function getIndexes(a, value) {
var indexes = [];
var i = a.indexOf(value);
while (i !== -1) {
indexes.push(i);
i = a.indexOf(value, i + 1);
}
return indexes;
}
// Available formats ---------------------------------------------------------
var _formats = {
'void': /^$/,
'url': /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)?(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i,
'date-time': /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z?|(-|\+)\d{2}:\d{2})$/,
'date': /^\d{4}-\d{2}-\d{2}$/,
'coolDateTime': /^\d{4}(-|\/)\d{2}(-|\/)\d{2}(T| )\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/,
'time': /^\d{2}\:\d{2}\:\d{2}$/,
'color': /^#([0-9a-f])+$/i,
'email': /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i,
'numeric': /^[0-9]+$/,
'integer': /^\-?[0-9]+$/,
'decimal': /^\-?[0-9]*\.?[0-9]+$/,
'alpha': /^[a-z]+$/i,
'alphaNumeric': /^[a-z0-9]+$/i,
'alphaDash': /^[a-z0-9_-]+$/i,
'javascript': /^[a-z_\$][a-z0-9_\$]*$/i,
'upperString': /^[A-Z ]*$/,
'lowerString': /^[a-z ]*$/
};
// Validation ------------------------------------------------------------------
var _validationAttribut = {
optional: function (schema, candidate) {
var opt = typeof schema.optional === 'boolean' ? schema.optional : (schema.optional === 'true'); // Default is false
if (opt === true) {
return;
}
if (typeof candidate === 'undefined') {
this.report('is missing and not optional', null, 'optional');
}
},
type: function (schema, candidate) {
// return because optional function already handle this case
if (typeof candidate === 'undefined' || (typeof schema.type !== 'string' && !(schema.type instanceof Array) && typeof schema.type !== 'function')) {
return;
}
var types = _typeIs.array(schema.type) ? schema.type : [schema.type];
var typeIsValid = types.some(function (type) {
return _simpleType(type, candidate);
});
if (!typeIsValid) {
types = types.map(function (t) {return typeof t === 'function' ? 'and instance of ' + t.name : t; });
this.report('must be ' + types.join(' or ') + ', but is ' + _realType(candidate), null, 'type');
}
},
uniqueness: function (schema, candidate) {
if (typeof schema.uniqueness === 'string') { schema.uniqueness = (schema.uniqueness === 'true'); }
if (typeof schema.uniqueness !== 'boolean' || schema.uniqueness === false || (!_typeIs.array(candidate) && typeof candidate !== 'string')) {
return;
}
var reported = [];
for (var i = 0; i < candidate.length; i++) {
if (reported.indexOf(candidate[i]) >= 0) {
continue;
}
var indexes = getIndexes(candidate, candidate[i]);
if (indexes.length > 1) {
reported.push(candidate[i]);
this.report('has value [' + candidate[i] + '] more than once at indexes [' + indexes.join(', ') + ']', null, 'uniqueness');
}
}
},
pattern: function (schema, candidate) {
var self = this;
var regexs = schema.pattern;
if (typeof candidate !== 'string') {
return;
}
var matches = false;
if (!_typeIs.array(regexs)) {
regexs = [regexs];
}
regexs.forEach(function (regex) {
if (typeof regex === 'string' && regex in _formats) {
regex = _formats[regex];
}
if (regex instanceof RegExp) {
if (regex.test(candidate)) {
matches = true;
}
}
});
if (!matches) {
self.report('must match [' + regexs.join(' or ') + '], but is equal to "' + candidate + '"', null, 'pattern');
}
},
validDate: function (schema, candidate) {
if (String(schema.validDate) === 'true' && candidate instanceof Date && isNaN(candidate.getTime())) {
this.report('must be a valid date', null, 'validDate');
}
},
minLength: function (schema, candidate) {
if (typeof candidate !== 'string' && !_typeIs.array(candidate)) {
return;
}
var minLength = Number(schema.minLength);
if (isNaN(minLength)) {
return;
}
if (candidate.length < minLength) {
this.report('must be longer than ' + minLength + ' elements, but it has ' + candidate.length, null, 'minLength');
}
},
maxLength: function (schema, candidate) {
if (typeof candidate !== 'string' && !_typeIs.array(candidate)) {
return;
}
var maxLength = Number(schema.maxLength);
if (isNaN(maxLength)) {
return;
}
if (candidate.length > maxLength) {
this.report('must be shorter than ' + maxLength + ' elements, but it has ' + candidate.length, null, 'maxLength');
}
},
exactLength: function (schema, candidate) {
if (typeof candidate !== 'string' && !_typeIs.array(candidate)) {
return;
}
var exactLength = Number(schema.exactLength);
if (isNaN(exactLength)) {
return;
}
if (candidate.length !== exactLength) {
this.report('must have exactly ' + exactLength + ' elements, but it have ' + candidate.length, null, 'exactLength');
}
},
lt: function (schema, candidate) {
var limit = Number(schema.lt);
if (typeof candidate !== 'number' || isNaN(limit)) {
return;
}
if (candidate >= limit) {
this.report('must be less than ' + limit + ', but is equal to "' + candidate + '"', null, 'lt');
}
},
lte: function (schema, candidate) {
var limit = Number(schema.lte);
if (typeof candidate !== 'number' || isNaN(limit)) {
return;
}
if (candidate > limit) {
this.report('must be less than or equal to ' + limit + ', but is equal to "' + candidate + '"', null, 'lte');
}
},
gt: function (schema, candidate) {
var limit = Number(schema.gt);
if (typeof candidate !== 'number' || isNaN(limit)) {
return;
}
if (candidate <= limit) {
this.report('must be greater than ' + limit + ', but is equal to "' + candidate + '"', null, 'gt');
}
},
gte: function (schema, candidate) {
var limit = Number(schema.gte);
if (typeof candidate !== 'number' || isNaN(limit)) {
return;
}
if (candidate < limit) {
this.report('must be greater than or equal to ' + limit + ', but is equal to "' + candidate + '"', null, 'gte');
}
},
eq: function (schema, candidate) {
if (typeof candidate !== 'number' && typeof candidate !== 'string' && typeof candidate !== 'boolean') {
return;
}
var limit = schema.eq;
if (typeof limit !== 'number' && typeof limit !== 'string' && typeof limit !== 'boolean' && !_typeIs.array(limit)) {
return;
}
if (_typeIs.array(limit)) {
for (var i = 0; i < limit.length; i++) {
if (candidate === limit[i]) {
return;
}
}
this.report('must be equal to [' + limit.map(function (l) {
return '"' + l + '"';
}).join(' or ') + '], but is equal to "' + candidate + '"', null, 'eq');
}
else {
if (candidate !== limit) {
this.report('must be equal to "' + limit + '", but is equal to "' + candidate + '"', null, 'eq');
}
}
},
ne: function (schema, candidate) {
if (typeof candidate !== 'number' && typeof candidate !== 'string') {
return;
}
var limit = schema.ne;
if (typeof limit !== 'number' && typeof limit !== 'string' && !_typeIs.array(limit)) {
return;
}
if (_typeIs.array(limit)) {
for (var i = 0; i < limit.length; i++) {
if (candidate === limit[i]) {
this.report('must not be equal to "' + limit[i] + '"', null, 'ne');
return;
}
}
}
else {
if (candidate === limit) {
this.report('must not be equal to "' + limit + '"', null, 'ne');
}
}
},
someKeys: function (schema, candidat) {
var _keys = schema.someKeys;
if (!_typeIs.object(candidat)) {
return;
}
var valid = _keys.some(function (action) {
return (action in candidat);
});
if (!valid) {
this.report('must have at least key ' + _keys.map(function (i) {
return '"' + i + '"';
}).join(' or '), null, 'someKeys');
}
},
strict: function (schema, candidate) {
if (typeof schema.strict === 'string') { schema.strict = (schema.strict === 'true'); }
if (schema.strict !== true || !_typeIs.object(candidate) || !_typeIs.object(schema.properties)) {
return;
}
var self = this;
if (typeof schema.properties['*'] === 'undefined') {
var intruder = Object.keys(candidate).filter(function (key) {
return (typeof schema.properties[key] === 'undefined');
});
if (intruder.length > 0) {
var msg = 'should not contains ' + (intruder.length > 1 ? 'properties' : 'property') +
' [' + intruder.map(function (i) { return '"' + i + '"'; }).join(', ') + ']';
self.report(msg, null, 'strict');
}
}
},
exec: function (schema, candidate, callback) {
var self = this;
if (typeof callback === 'function') {
return this.asyncExec(schema, candidate, callback);
}
(_typeIs.array(schema.exec) ? schema.exec : [schema.exec]).forEach(function (exec) {
if (typeof exec === 'function') {
exec.call(self, schema, candidate);
}
});
},
properties: function (schema, candidate, callback) {
if (typeof callback === 'function') {
return this.asyncProperties(schema, candidate, callback);
}
if (!(schema.properties instanceof Object) || !(candidate instanceof Object)) {
return;
}
var properties = schema.properties,
i;
if (properties['*'] != null) {
for (i in candidate) {
if (i in properties) {
continue;
}
this._deeperObject(i);
this._validate(properties['*'], candidate[i]);
this._back();
}
}
for (i in properties) {
if (i === '*') {
continue;
}
this._deeperObject(i);
this._validate(properties[i], candidate[i]);
this._back();
}
},
items: function (schema, candidate, callback) {
if (typeof callback === 'function') {
return this.asyncItems(schema, candidate, callback);
}
if (!(schema.items instanceof Object) || !(candidate instanceof Object)) {
return;
}
var items = schema.items;
var i, l;
// If provided schema is an array
// then call validate for each case
// else it is an Object
// then call validate for each key
if (_typeIs.array(items) && _typeIs.array(candidate)) {
for (i = 0, l = items.length; i < l; i++) {
this._deeperArray(i);
this._validate(items[i], candidate[i]);
this._back();
}
}
else {
for (var key in candidate) {
if (Object.prototype.hasOwnProperty.call(candidate, key)) {
this._deeperArray(key);
this._validate(items, candidate[key]);
this._back();
}
}
}
}
};
var _asyncValidationAttribut = {
asyncExec: function (schema, candidate, callback) {
var self = this;
async.eachSeries(_typeIs.array(schema.exec) ? schema.exec : [schema.exec], function (exec, done) {
if (typeof exec === 'function') {
if (exec.length > 2) {
return exec.call(self, schema, candidate, done);
}
exec.call(self, schema, candidate);
}
async.nextTick(done);
}, callback);
},
asyncProperties: function (schema, candidate, callback) {
if (!(schema.properties instanceof Object) || !_typeIs.object(candidate)) {
return callback();
}
var self = this;
var properties = schema.properties;
async.series([
function (next) {
if (properties['*'] == null) {
return next();
}
async.eachSeries(Object.keys(candidate), function (i, done) {
if (i in properties) {
return async.nextTick(done);
}
self._deeperObject(i);
self._asyncValidate(properties['*'], candidate[i], function (err) {
self._back();
done(err);
});
}, next);
},
function (next) {
async.eachSeries(Object.keys(properties), function (i, done) {
if (i === '*') {
return async.nextTick(done);
}
self._deeperObject(i);
self._asyncValidate(properties[i], candidate[i], function (err) {
self._back();
done(err);
});
}, next);
}
], callback);
},
asyncItems: function (schema, candidate, callback) {
if (!(schema.items instanceof Object) || !(candidate instanceof Object)) {
return callback();
}
var self = this;
var items = schema.items;
var i, l;
if (_typeIs.array(items) && _typeIs.array(candidate)) {
async.timesSeries(items.length, function (i, done) {
self._deeperArray(i);
self._asyncValidate(items[i], candidate[i], function (err, res) {
self._back();
done(err, res);
});
self._back();
}, callback);
}
else {
async.eachSeries(Object.keys(candidate), function (key, done) {
self._deeperArray(key);
self._asyncValidate(items, candidate[key], function (err, res) {
self._back();
done(err, res);
});
}, callback);
}
}
};
// Validation Class ----------------------------------------------------------
// inherits from Inspection class (actually we just call Inspection
// constructor with the new context, because its prototype is empty
function Validation(schema, custom) {
Inspection.prototype.constructor.call(this, schema, _merge(Validation.custom, custom));
var _error = [];
this._basicFields = Object.keys(_validationAttribut);
this._customFields = Object.keys(this._custom);
this.origin = null;
this.report = function (message, code, reason) {
var newErr = {
code: code || this.userCode || null,
reason: reason || 'unknown',
message: this.userError || message || 'is invalid',
property: this.userAlias ? (this.userAlias + ' (' + this._dumpStack() + ')') : this._dumpStack()
};
_error.push(newErr);
return this;
};
this.result = function () {
return {
error: _error,
valid: _error.length === 0,
format: function () {
if (this.valid === true) {
return 'Candidate is valid';
}
return this.error.map(function (i) {
return 'Property ' + i.property + ': ' + i.message;
}).join('\n');
}
};
};
}
_extend(Validation.prototype, _validationAttribut);
_extend(Validation.prototype, _asyncValidationAttribut);
_extend(Validation, new Customisable());
Validation.prototype.validate = function (candidate, callback) {
this.origin = candidate;
if (typeof callback === 'function') {
var self = this;
return async.nextTick(function () {
self._asyncValidate(self._schema, candidate, function (err) {
self.origin = null;
callback(err, self.result());
});
});
}
return this._validate(this._schema, candidate).result();
};
Validation.prototype._validate = function (schema, candidate, callback) {
this.userCode = schema.code || null;
this.userError = schema.error || null;
this.userAlias = schema.alias || null;
this._basicFields.forEach(function (i) {
if ((i in schema || i === 'optional') && typeof this[i] === 'function') {
this[i](schema, candidate);
}
}, this);
this._customFields.forEach(function (i) {
if (i in schema && typeof this._custom[i] === 'function') {
this._custom[i].call(this, schema, candidate);
}
}, this);
return this;
};
Validation.prototype._asyncValidate = function (schema, candidate, callback) {
var self = this;
this.userCode = schema.code || null;
this.userError = schema.error || null;
this.userAlias = schema.alias || null;
async.series([
function (next) {
async.eachSeries(Object.keys(_validationAttribut), function (i, done) {
async.nextTick(function () {
if ((i in schema || i === 'optional') && typeof self[i] === 'function') {
if (self[i].length > 2) {
return self[i](schema, candidate, done);
}
self[i](schema, candidate);
}
done();
});
}, next);
},
function (next) {
async.eachSeries(Object.keys(self._custom), function (i, done) {
async.nextTick(function () {
if (i in schema && typeof self._custom[i] === 'function') {
if (self._custom[i].length > 2) {
return self._custom[i].call(self, schema, candidate, done);
}
self._custom[i].call(self, schema, candidate);
}
done();
});
}, next);
}
], callback);
};
// Sanitization ----------------------------------------------------------------
// functions called by _sanitization.type method.
var _forceType = {
number: function (post, schema) {
var n;
if (typeof post === 'number') {
return post;
}
else if (post === '') {
if (typeof schema.def !== 'undefined')
return schema.def;
return null;
}
else if (typeof post === 'string') {
n = parseFloat(post.replace(/,/g, '.').replace(/ /g, ''));
if (typeof n === 'number') {
return n;
}
}
else if (post instanceof Date) {
return +post;
}
return null;
},
integer: function (post, schema) {
var n;
if (typeof post === 'number' && post % 1 === 0) {
return post;
}
else if (post === '') {
if (typeof schema.def !== 'undefined')
return schema.def;
return null;
}
else if (typeof post === 'string') {
n = parseInt(post.replace(/ /g, ''), 10);
if (typeof n === 'number') {
return n;
}
}
else if (typeof post === 'number') {
return parseInt(post, 10);
}
else if (typeof post === 'boolean') {
if (post) { return 1; }
return 0;
}
else if (post instanceof Date) {
return +post;
}
return null;
},
string: function (post, schema) {
if (typeof post === 'boolean' || typeof post === 'number' || post instanceof Date) {
return post.toString();
}
else if (_typeIs.array(post)) {
// If user authorize array and strings...
if (schema.items || schema.properties)
return post;
return post.join(String(schema.joinWith || ','));
}
else if (post instanceof Object) {
// If user authorize objects ans strings...
if (schema.items || schema.properties)
return post;
return JSON.stringify(post);
}
else if (typeof post === 'string' && post.length) {
return post;
}
return null;
},
date: function (post, schema) {
if (post instanceof Date) {
return post;
}
else {
var d = new Date(post);
if (!isNaN(d.getTime())) { // if valid date
return d;
}
}
return null;
},
boolean: function (post, schema) {
if (typeof post === 'undefined') return null;
if (typeof post === 'string' && post.toLowerCase() === 'false') return false;
return !!post;
},
object: function (post, schema) {
if (typeof post !== 'string' || _typeIs.object(post)) {
return post;
}
try {
return JSON.parse(post);
}
catch (e) {
return null;
}
},
array: function (post, schema) {
if (_typeIs.array(post))
return post;
if (typeof post === 'undefined')
return null;
if (typeof post === 'string') {
if (post.substring(0,1) === '[' && post.slice(-1) === ']') {
try {
return JSON.parse(post);
}
catch (e) {
return null;
}
}
return post.split(String(schema.splitWith || ','));
}
if (!_typeIs.array(post))
return [ post ];
return null;
}
};
var _applyRules = {
upper: function (post) {
return post.toUpperCase();
},
lower: function (post) {
return post.toLowerCase();
},
title: function (post) {
// Fix by seb (replace \w\S* by \S* => exemple : coucou ça va)
return post.replace(/\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
},
capitalize: function (post) {
return post.charAt(0).toUpperCase() + post.substr(1).toLowerCase();
},
ucfirst: function (post) {
return post.charAt(0).toUpperCase() + post.substr(1);
},
trim: function (post) {
return post.trim();
}
};
// Every function return the future value of each property. Therefore you
// have to return post even if you do not change its value
var _sanitizationAttribut = {
strict: function (schema, post) {
if (typeof schema.strict === 'string') { schema.strict = (schema.strict === 'true'); }
if (schema.strict !== true)
return post;
if (!_typeIs.object(schema.properties))
return post;
if (!_typeIs.object(post))
return post;
var that = this;
Object.keys(post).forEach(function (key) {
if (!(key in schema.properties)) {
delete post[key];
}
});
return post;
},
optional: function (schema, post) {
var opt = typeof schema.optional === 'boolean' ? schema.optional : (schema.optional !== 'false'); // Default: true
if (opt === true) {
return post;
}
if (typeof post !== 'undefined') {
return post;
}
this.report();
if (schema.def === Date) {
return new Date();
}
return schema.def;
},
type: function (schema, post) {
// if (_typeIs['object'](post) || _typeIs.array(post)) {
// return post;
// }
if (typeof schema.type !== 'string' || typeof _forceType[schema.type] !== 'function') {
return post;
}
var n;
var opt = typeof schema.optional === 'boolean' ? schema.optional : true;
if (typeof _forceType[schema.type] === 'function') {
n = _forceType[schema.type](post, schema);
if ((n === null && !opt) || (!n && isNaN(n)) || (n === null && schema.type === 'string')) {
n = schema.def;
}
}
else if (!opt) {
n = schema.def;
}
if ((n != null || (typeof schema.def !== 'undefined' && schema.def === n)) && n !== post) {
this.report();
return n;
}
return post;
},
rules: function (schema, post) {
var rules = schema.rules;
if (typeof post !== 'string' || (typeof rules !== 'string' && !_typeIs.array(rules))) {
return post;
}
var modified = false;
(_typeIs.array(rules) ? rules : [rules]).forEach(function (rule) {
if (typeof _applyRules[rule] === 'function') {
post = _applyRules[rule](post);
modified = true;
}
});
if (modified) {
this.report();
}
return post;
},
min: function (schema, post) {
var postTest = Number(post);
if (isNaN(postTest)) {
return post;
}
var min = Number(schema.min);
if (isNaN(min)) {
return post;
}
if (postTest < min) {
this.report();
return min;
}
return post;
},
max: function (schema, post) {
var postTest = Number(post);
if (isNaN(postTest)) {
return post;
}
var max = Number(schema.max);
if (isNaN(max)) {
return post;
}
if (postTest > max) {
this.report();
return max;
}
return post;
},
minLength: function (schema, post) {
var limit = Number(schema.minLength);
if (typeof post !== 'string' || isNaN(limit) || limit < 0) {
return post;
}
var str = '';
var gap = limit - post.length;
if (gap > 0) {
for (var i = 0; i < gap; i++) {
str += '-';
}
this.report();
return post + str;
}
return post;
},
maxLength: function (schema, post) {
var limit = Number(schema.maxLength);
if (typeof post !== 'string' || isNaN(limit) || limit < 0) {
return post;
}
if (post.length > limit) {
this.report();
return post.slice(0, limit);
}
return post;
},
properties: function (schema, post, callback) {
if (typeof callback === 'function') {
return this.asyncProperties(schema, post, callback);
}
if (!post || typeof post !== 'object') {
return post;
}
var properties = schema.properties;
var tmp;
var i;
if (typeof properties['*'] !== 'undefined') {
for (i in post) {
if (i in properties) {
continue;
}
this._deeperObject(i);
tmp = this._sanitize(schema.properties['*'], post[i]);
if (typeof tmp !== 'undefined') {
post[i]= tmp;
}
this._back();
}
}
for (i in schema.properties) {
if (i !== '*') {
this._deeperObject(i);
tmp = this._sanitize(schema.properties[i], post[i]);
if (typeof tmp !== 'undefined') {
post[i]= tmp;
}
this._back();
}
}
return post;
},
items: function (schema, post, callback) {
if (typeof callback === 'function') {
return this.asyncItems(schema, post, callback);
}
if (!(schema.items instanceof Object) || !(post instanceof Object)) {
return post;
}
var i;
if (_typeIs.array(schema.items) && _typeIs.array(post)) {
var minLength = schema.items.length < post.length ? schema.items.length : post.length;
for (i = 0; i < minLength; i++) {
this._deeperArray(i);
post[i] = this._sanitize(schema.items[i], post[i]);
this._back();
}
}
else {
for (i in post) {
if (Object.prototype.hasOwnProperty.call(post, i)) {
this._deeperArray(i);
post[i] = this._sanitize(schema.items, post[i]);
this._back();
}
}
}
return post;
},
exec: function (schema, post, callback) {
if (typeof callback === 'function') {
return this.asyncExec(schema, post, callback);
}
var execs = _typeIs.array(schema.exec) ? schema.exec : [schema.exec];
execs.forEach(function (exec) {
if (typeof exec === 'function') {
post = exec.call(this, schema, post);
}
}, this);
return post;
}
};
var _asyncSanitizationAttribut = {
asyncExec: function (schema, post, callback) {
var self = this;
var execs = _typeIs.array(schema.exec) ? schema.exec : [schema.exec];
async.eachSeries(execs, function (exec, done) {
if (typeof exec === 'function') {
if (exec.length > 2) {
return exec.call(self, schema, post, function (err, res) {
if (err) {
return done(err);
}
post = res;
done();
});
}
post = exec.call(self, schema, post);
}
done();
}, function (err) {
callback(err, post);
});
},
asyncProperties: function (schema, post, callback) {
if (!post || typeof post !== 'object') {
return callback(null, post);
}
var self = this;
var properties = schema.properties;
async.series([
function (next) {
if (properties['*'] == null) {
return next();
}
var globing = properties['*'];
async.eachSeries(Object.keys(post), function (i, next) {
if (i in properties) {
return next();
}
self._deeperObject(i);
self._asyncSanitize(globing, post[i], function (err, res) {
if (err) { /* Error can safely be ignored here */ }
if (typeof res !== 'undefined') {
post[i] = res;
}
self._back();
next();
});
}, next);
},
function (next) {
async.eachSeries(Object.keys(properties), function (i, next) {
if (i === '*') {
return next();
}
self._deeperObject(i);
self._asyncSanitize(properties[i], post[i], function (err, res) {
if (err) {
return next(err);
}
if (typeof res !== 'undefined') {
post[i] = res;
}
self._back();
next();
});
}, next);
}
], function (err) {
return callback(err, post);
});
},
asyncItems: function (schema, post, callback) {
if (!(schema.items instanceof Object) || !(post instanceof Object)) {
return callback(null, post);
}
var self = this;
var items = schema.items;
if (_typeIs.array(items) && _typeIs.array(post)) {
var minLength = items.length < post.length ? items.length : post.length;
async.timesSeries(minLength, function (i, next) {
self._deeperArray(i);
self._asyncSanitize(items[i], post[i], function (err, res) {
if (err) {
return next(err);
}
post[i] = res;
self._back();
next();
});
}, function (err) {
callback(err, post);
});
}
else {
async.eachSeries(Object.keys(post), function (key, next) {
self._deeperArray(key);
self._asyncSanitize(items, post[key], function (err, res) {
if (err) {
return next();
}
post[key] = res;
self._back();
next();
});
}, function (err) {
callback(err, post);
});
}
return post;
}
};
// Sanitization Class --------------------------------------------------------
// inherits from Inspection class (actually we just call Inspection
// constructor with the new context, because its prototype is empty
function Sanitization(schema, custom) {
Inspection.prototype.constructor.call(this, schema, _merge(Sanitization.custom, custom));
var _reporting = [];
this._basicFields = Object.keys(_sanitizationAttribut);
this._customFields = Object.keys(this._custom);
this.origin = null;
this.report = function (message) {
var newNot = {
message: message || 'was sanitized',
property: this.userAlias ? (this.userAlias + ' (' + this._dumpStack() + ')') : this._dumpStack()
};
if (!_reporting.some(function (e) { return e.property === newNot.property; })) {
_reporting.push(newNot);
}
};
this.result = function (data) {
return {
data: data,
reporting: _reporting,
format: function () {
return this.reporting.map(function (i) {
return 'Property ' + i.property + ' ' + i.message;
}).join('\n');
}
};
};
}
_extend(Sanitization.prototype, _sanitizationAttribut);
_extend(Sanitization.prototype, _asyncSanitizationAttribut);
_extend(Sanitization, new Customisable());
Sanitization.prototype.sanitize = function (post, callback) {
this.origin = post;
if (typeof callback === 'function') {
var self = this;
return this._asyncSanitize(this._schema, post, function (err, data) {
self.origin = null;
callback(err, self.result(data));
});
}
var data = this._sanitize(this._schema, post);
this.origin = null;
return this.result(data);
};
Sanitization.prototype._sanitize = function (schema, post) {
this.userAlias = schema.alias || null;
this._basicFields.forEach(function (i) {
if ((i in schema || i === 'optional') && typeof this[i] === 'function') {
post = this[i](schema, post);
}
}, this);
this._customFields.forEach(function (i) {
if (i in schema && typeof this._custom[i] === 'function') {
post = this._custom[i].call(this, schema, post);
}
}, this);
return post;
};
Sanitization.prototype._asyncSanitize = function (schema, post, callback) {
var self = this;
this.userAlias = schema.alias || null;
async.waterfall([
function (next) {
async.reduce(self._basicFields, post, function (value, i, next) {
async.nextTick(function () {
if ((i in schema || i === 'optional') && typeof self[i] === 'function') {
if (self[i].length > 2) {
return self[i](schema, value, next);
}
value = self[i](schema, value);
}
next(null, value);
});
}, next);
},
function (inter, next) {
async.reduce(self._customFields, inter, function (value, i, next) {
async.nextTick(function () {
if (i in schema && typeof self._custom[i] === 'function') {
if (self._custom[i].length > 2) {
return self._custom[i].call(self, schema, value, next);
}
value = self._custom[i].call(self, schema, value);
}
next(null, value);
});
}, next);
}
], callback);
};
// ---------------------------------------------------------------------------
var INT_MIN = -2147483648;
var INT_MAX = 2147483647;
var _rand = {
int: function (min, max) {
return min + (0 | Math.random() * (max - min + 1));
},
float: function (min, max) {
return (Math.random() * (max - min) + min);
},
bool: function () {
return (Math.random() > 0.5);
},
char: function (min, max) {
return String.fromCharCode(this.int(min, max));
},
fromList: function (list) {
return list[this.int(0, list.length - 1)];
}
};
var _formatSample = {
'date-time': function () {
return new Date().toISOString();
},
'date': function () {
return new Date().toISOString().replace(/T.*$/, '');
},
'time': function () {
return new Date().toLocaleTimeString({}, { hour12: false });
},
'color': function (min, max) {
var s = '#';
if (min < 1) {
min = 1;
}
for (var i = 0, l = _rand.int(min, max); i < l; i++) {
s += _rand.fromList('0123456789abcdefABCDEF');
}
return s;
},
'numeric': function () {
return '' + _rand.int(0, INT_MAX);
},
'integer': function () {
if (_rand.bool() === true) {
return '-' + this.numeric();
}
return this.numeric();
},
'decimal': function () {
return this.integer() + '.' + this.numeric();
},
'alpha': function (min, max) {
var s = '';
if (min < 1) {
min = 1;
}
for (var i = 0, l = _rand.int(min, max); i < l; i++) {
s += _rand.fromList('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
}
return s;
},
'alphaNumeric': function (min, max) {
var s = '';
if (min < 1) {
min = 1;
}
for (var i = 0, l = _rand.int(min, max); i < l; i++) {
s += _rand.fromList('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
}
return s;
},
'alphaDash': function (min, max) {
var s = '';
if (min < 1) {
min = 1;
}
for (var i = 0, l = _rand.int(min, max); i < l; i++) {
s += _rand.fromList('_-abcdefghijklmnopqrstuvwxyz_-ABCDEFGHIJKLMNOPQRSTUVWXYZ_-0123456789_-');
}
return s;
},
'javascript': function (min, max) {
var s = _rand.fromList('_$abcdefghijklmnopqrstuvwxyz_$ABCDEFGHIJKLMNOPQRSTUVWXYZ_$');
for (var i = 0, l = _rand.int(min, max - 1); i < l; i++) {
s += _rand.fromList('_$abcdefghijklmnopqrstuvwxyz_$ABCDEFGHIJKLMNOPQRSTUVWXYZ_$0123456789_$');
}
return s;
}
};
function _getLimits(schema) {
var min = INT_MIN;
var max = INT_MAX;
if (schema.gte != null) {
min = schema.gte;
}
else if (schema.gt != null) {
min = schema.gt + 1;
}
if (schema.lte != null) {
max = schema.lte;
}
else if (schema.lt != null) {
max = schema.lt - 1;
}
return { min: min, max: max };
}
var _typeGenerator = {
string: function (schema) {
if (schema.eq != null) {
return schema.eq;
}
var s = '';
var minLength = schema.minLength != null ? schema.minLength : 0;
var maxLength = schema.maxLength != null ? schema.maxLength : 32;
if (typeof schema.pattern === 'string' && typeof _formatSample[schema.pattern] === 'function') {
return _formatSample[schema.pattern](minLength, maxLength);
}
var l = schema.exactLength != null ? schema.exactLength : _rand.int(minLength, maxLength);
for (var i = 0; i < l; i++) {
s += _rand.char(32, 126);
}
return s;
},
number: function (schema) {
if (schema.eq != null) {
return schema.eq;
}
var limit = _getLimits(schema);
var n = _rand.float(limit.min, limit.max);
if (schema.ne != null) {
var ne = _typeIs.array(schema.ne) ? schema.ne : [schema.ne];
while (ne.indexOf(n) !== -1) {
n = _rand.float(limit.min, limit.max);
}
}
return n;
},
integer: function (schema) {
if (schema.eq != null) {
return schema.eq;
}
var limit = _getLimits(schema);
var n = _rand.int(limit.min, limit.max);
if (schema.ne != null) {
var ne = _typeIs.array(schema.ne) ? schema.ne : [schema.ne];
while (ne.indexOf(n) !== -1) {
n = _rand.int(limit.min, limit.max);
}
}
return n;
},
boolean: function (schema) {
if (schema.eq != null) {
return schema.eq;
}
return _rand.bool();
},
"null": function (schema) {
return null;
},
date: function (schema) {
if (schema.eq != null) {
return schema.eq;
}
return new Date();
},
object: function (schema) {
var o = {};
var prop = schema.properties || {};
for (var key in prop) {
if (Object.prototype.hasOwnProperty.call(prop, key)) {
if (prop[key].optional === true && _rand.bool() === true) {
continue;
}
if (key !== '*') {
o[key] = this.generate(prop[key]);
}
else {
var rk = '__random_key_';
var randomKey = rk + 0;
var n = _rand.int(1, 9);
for (var i = 1; i <= n; i++) {
if (!(randomKey in prop)) {
o[randomKey] = this.generate(prop[key]);
}
randomKey = rk + i;
}
}
}
}
return o;
},
array: function (schema) {
var self = this;
var items = schema.items || {};
var minLength = schema.minLength != null ? schema.minLength : 0;
var maxLength = schema.maxLength != null ? schema.maxLength : 16;
var type;
var candidate;
var size;
var i;
if (_typeIs.array(items)) {
size = items.length;
if (schema.exactLength != null) {
size = schema.exactLength;
}
else if (size < minLength) {
size = minLength;
}
else if (size > maxLength) {
size = maxLength;
}
candidate = new Array(size);
type = null;
for (i = 0; i < size; i++) {
type = items[i].type || 'any';
if (_typeIs.array(type)) {
type = type[_rand.int(0, type.length - 1)];
}
candidate[i] = self[type](items[i]);
}
}
else {
size = schema.exactLength != null ? schema.exactLength : _rand.int(minLength, maxLength);
candidate = new Array(size);
type = items.type || 'any';
if (_typeIs.array(type)) {
type = type[_rand.int(0, type.length - 1)];
}
for (i = 0; i < size; i++) {
candidate[i] = self[type](items);
}
}
return candidate;
},
any: function (schema) {
var fields = Object.keys(_typeGenerator);
var i = fields[_rand.int(0, fields.length - 2)];
return this[i](schema);
}
};
// CandidateGenerator Class (Singleton) --------------------------------------
function CandidateGenerator() {
// Maybe extends Inspection class too ?
}
_extend(CandidateGenerator.prototype, _typeGenerator);
var _instance = null;
CandidateGenerator.instance = function () {
if (!(_instance instanceof CandidateGenerator)) {
_instance = new CandidateGenerator();
}
return _instance;
};
CandidateGenerator.prototype.generate = function (schema) {
var type = schema.type || 'any';
if (_typeIs.array(type)) {
type = type[_rand.int(0, type.length - 1)];
}
return this[type](schema);
};
// Exports ---------------------------------------------------------------------
var SchemaInspector = {};
// if server-side (node.js) else client-side
if (typeof module !== 'undefined' && module.exports) {
module.exports = SchemaInspector;
}
else {
window.SchemaInspector = SchemaInspector;
}
SchemaInspector.newSanitization = function (schema, custom) {
return new Sanitization(schema, custom);
};
SchemaInspector.newValidation = function (schema, custom) {
return new Validation(schema, custom);
};
SchemaInspector.Validation = Validation;
SchemaInspector.Sanitization = Sanitization;
SchemaInspector.sanitize = function (schema, post, custom, callback) {
if (arguments.length === 3 && typeof custom === 'function') {
callback = custom;
custom = null;
}
return new Sanitization(schema, custom).sanitize(post, callback);
};
SchemaInspector.validate = function (schema, candidate, custom, callback) {
if (arguments.length === 3 && typeof custom === 'function') {
callback = custom;
custom = null;
}
return new Validation(schema, custom).validate(candidate, callback);
};
SchemaInspector.generate = function (schema, n) {
if (typeof n === 'number') {
var r = new Array(n);
for (var i = 0; i < n; i++) {
r[i] = CandidateGenerator.instance().generate(schema);
}
return r;
}
return CandidateGenerator.instance().generate(schema);
};
})();