mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-12 16:14:25 +03:00
44f2cb0153
Closes #581. * Basically adds the client side of node validator, that we're already using * Validator is plonked onto `Ghost.Validator` * Usage is identical as to https://github.com/chriso/node-validator * Has sanitizing values et al * `Ghost.Validator.error` is redefined, it populates Ghost.Validator._errors (Array) * `Ghost.Validator.handleErrors` is supposed to print out the multiple error messages, if there are multiple (this is broken due to how notifications are presented `.html` instead of `.append`), and also apply class to element * The ajax calls are wrapped in an if to prevent network traffic if something's not right on client side * Added validation to general settings and user settings screens. * On validation error, optionally adds `.input-error` to whatever element you reference, see below (if `el` exists on the error object). This is the only place where usage is different to the original implementation. Redeclared `error()` function in `init.js` * Usage: `Ghost.Validate.check(valueToCheck, {message: "the error message", el: $('#the element')}).isEmail()` * The element above will receive the `.input-error` class. `isEmail()` is one of the stuff you can check against.
1011 lines
33 KiB
JavaScript
1011 lines
33 KiB
JavaScript
/*!
|
|
* Copyright (c) 2010 Chris O'Hara <cohara87@gmail.com>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining
|
|
* a copy of this software and associated documentation files (the
|
|
* "Software"), to deal in the Software without restriction, including
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
* the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
// follow Universal Module Definition (UMD) pattern for defining module as AMD, CommonJS, and Browser compatible
|
|
(function (root, factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
// AMD. Register as an anonymous module.
|
|
define(['exports'], factory);
|
|
} else if (typeof exports === 'object') {
|
|
// CommonJS
|
|
factory(exports);
|
|
} else {
|
|
// Browser globals
|
|
// N.B. Here is a slight difference to regular UMD as the current API for node-validator in browser adds each export directly to the window
|
|
// rather than to a namespaced object such as window.nodeValidator, which would be better practice, but would break backwards compatibility
|
|
// as such unable to use build tools like grunt-umd
|
|
factory(root);
|
|
}
|
|
}(this, function(exports) {
|
|
|
|
var entities = {
|
|
' ': '\u00a0',
|
|
'¡': '\u00a1',
|
|
'¢': '\u00a2',
|
|
'£': '\u00a3',
|
|
'¤': '\u20ac',
|
|
'¥': '\u00a5',
|
|
'¦': '\u0160',
|
|
'§': '\u00a7',
|
|
'¨': '\u0161',
|
|
'©': '\u00a9',
|
|
'ª': '\u00aa',
|
|
'«': '\u00ab',
|
|
'¬': '\u00ac',
|
|
'­': '\u00ad',
|
|
'®': '\u00ae',
|
|
'¯': '\u00af',
|
|
'°': '\u00b0',
|
|
'±': '\u00b1',
|
|
'²': '\u00b2',
|
|
'³': '\u00b3',
|
|
'´': '\u017d',
|
|
'µ': '\u00b5',
|
|
'¶': '\u00b6',
|
|
'·': '\u00b7',
|
|
'¸': '\u017e',
|
|
'¹': '\u00b9',
|
|
'º': '\u00ba',
|
|
'»': '\u00bb',
|
|
'¼': '\u0152',
|
|
'½': '\u0153',
|
|
'¾': '\u0178',
|
|
'¿': '\u00bf',
|
|
'À': '\u00c0',
|
|
'Á': '\u00c1',
|
|
'Â': '\u00c2',
|
|
'Ã': '\u00c3',
|
|
'Ä': '\u00c4',
|
|
'Å': '\u00c5',
|
|
'Æ': '\u00c6',
|
|
'Ç': '\u00c7',
|
|
'È': '\u00c8',
|
|
'É': '\u00c9',
|
|
'Ê': '\u00ca',
|
|
'Ë': '\u00cb',
|
|
'Ì': '\u00cc',
|
|
'Í': '\u00cd',
|
|
'Î': '\u00ce',
|
|
'Ï': '\u00cf',
|
|
'Ð': '\u00d0',
|
|
'Ñ': '\u00d1',
|
|
'Ò': '\u00d2',
|
|
'Ó': '\u00d3',
|
|
'Ô': '\u00d4',
|
|
'Õ': '\u00d5',
|
|
'Ö': '\u00d6',
|
|
'×': '\u00d7',
|
|
'Ø': '\u00d8',
|
|
'Ù': '\u00d9',
|
|
'Ú': '\u00da',
|
|
'Û': '\u00db',
|
|
'Ü': '\u00dc',
|
|
'Ý': '\u00dd',
|
|
'Þ': '\u00de',
|
|
'ß': '\u00df',
|
|
'à': '\u00e0',
|
|
'á': '\u00e1',
|
|
'â': '\u00e2',
|
|
'ã': '\u00e3',
|
|
'ä': '\u00e4',
|
|
'å': '\u00e5',
|
|
'æ': '\u00e6',
|
|
'ç': '\u00e7',
|
|
'è': '\u00e8',
|
|
'é': '\u00e9',
|
|
'ê': '\u00ea',
|
|
'ë': '\u00eb',
|
|
'ì': '\u00ec',
|
|
'í': '\u00ed',
|
|
'î': '\u00ee',
|
|
'ï': '\u00ef',
|
|
'ð': '\u00f0',
|
|
'ñ': '\u00f1',
|
|
'ò': '\u00f2',
|
|
'ó': '\u00f3',
|
|
'ô': '\u00f4',
|
|
'õ': '\u00f5',
|
|
'ö': '\u00f6',
|
|
'÷': '\u00f7',
|
|
'ø': '\u00f8',
|
|
'ù': '\u00f9',
|
|
'ú': '\u00fa',
|
|
'û': '\u00fb',
|
|
'ü': '\u00fc',
|
|
'ý': '\u00fd',
|
|
'þ': '\u00fe',
|
|
'ÿ': '\u00ff',
|
|
'"': '\u0022',
|
|
'<': '\u003c',
|
|
'>': '\u003e',
|
|
''': '\u0027',
|
|
'−': '\u2212',
|
|
'ˆ': '\u02c6',
|
|
'˜': '\u02dc',
|
|
'Š': '\u0160',
|
|
'‹': '\u2039',
|
|
'Œ': '\u0152',
|
|
'‘': '\u2018',
|
|
'’': '\u2019',
|
|
'“': '\u201c',
|
|
'”': '\u201d',
|
|
'•': '\u2022',
|
|
'–': '\u2013',
|
|
'—': '\u2014',
|
|
'™': '\u2122',
|
|
'š': '\u0161',
|
|
'›': '\u203a',
|
|
'œ': '\u0153',
|
|
'Ÿ': '\u0178',
|
|
'ƒ': '\u0192',
|
|
'Α': '\u0391',
|
|
'Β': '\u0392',
|
|
'Γ': '\u0393',
|
|
'Δ': '\u0394',
|
|
'Ε': '\u0395',
|
|
'Ζ': '\u0396',
|
|
'Η': '\u0397',
|
|
'Θ': '\u0398',
|
|
'Ι': '\u0399',
|
|
'Κ': '\u039a',
|
|
'Λ': '\u039b',
|
|
'Μ': '\u039c',
|
|
'Ν': '\u039d',
|
|
'Ξ': '\u039e',
|
|
'Ο': '\u039f',
|
|
'Π': '\u03a0',
|
|
'Ρ': '\u03a1',
|
|
'Σ': '\u03a3',
|
|
'Τ': '\u03a4',
|
|
'Υ': '\u03a5',
|
|
'Φ': '\u03a6',
|
|
'Χ': '\u03a7',
|
|
'Ψ': '\u03a8',
|
|
'Ω': '\u03a9',
|
|
'α': '\u03b1',
|
|
'β': '\u03b2',
|
|
'γ': '\u03b3',
|
|
'δ': '\u03b4',
|
|
'ε': '\u03b5',
|
|
'ζ': '\u03b6',
|
|
'η': '\u03b7',
|
|
'θ': '\u03b8',
|
|
'ι': '\u03b9',
|
|
'κ': '\u03ba',
|
|
'λ': '\u03bb',
|
|
'μ': '\u03bc',
|
|
'ν': '\u03bd',
|
|
'ξ': '\u03be',
|
|
'ο': '\u03bf',
|
|
'π': '\u03c0',
|
|
'ρ': '\u03c1',
|
|
'ς': '\u03c2',
|
|
'σ': '\u03c3',
|
|
'τ': '\u03c4',
|
|
'υ': '\u03c5',
|
|
'φ': '\u03c6',
|
|
'χ': '\u03c7',
|
|
'ψ': '\u03c8',
|
|
'ω': '\u03c9',
|
|
'ϑ': '\u03d1',
|
|
'ϒ': '\u03d2',
|
|
'ϖ': '\u03d6',
|
|
' ': '\u2002',
|
|
' ': '\u2003',
|
|
' ': '\u2009',
|
|
'‌': '\u200c',
|
|
'‍': '\u200d',
|
|
'‎': '\u200e',
|
|
'‏': '\u200f',
|
|
'‚': '\u201a',
|
|
'„': '\u201e',
|
|
'†': '\u2020',
|
|
'‡': '\u2021',
|
|
'…': '\u2026',
|
|
'‰': '\u2030',
|
|
'′': '\u2032',
|
|
'″': '\u2033',
|
|
'‾': '\u203e',
|
|
'⁄': '\u2044',
|
|
'€': '\u20ac',
|
|
'ℑ': '\u2111',
|
|
'℘': '\u2118',
|
|
'ℜ': '\u211c',
|
|
'ℵ': '\u2135',
|
|
'←': '\u2190',
|
|
'↑': '\u2191',
|
|
'→': '\u2192',
|
|
'↓': '\u2193',
|
|
'↔': '\u2194',
|
|
'↵': '\u21b5',
|
|
'⇐': '\u21d0',
|
|
'⇑': '\u21d1',
|
|
'⇒': '\u21d2',
|
|
'⇓': '\u21d3',
|
|
'⇔': '\u21d4',
|
|
'∀': '\u2200',
|
|
'∂': '\u2202',
|
|
'∃': '\u2203',
|
|
'∅': '\u2205',
|
|
'∇': '\u2207',
|
|
'∈': '\u2208',
|
|
'∉': '\u2209',
|
|
'∋': '\u220b',
|
|
'∏': '\u220f',
|
|
'∑': '\u2211',
|
|
'∗': '\u2217',
|
|
'√': '\u221a',
|
|
'∝': '\u221d',
|
|
'∞': '\u221e',
|
|
'∠': '\u2220',
|
|
'∧': '\u2227',
|
|
'∨': '\u2228',
|
|
'∩': '\u2229',
|
|
'∪': '\u222a',
|
|
'∫': '\u222b',
|
|
'∴': '\u2234',
|
|
'∼': '\u223c',
|
|
'≅': '\u2245',
|
|
'≈': '\u2248',
|
|
'≠': '\u2260',
|
|
'≡': '\u2261',
|
|
'≤': '\u2264',
|
|
'≥': '\u2265',
|
|
'⊂': '\u2282',
|
|
'⊃': '\u2283',
|
|
'⊄': '\u2284',
|
|
'⊆': '\u2286',
|
|
'⊇': '\u2287',
|
|
'⊕': '\u2295',
|
|
'⊗': '\u2297',
|
|
'⊥': '\u22a5',
|
|
'⋅': '\u22c5',
|
|
'⌈': '\u2308',
|
|
'⌉': '\u2309',
|
|
'⌊': '\u230a',
|
|
'⌋': '\u230b',
|
|
'⟨': '\u2329',
|
|
'⟩': '\u232a',
|
|
'◊': '\u25ca',
|
|
'♠': '\u2660',
|
|
'♣': '\u2663',
|
|
'♥': '\u2665',
|
|
'♦': '\u2666'
|
|
};
|
|
|
|
var decode = function (str) {
|
|
if (!~str.indexOf('&')) return str;
|
|
|
|
//Decode literal entities
|
|
for (var i in entities) {
|
|
str = str.replace(new RegExp(i, 'g'), entities[i]);
|
|
}
|
|
|
|
//Decode hex entities
|
|
str = str.replace(/&#x(0*[0-9a-f]{2,5});?/gi, function (m, code) {
|
|
return String.fromCharCode(parseInt(+code, 16));
|
|
});
|
|
|
|
//Decode numeric entities
|
|
str = str.replace(/&#([0-9]{2,4});?/gi, function (m, code) {
|
|
return String.fromCharCode(+code);
|
|
});
|
|
|
|
str = str.replace(/&/g, '&');
|
|
|
|
return str;
|
|
}
|
|
|
|
var encode = function (str) {
|
|
str = str.replace(/&/g, '&');
|
|
|
|
//IE doesn't accept '
|
|
str = str.replace(/'/g, ''');
|
|
|
|
//Encode literal entities
|
|
for (var i in entities) {
|
|
str = str.replace(new RegExp(entities[i], 'g'), i);
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
exports.entities = {
|
|
encode: encode,
|
|
decode: decode
|
|
}
|
|
|
|
//This module is adapted from the CodeIgniter framework
|
|
//The license is available at http://codeigniter.com/
|
|
|
|
var never_allowed_str = {
|
|
'document.cookie': '',
|
|
'document.write': '',
|
|
'.parentNode': '',
|
|
'.innerHTML': '',
|
|
'window.location': '',
|
|
'-moz-binding': '',
|
|
'<!--': '<!--',
|
|
'-->': '-->',
|
|
'<![CDATA[': '<![CDATA['
|
|
};
|
|
|
|
var never_allowed_regex = {
|
|
'javascript\\s*:': '',
|
|
'expression\\s*(\\(|&\\#40;)': '',
|
|
'vbscript\\s*:': '',
|
|
'Redirect\\s+302': ''
|
|
};
|
|
|
|
var non_displayables = [
|
|
/%0[0-8bcef]/g, // url encoded 00-08, 11, 12, 14, 15
|
|
/%1[0-9a-f]/g, // url encoded 16-31
|
|
/[\x00-\x08]/g, // 00-08
|
|
/\x0b/g, /\x0c/g, // 11,12
|
|
/[\x0e-\x1f]/g // 14-31
|
|
];
|
|
|
|
var compact_words = [
|
|
'javascript', 'expression', 'vbscript',
|
|
'script', 'applet', 'alert', 'document',
|
|
'write', 'cookie', 'window'
|
|
];
|
|
|
|
exports.xssClean = function(str, is_image) {
|
|
|
|
//Recursively clean objects and arrays
|
|
if (typeof str === 'object') {
|
|
for (var i in str) {
|
|
str[i] = exports.xssClean(str[i]);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
//Remove invisible characters
|
|
str = remove_invisible_characters(str);
|
|
|
|
//Protect query string variables in URLs => 901119URL5918AMP18930PROTECT8198
|
|
str = str.replace(/\&([a-z\_0-9]+)\=([a-z\_0-9]+)/i, xss_hash() + '$1=$2');
|
|
|
|
//Validate standard character entities - add a semicolon if missing. We do this to enable
|
|
//the conversion of entities to ASCII later.
|
|
str = str.replace(/(&\#?[0-9a-z]{2,})([\x00-\x20])*;?/i, '$1;$2');
|
|
|
|
//Validate UTF16 two byte encoding (x00) - just as above, adds a semicolon if missing.
|
|
str = str.replace(/(&\#x?)([0-9A-F]+);?/i, '$1;$2');
|
|
|
|
//Un-protect query string variables
|
|
str = str.replace(xss_hash(), '&');
|
|
|
|
//Decode just in case stuff like this is submitted:
|
|
//<a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
|
|
try {
|
|
str = decodeURIComponent(str);
|
|
} catch (e) {
|
|
// str was not actually URI-encoded
|
|
}
|
|
|
|
//Convert character entities to ASCII - this permits our tests below to work reliably.
|
|
//We only convert entities that are within tags since these are the ones that will pose security problems.
|
|
str = str.replace(/[a-z]+=([\'\"]).*?\1/gi, function(m, match) {
|
|
return m.replace(match, convert_attribute(match));
|
|
});
|
|
|
|
//Remove invisible characters again
|
|
str = remove_invisible_characters(str);
|
|
|
|
//Convert tabs to spaces
|
|
str = str.replace('\t', ' ');
|
|
|
|
//Captured the converted string for later comparison
|
|
var converted_string = str;
|
|
|
|
//Remove strings that are never allowed
|
|
for (var i in never_allowed_str) {
|
|
str = str.replace(i, never_allowed_str[i]);
|
|
}
|
|
|
|
//Remove regex patterns that are never allowed
|
|
for (var i in never_allowed_regex) {
|
|
str = str.replace(new RegExp(i, 'i'), never_allowed_regex[i]);
|
|
}
|
|
|
|
//Compact any exploded words like: j a v a s c r i p t
|
|
// We only want to do this when it is followed by a non-word character
|
|
for (var i in compact_words) {
|
|
var spacified = compact_words[i].split('').join('\\s*')+'\\s*';
|
|
|
|
str = str.replace(new RegExp('('+spacified+')(\\W)', 'ig'), function(m, compat, after) {
|
|
return compat.replace(/\s+/g, '') + after;
|
|
});
|
|
}
|
|
|
|
//Remove disallowed Javascript in links or img tags
|
|
do {
|
|
var original = str;
|
|
|
|
if (str.match(/<a/i)) {
|
|
str = str.replace(/<a\s+([^>]*?)(>|$)/gi, function(m, attributes, end_tag) {
|
|
attributes = filter_attributes(attributes.replace('<','').replace('>',''));
|
|
return m.replace(attributes, attributes.replace(/href=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)/gi, ''));
|
|
});
|
|
}
|
|
|
|
if (str.match(/<img/i)) {
|
|
str = str.replace(/<img\s+([^>]*?)(\s?\/?>|$)/gi, function(m, attributes, end_tag) {
|
|
attributes = filter_attributes(attributes.replace('<','').replace('>',''));
|
|
return m.replace(attributes, attributes.replace(/src=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)/gi, ''));
|
|
});
|
|
}
|
|
|
|
if (str.match(/script/i) || str.match(/xss/i)) {
|
|
str = str.replace(/<(\/*)(script|xss)(.*?)\>/gi, '');
|
|
}
|
|
|
|
} while(original != str);
|
|
|
|
//Remove JavaScript Event Handlers - Note: This code is a little blunt. It removes the event
|
|
//handler and anything up to the closing >, but it's unlikely to be a problem.
|
|
event_handlers = ['[^a-z_\-]on\\w*'];
|
|
|
|
//Adobe Photoshop puts XML metadata into JFIF images, including namespacing,
|
|
//so we have to allow this for images
|
|
if (!is_image) {
|
|
event_handlers.push('xmlns');
|
|
}
|
|
|
|
str = str.replace(new RegExp("<([^><]+?)("+event_handlers.join('|')+")(\\s*=\\s*[^><]*)([><]*)", 'i'), '<$1$4');
|
|
|
|
//Sanitize naughty HTML elements
|
|
//If a tag containing any of the words in the list
|
|
//below is found, the tag gets converted to entities.
|
|
//So this: <blink>
|
|
//Becomes: <blink>
|
|
naughty = 'alert|applet|audio|basefont|base|behavior|bgsound|blink|body|embed|expression|form|frameset|frame|head|html|ilayer|iframe|input|isindex|layer|link|meta|object|plaintext|style|script|textarea|title|video|xml|xss';
|
|
str = str.replace(new RegExp('<(/*\\s*)('+naughty+')([^><]*)([><]*)', 'gi'), function(m, a, b, c, d) {
|
|
return '<' + a + b + c + d.replace('>','>').replace('<','<');
|
|
});
|
|
|
|
//Sanitize naughty scripting elements Similar to above, only instead of looking for
|
|
//tags it looks for PHP and JavaScript commands that are disallowed. Rather than removing the
|
|
//code, it simply converts the parenthesis to entities rendering the code un-executable.
|
|
//For example: eval('some code')
|
|
//Becomes: eval('some code')
|
|
str = str.replace(/(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)/gi, '$1$2($3)');
|
|
|
|
//This adds a bit of extra precaution in case something got through the above filters
|
|
for (var i in never_allowed_str) {
|
|
str = str.replace(i, never_allowed_str[i]);
|
|
}
|
|
for (var i in never_allowed_regex) {
|
|
str = str.replace(new RegExp(i, 'i'), never_allowed_regex[i]);
|
|
}
|
|
|
|
//Images are handled in a special way
|
|
if (is_image && str !== converted_string) {
|
|
throw new Error('Image may contain XSS');
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
function remove_invisible_characters(str) {
|
|
for (var i in non_displayables) {
|
|
str = str.replace(non_displayables[i], '');
|
|
}
|
|
return str;
|
|
}
|
|
|
|
function xss_hash() {
|
|
//TODO: Create a random hash
|
|
return '!*$^#(@*#&';
|
|
}
|
|
|
|
function convert_attribute(str) {
|
|
return str.replace('>','>').replace('<','<').replace('\\','\\\\');
|
|
}
|
|
|
|
//Filter Attributes - filters tag attributes for consistency and safety
|
|
function filter_attributes(str) {
|
|
var comments = /\/\*.*?\*\//g;
|
|
return str.replace(/\s*[a-z-]+\s*=\s*'[^']*'/gi, function (m) {
|
|
return m.replace(comments, '');
|
|
}).replace(/\s*[a-z-]+\s*=\s*"[^"]*"/gi, function (m) {
|
|
return m.replace(comments, '');
|
|
}).replace(/\s*[a-z-]+\s*=\s*[^\s]+/gi, function (m) {
|
|
return m.replace(comments, '');
|
|
});
|
|
}
|
|
|
|
var Validator = exports.Validator = function() {}
|
|
|
|
Validator.prototype.check = function(str, fail_msg) {
|
|
this.str = typeof( str ) === 'undefined' || str === null || (isNaN(str) && str.length === undefined) ? '' : str+'';
|
|
this.msg = fail_msg;
|
|
this._errors = this._errors || [];
|
|
return this;
|
|
}
|
|
|
|
function internal_is_ipv4(str) {
|
|
if (/^(\d?\d?\d)\.(\d?\d?\d)\.(\d?\d?\d)\.(\d?\d?\d)$/.test(str)) {
|
|
var parts = str.split('.').sort();
|
|
// no need to check for < 0 as regex won't match in that case
|
|
if (parts[3] > 255) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function internal_is_ipv6(str) {
|
|
if (/^::|^::1|^([a-fA-F0-9]{1,4}::?){1,7}([a-fA-F0-9]{1,4})$/.test(str)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//Create some aliases - may help code readability
|
|
Validator.prototype.validate = Validator.prototype.check;
|
|
Validator.prototype.assert = Validator.prototype.check;
|
|
|
|
Validator.prototype.error = function(msg) {
|
|
throw new Error(msg);
|
|
}
|
|
|
|
function toDate(date) {
|
|
if (date instanceof Date) {
|
|
return date;
|
|
}
|
|
var intDate = Date.parse(date);
|
|
if (isNaN(intDate)) {
|
|
return null;
|
|
}
|
|
return new Date(intDate);
|
|
}
|
|
|
|
Validator.prototype.isAfter = function(date) {
|
|
date = date || new Date();
|
|
var origDate = toDate(this.str)
|
|
, compDate = toDate(date);
|
|
if (!(origDate && compDate && origDate >= compDate)) {
|
|
return this.error(this.msg || 'Invalid date');
|
|
}
|
|
return this;
|
|
};
|
|
|
|
Validator.prototype.isBefore = function(date) {
|
|
date = date || new Date();
|
|
var origDate = toDate(this.str)
|
|
, compDate = toDate(date);
|
|
if (!(origDate && compDate && origDate <= compDate)) {
|
|
return this.error(this.msg || 'Invalid date');
|
|
}
|
|
return this;
|
|
};
|
|
|
|
Validator.prototype.isEmail = function() {
|
|
if (!this.str.match(/^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!\.)){0,61}[a-zA-Z0-9]?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-](?!$)){0,61}[a-zA-Z0-9]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$/)) {
|
|
return this.error(this.msg || 'Invalid email');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
//Will work against Visa, MasterCard, American Express, Discover, Diners Club, and JCB card numbering formats
|
|
Validator.prototype.isCreditCard = function() {
|
|
this.str = this.str.replace(/[^0-9]+/g, ''); //remove all dashes, spaces, etc.
|
|
if (!this.str.match(/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/)) {
|
|
return this.error(this.msg || 'Invalid credit card');
|
|
}
|
|
// Doing Luhn check
|
|
var sum = 0;
|
|
var digit;
|
|
var tmpNum;
|
|
var shouldDouble = false;
|
|
for (var i = this.length - 1; i >= 0; i--) {
|
|
digit = this.substring(i, (i + 1));
|
|
tmpNum = parseInt(digit, 10);
|
|
if (shouldDouble) {
|
|
tmpNum *= 2;
|
|
if (tmpNum >= 10) {
|
|
sum += ((tmpNum % 10) + 1);
|
|
}
|
|
else {
|
|
sum += tmpNum;
|
|
}
|
|
}
|
|
else {
|
|
sum += tmpNum;
|
|
}
|
|
if (shouldDouble) {
|
|
shouldDouble = false;
|
|
}
|
|
else {
|
|
shouldDouble = true;
|
|
}
|
|
}
|
|
if ((sum % 10) !== 0) {
|
|
return this.error(this.msg || 'Invalid credit card');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isUrl = function() {
|
|
if (!this.str.match(/^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i) || this.str.length > 2083) {
|
|
return this.error(this.msg || 'Invalid URL');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isIPv4 = function() {
|
|
if (internal_is_ipv4(this.str)) {
|
|
return this;
|
|
}
|
|
return this.error(this.msg || 'Invalid IP');
|
|
}
|
|
|
|
Validator.prototype.isIPv6 = function() {
|
|
if (internal_is_ipv6(this.str)) {
|
|
return this;
|
|
}
|
|
return this.error(this.msg || 'Invalid IP');
|
|
}
|
|
|
|
Validator.prototype.isIP = function() {
|
|
if (internal_is_ipv4(this.str) || internal_is_ipv6(this.str)) {
|
|
return this;
|
|
}
|
|
return this.error(this.msg || 'Invalid IP');
|
|
}
|
|
|
|
Validator.prototype.isAlpha = function() {
|
|
if (!this.str.match(/^[a-zA-Z]+$/)) {
|
|
return this.error(this.msg || 'Invalid characters');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isAlphanumeric = function() {
|
|
if (!this.str.match(/^[a-zA-Z0-9]+$/)) {
|
|
return this.error(this.msg || 'Invalid characters');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isNumeric = function() {
|
|
if (!this.str.match(/^-?[0-9]+$/)) {
|
|
return this.error(this.msg || 'Invalid number');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isHexadecimal = function() {
|
|
if (!this.str.match(/^[0-9a-fA-F]+$/)) {
|
|
return this.error(this.msg || 'Invalid hexadecimal');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isHexColor = function() {
|
|
if (!this.str.match(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/)) {
|
|
return this.error(this.msg || 'Invalid hexcolor');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isLowercase = function() {
|
|
if (this.str !== this.str.toLowerCase()) {
|
|
return this.error(this.msg || 'Invalid characters');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isUppercase = function() {
|
|
if (this.str !== this.str.toUpperCase()) {
|
|
return this.error(this.msg || 'Invalid characters');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isInt = function() {
|
|
if (!this.str.match(/^(?:-?(?:0|[1-9][0-9]*))$/)) {
|
|
return this.error(this.msg || 'Invalid integer');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isDecimal = function() {
|
|
if (!this.str.match(/^(?:-?(?:0|[1-9][0-9]*))?(?:\.[0-9]*)?$/)) {
|
|
return this.error(this.msg || 'Invalid decimal');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isDivisibleBy = function(n) {
|
|
return (parseFloat(this.str) % parseInt(n, 10)) === 0;
|
|
}
|
|
|
|
Validator.prototype.isFloat = function() {
|
|
return this.isDecimal();
|
|
}
|
|
|
|
Validator.prototype.notNull = function() {
|
|
if (this.str === '') {
|
|
return this.error(this.msg || 'String is empty');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isNull = function() {
|
|
if (this.str !== '') {
|
|
return this.error(this.msg || 'String is not empty');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.notEmpty = function() {
|
|
if (this.str.match(/^[\s\t\r\n]*$/)) {
|
|
return this.error(this.msg || 'String is whitespace');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.equals = function(equals) {
|
|
if (this.str != equals) {
|
|
return this.error(this.msg || 'Not equal');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.contains = function(str) {
|
|
if (this.str.indexOf(str) === -1 || !str) {
|
|
return this.error(this.msg || 'Invalid characters');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.notContains = function(str) {
|
|
if (this.str.indexOf(str) >= 0) {
|
|
return this.error(this.msg || 'Invalid characters');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.regex = Validator.prototype.is = function(pattern, modifiers) {
|
|
if (Object.prototype.toString.call(pattern).slice(8, -1) !== 'RegExp') {
|
|
pattern = new RegExp(pattern, modifiers);
|
|
}
|
|
if (! this.str.match(pattern)) {
|
|
return this.error(this.msg || 'Invalid characters');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.notRegex = Validator.prototype.not = function(pattern, modifiers) {
|
|
if (Object.prototype.toString.call(pattern).slice(8, -1) !== 'RegExp') {
|
|
pattern = new RegExp(pattern, modifiers);
|
|
}
|
|
if (this.str.match(pattern)) {
|
|
this.error(this.msg || 'Invalid characters');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.len = function(min, max) {
|
|
if (this.str.length < min) {
|
|
return this.error(this.msg || 'String is too small');
|
|
}
|
|
if (typeof max !== undefined && this.str.length > max) {
|
|
return this.error(this.msg || 'String is too large');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
//Thanks to github.com/sreuter for the idea.
|
|
Validator.prototype.isUUID = function(version) {
|
|
var pattern;
|
|
if (version == 3 || version == 'v3') {
|
|
pattern = /[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
|
|
} else if (version == 4 || version == 'v4') {
|
|
pattern = /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
|
|
} else if (version == 5 || version == 'v5') {
|
|
pattern = /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
|
|
} else {
|
|
pattern = /[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
|
|
}
|
|
if (!this.str.match(pattern)) {
|
|
return this.error(this.msg || 'Not a UUID');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isUUIDv3 = function() {
|
|
return this.isUUID(3);
|
|
}
|
|
|
|
Validator.prototype.isUUIDv4 = function() {
|
|
return this.isUUID(4);
|
|
}
|
|
|
|
Validator.prototype.isUUIDv5 = function() {
|
|
return this.isUUID(5);
|
|
}
|
|
|
|
Validator.prototype.isDate = function() {
|
|
var intDate = Date.parse(this.str);
|
|
if (isNaN(intDate)) {
|
|
return this.error(this.msg || 'Not a date');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.isIn = function(options) {
|
|
if (options && typeof options.indexOf === 'function') {
|
|
if (!~options.indexOf(this.str)) {
|
|
return this.error(this.msg || 'Unexpected value');
|
|
}
|
|
return this;
|
|
} else {
|
|
return this.error(this.msg || 'Invalid in() argument');
|
|
}
|
|
}
|
|
|
|
Validator.prototype.notIn = function(options) {
|
|
if (options && typeof options.indexOf === 'function') {
|
|
if (options.indexOf(this.str) !== -1) {
|
|
return this.error(this.msg || 'Unexpected value');
|
|
}
|
|
return this;
|
|
} else {
|
|
return this.error(this.msg || 'Invalid notIn() argument');
|
|
}
|
|
}
|
|
|
|
Validator.prototype.min = function(val) {
|
|
var number = parseFloat(this.str);
|
|
|
|
if (!isNaN(number) && number < val) {
|
|
return this.error(this.msg || 'Invalid number');
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
Validator.prototype.max = function(val) {
|
|
var number = parseFloat(this.str);
|
|
if (!isNaN(number) && number > val) {
|
|
return this.error(this.msg || 'Invalid number');
|
|
}
|
|
return this;
|
|
}
|
|
|
|
var Filter = exports.Filter = function() {}
|
|
|
|
var whitespace = '\\r\\n\\t\\s';
|
|
|
|
Filter.prototype.modify = function(str) {
|
|
this.str = str;
|
|
}
|
|
|
|
//Create some aliases - may help code readability
|
|
Filter.prototype.convert = Filter.prototype.sanitize = function(str) {
|
|
this.str = str == null ? '' : str + '';
|
|
return this;
|
|
}
|
|
|
|
Filter.prototype.xss = function(is_image) {
|
|
this.modify(exports.xssClean(this.str, is_image));
|
|
return this.str;
|
|
}
|
|
|
|
Filter.prototype.entityDecode = function() {
|
|
this.modify(decode(this.str));
|
|
return this.str;
|
|
}
|
|
|
|
Filter.prototype.entityEncode = function() {
|
|
this.modify(encode(this.str));
|
|
return this.str;
|
|
}
|
|
|
|
Filter.prototype.escape = function() {
|
|
this.modify(this.str.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>'));
|
|
return this.str;
|
|
};
|
|
|
|
Filter.prototype.ltrim = function(chars) {
|
|
chars = chars || whitespace;
|
|
this.modify(this.str.replace(new RegExp('^['+chars+']+', 'g'), ''));
|
|
return this.str;
|
|
}
|
|
|
|
Filter.prototype.rtrim = function(chars) {
|
|
chars = chars || whitespace;
|
|
this.modify(this.str.replace(new RegExp('['+chars+']+$', 'g'), ''));
|
|
return this.str;
|
|
}
|
|
|
|
Filter.prototype.trim = function(chars) {
|
|
chars = chars || whitespace;
|
|
this.modify(this.str.replace(new RegExp('^['+chars+']+|['+chars+']+$', 'g'), ''));
|
|
return this.str;
|
|
}
|
|
|
|
Filter.prototype.ifNull = function(replace) {
|
|
if (!this.str || this.str === '') {
|
|
this.modify(replace);
|
|
}
|
|
return this.str;
|
|
}
|
|
|
|
Filter.prototype.toFloat = function() {
|
|
this.modify(parseFloat(this.str));
|
|
return this.str;
|
|
}
|
|
|
|
Filter.prototype.toInt = function(radix) {
|
|
radix = radix || 10;
|
|
this.modify(parseInt(this.str, radix));
|
|
return this.str;
|
|
}
|
|
|
|
//Any strings with length > 0 (except for '0' and 'false') are considered true,
|
|
//all other strings are false
|
|
Filter.prototype.toBoolean = function() {
|
|
if (!this.str || this.str == '0' || this.str == 'false' || this.str == '') {
|
|
this.modify(false);
|
|
} else {
|
|
this.modify(true);
|
|
}
|
|
return this.str;
|
|
}
|
|
|
|
//String must be equal to '1' or 'true' to be considered true, all other strings
|
|
//are false
|
|
Filter.prototype.toBooleanStrict = function() {
|
|
if (this.str == '1' || this.str == 'true') {
|
|
this.modify(true);
|
|
} else {
|
|
this.modify(false);
|
|
}
|
|
return this.str;
|
|
}
|
|
|
|
//Quick access methods
|
|
exports.sanitize = exports.convert = function(str) {
|
|
var filter = new exports.Filter();
|
|
return filter.sanitize(str);
|
|
}
|
|
|
|
exports.check = exports.validate = exports.assert = function(str, fail_msg) {
|
|
var validator = new exports.Validator();
|
|
return validator.check(str, fail_msg);
|
|
}
|
|
|
|
return exports;
|
|
|
|
}));
|