diff --git a/bower.json b/bower.json index 3ced1bf6f6..b41dcf19ce 100644 --- a/bower.json +++ b/bower.json @@ -15,6 +15,6 @@ "nprogress": "0.1.2", "fastclick": "1.0.0", "Countable": "2.0.2", - "validator-js": "1.5.1" + "validator-js": "3.4.0" } } diff --git a/core/client/assets/vendor/validator-client.js b/core/client/assets/vendor/validator-client.js index 70c13a0099..5925e5abcc 100644 --- a/core/client/assets/vendor/validator-client.js +++ b/core/client/assets/vendor/validator-client.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2010 Chris O'Hara + * Copyright (c) 2014 Chris O'Hara * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -21,990 +21,353 @@ * 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); +(function (name, definition) { + if (typeof module !== 'undefined') { + module.exports = definition(); + } else if (typeof define === 'function' && typeof define.amd === 'object') { + define(definition); } 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[name] = definition(); } -}(this, function(exports) { +})('validator', function (validator) { - 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' + 'use strict'; + + validator = { version: '3.4.0' }; + + var email = /^(?:[\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])\]))$/; + + var creditCard = /^(?: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})$/; + + var isbn10Maybe = /^(?:[0-9]{9}X|[0-9]{10})$/ + , isbn13Maybe = /^(?:[0-9]{13})$/; + + var ipv4Maybe = /^(\d?\d?\d)\.(\d?\d?\d)\.(\d?\d?\d)\.(\d?\d?\d)$/ + , ipv6 = /^::|^::1|^([a-fA-F0-9]{1,4}::?){1,7}([a-fA-F0-9]{1,4})$/; + + var uuid = { + '3': /^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i + , '4': /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i + , '5': /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i + , all: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i }; - var decode = function (str) { - if (!~str.indexOf('&')) return str; + var alpha = /^[a-zA-Z]+$/ + , alphanumeric = /^[a-zA-Z0-9]+$/ + , numeric = /^-?[0-9]+$/ + , int = /^(?:-?(?:0|[1-9][0-9]*))$/ + , float = /^(?:-?(?:[0-9]+))?(?:\.[0-9]*)?(?:[eE][\+\-]?(?:[0-9]+))?$/ + , hexadecimal = /^[0-9a-fA-F]+$/ + , hexcolor = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; - //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': '', - '': '-->', - '= 0) { + continue; + } + validator.extend(name, validator[name]); + } }; - 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; + validator.toString = function (input) { + if (input === null || typeof input === 'undefined' || (isNaN(input) && !input.length)) { + input = ''; + } else if (typeof input === 'object' && input.toString) { + input = input.toString(); + } else if (typeof input !== 'string') { + input += ''; } + return input; + }; - //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: - //Google - 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(/]*?)(>|$)/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|]*?)(\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|/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: - //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) { + validator.toDate = function (date) { + if (Object.prototype.toString.call(date) === '[object 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; + date = Date.parse(date); + return !isNaN(date) ? new Date(date) : null; }; - 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.toFloat = function (str) { + return parseFloat(str); }; - 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; - } + validator.toInt = function (str, radix) { + return parseInt(str, radix || 10); + }; - //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'); + validator.toBoolean = function (str, strict) { + if (strict) { + return str === '1' || str === 'true'; } - // 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 { + return str !== '0' && str !== 'false' && str !== ''; + }; + + validator.flatten = function (array, separator) { + if (!array) { + return ''; + } + var str = array[0]; + for (var i = 1; i < array.length; i++) { + str += separator + array[i]; + } + return str; + }; + + validator.merge = function (obj, defaults) { + obj = obj || {}; + for (var key in defaults) { + if (typeof obj[key] === 'undefined') { + obj[key] = defaults[key]; + } + } + return obj; + }; + + validator.equals = function (str, comparison) { + return str === validator.toString(comparison); + }; + + validator.contains = function (str, elem) { + return str.indexOf(validator.toString(elem)) >= 0; + }; + + validator.matches = function (str, pattern, modifiers) { + if (Object.prototype.toString.call(pattern) !== '[object RegExp]') { + pattern = new RegExp(pattern, modifiers); + } + return pattern.test(str); + }; + + validator.isEmail = function (str) { + return email.test(str); + }; + + var default_url_options = { + protocols: [ 'http', 'https', 'ftp' ] + , require_tld: true + , require_protocol: false + }; + + validator.isURL = function (str, options) { + options = validator.merge(options, default_url_options); + var url = new RegExp('^(?!mailto:)(?:(?:' + validator.flatten(options.protocols, '|') + ')://)' + (options.require_protocol ? '' : '?') + '(?:\\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}(?:\\.(?:[0-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,}))' + (options.require_tld ? '' : '?') + ')|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$', 'i'); + return str.length < 2083 && url.test(str); + }; + + validator.isIP = function (str, version) { + version = validator.toString(version); + if (!version) { + return validator.isIP(str, 4) || validator.isIP(str, 6); + } else if (version === '4') { + if (!ipv4Maybe.test(str)) { + return false; + } + var parts = str.split('.').sort(); + return parts[3] <= 255; + } + return version === '6' && ipv6.test(str); + }; + + validator.isAlpha = function (str) { + return alpha.test(str); + }; + + validator.isAlphanumeric = function (str) { + return alphanumeric.test(str); + }; + + validator.isNumeric = function (str) { + return numeric.test(str); + }; + + validator.isHexadecimal = function (str) { + return hexadecimal.test(str); + }; + + validator.isHexColor = function (str) { + return hexcolor.test(str); + }; + + validator.isLowercase = function (str) { + return str === str.toLowerCase(); + }; + + validator.isUppercase = function (str) { + return str === str.toUpperCase(); + }; + + validator.isInt = function (str) { + return int.test(str); + }; + + validator.isFloat = function (str) { + return str !== '' && float.test(str); + }; + + validator.isDivisibleBy = function (str, num) { + return validator.toFloat(str) % validator.toInt(num) === 0; + }; + + validator.isNull = function (str) { + return str.length === 0; + }; + + validator.isLength = function (str, min, max) { + return str.length >= min && (typeof max === 'undefined' || str.length <= max); + }; + + validator.isUUID = function (str, version) { + var pattern = uuid[version ? version : 'all']; + return pattern && pattern.test(str); + }; + + validator.isDate = function (str) { + return !isNaN(Date.parse(str)); + }; + + validator.isAfter = function (str, date) { + var comparison = validator.toDate(date || new Date()) + , original = validator.toDate(str); + return original && comparison && original > comparison; + }; + + validator.isBefore = function (str, date) { + var comparison = validator.toDate(date || new Date()) + , original = validator.toDate(str); + return original && comparison && original < comparison; + }; + + validator.isIn = function (str, options) { + if (!options || typeof options.indexOf !== 'function') { + return false; + } + if (Object.prototype.toString.call(options) === '[object Array]') { + var array = []; + for (var i = 0, len = options.length; i < len; i++) { + array[i] = validator.toString(options[i]); + } + options = array; + } + return options.indexOf(str) >= 0; + }; + + validator.isCreditCard = function (str) { + var sanitized = str.replace(/[^0-9]+/g, ''); + if (!creditCard.test(sanitized)) { + return false; + } + var sum = 0, digit, tmpNum, shouldDouble; + for (var i = sanitized.length - 1; i >= 0; i--) { + digit = sanitized.substring(i, (i + 1)); + tmpNum = parseInt(digit, 10); + if (shouldDouble) { + tmpNum *= 2; + if (tmpNum >= 10) { + sum += ((tmpNum % 10) + 1); + } else { sum += tmpNum; } - if (shouldDouble) { - shouldDouble = false; - } - else { - shouldDouble = true; - } + } else { + sum += tmpNum; } - 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'); + shouldDouble = !shouldDouble; } - 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, '>')); - return this.str; + return (sum % 10) === 0 ? sanitized : false; }; - 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); + validator.isISBN = function (str, version) { + version = validator.toString(version); + if (!version) { + return validator.isISBN(str, 10) || validator.isISBN(str, 13); } - 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); + var sanitized = str.replace(/[\s-]+/g, '') + , checksum = 0, i; + if (version === '10') { + if (!isbn10Maybe.test(sanitized)) { + return false; + } + for (i = 0; i < 9; i++) { + checksum += (i + 1) * sanitized.charAt(i); + } + if (sanitized.charAt(9) === 'X') { + checksum += 10 * 10; + } else { + checksum += 10 * sanitized.charAt(9); + } + if ((checksum % 11) === 0) { + return sanitized; + } + } else if (version === '13') { + if (!isbn13Maybe.test(sanitized)) { + return false; + } + var factor = [ 1, 3 ]; + for (i = 0; i < 12; i++) { + checksum += factor[i % 2] * sanitized.charAt(i); + } + if (sanitized.charAt(12) - ((10 - (checksum % 10)) % 10) === 0) { + return sanitized; + } } - return this.str; - } + return false; + }; - //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); + validator.isJSON = function (str) { + try { + JSON.parse(str); + } catch (e) { + if (e instanceof SyntaxError) { + return false; + } } - return this.str; - } + return true; + }; - //Quick access methods - exports.sanitize = exports.convert = function(str) { - var filter = new exports.Filter(); - return filter.sanitize(str); - } + validator.ltrim = function (str, chars) { + var pattern = chars ? new RegExp('^[' + chars + ']+', 'g') : /^\s+/g; + return str.replace(pattern, ''); + }; - exports.check = exports.validate = exports.assert = function(str, fail_msg) { - var validator = new exports.Validator(); - return validator.check(str, fail_msg); - } + validator.rtrim = function (str, chars) { + var pattern = chars ? new RegExp('[' + chars + ']+$', 'g') : /\s+$/g; + return str.replace(pattern, ''); + }; - return exports; + validator.trim = function (str, chars) { + var pattern = chars ? new RegExp('^[' + chars + ']+|[' + chars + ']+$', 'g') : /^\s+|\s+$/g; + return str.replace(pattern, ''); + }; -})); + validator.escape = function (str) { + return (str.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>')); + }; + + validator.whitelist = function (str, chars) { + return str.replace(new RegExp('[^' + chars + ']+', 'g'), ''); + }; + + validator.blacklist = function (str, chars) { + return str.replace(new RegExp('[' + chars + ']+', 'g'), ''); + }; + + validator.init(); + + return validator; + +}); diff --git a/core/client/init.js b/core/client/init.js index 243eefb62b..9f4bda35dc 100644 --- a/core/client/init.js +++ b/core/client/init.js @@ -1,4 +1,4 @@ -/*globals window, $, _, Backbone, Validator */ +/*globals window, $, _, Backbone, validator */ (function () { 'use strict'; @@ -17,7 +17,6 @@ Views : {}, Collections : {}, Models : {}, - Validate : new Validator(), paths: ghostPaths(), @@ -62,21 +61,16 @@ }); }; - Ghost.Validate.error = function (object) { - this._errors.push(object); - - return this; - }; - - Ghost.Validate.handleErrors = function () { + validator.handleErrors = function (errors) { Ghost.notifications.clearEverything(); - _.each(Ghost.Validate._errors, function (errorObj) { + _.each(errors, function (errorObj) { Ghost.notifications.addItem({ type: 'error', message: errorObj.message || errorObj, status: 'passive' }); + if (errorObj.hasOwnProperty('el')) { errorObj.el.addClass('input-error'); } diff --git a/core/client/views/login.js b/core/client/views/login.js index 6f0c946a1c..d4a8886d57 100644 --- a/core/client/views/login.js +++ b/core/client/views/login.js @@ -1,4 +1,4 @@ -/*global window, Ghost, $ */ +/*global window, Ghost, $, validator */ (function () { "use strict"; @@ -25,14 +25,19 @@ event.preventDefault(); var email = this.$el.find('.email').val(), password = this.$el.find('.password').val(), - redirect = Ghost.Views.Utils.getUrlVariables().r; + redirect = Ghost.Views.Utils.getUrlVariables().r, + validationErrors = []; - Ghost.Validate._errors = []; - Ghost.Validate.check(email).isEmail(); - Ghost.Validate.check(password, "Please enter a password").len(0); + if (!validator.isEmail(email)) { + validationErrors.push("Invalid Email"); + } - if (Ghost.Validate._errors.length > 0) { - Ghost.Validate.handleErrors(); + if (!validator.isLength(password, 0)) { + validationErrors.push("Please enter a password"); + } + + if (validationErrors.length) { + validator.handleErrors(validationErrors); } else { $.ajax({ url: Ghost.paths.subdir + '/ghost/signin/', @@ -88,18 +93,27 @@ event.preventDefault(); var name = this.$('.name').val(), email = this.$('.email').val(), - password = this.$('.password').val(); + password = this.$('.password').val(), + validationErrors = []; - // This is needed due to how error handling is done. If this is not here, there will not be a time - // when there is no error. - Ghost.Validate._errors = []; - Ghost.Validate.check(name, "Please enter a name").len(1); - Ghost.Validate.check(email, "Please enter a correct email address").isEmail(); - Ghost.Validate.check(password, "Your password is not long enough. It must be at least 8 characters long.").len(8); - Ghost.Validate.check(this.submitted, "Ghost is signing you up. Please wait...").equals("no"); + if (!validator.isLength(name, 1)) { + validationErrors.push("Please enter a name."); + } - if (Ghost.Validate._errors.length > 0) { - Ghost.Validate.handleErrors(); + if (!validator.isEmail(email)) { + validationErrors.push("Please enter a correct email address."); + } + + if (!validator.isLength(password, 0)) { + validationErrors.push("Please enter a password"); + } + + if (!validator.equals(this.submitted, "no")) { + validationErrors.push("Ghost is signing you up. Please wait..."); + } + + if (validationErrors.length) { + validator.handleErrors(validationErrors); } else { this.submitted = "yes"; $.ajax({ @@ -152,13 +166,15 @@ submitHandler: function (event) { event.preventDefault(); - var email = this.$el.find('.email').val(); + var email = this.$el.find('.email').val(), + validationErrors = []; - Ghost.Validate._errors = []; - Ghost.Validate.check(email).isEmail(); + if (!validator.isEmail(email)) { + validationErrors.push("Please enter a correct email address."); + } - if (Ghost.Validate._errors.length > 0) { - Ghost.Validate.handleErrors(); + if (validationErrors.length) { + validator.handleErrors(validationErrors); } else { $.ajax({ url: Ghost.paths.subdir + '/ghost/forgotten/', diff --git a/core/client/views/settings.js b/core/client/views/settings.js index 8540d070ae..db22ca3d2a 100644 --- a/core/client/views/settings.js +++ b/core/client/views/settings.js @@ -1,4 +1,4 @@ -/*global document, Ghost, $, _, Countable */ +/*global document, Ghost, $, _, Countable, validator */ (function () { "use strict"; @@ -160,28 +160,32 @@ description = this.$('#blog-description').val(), email = this.$('#email-address').val(), postsPerPage = this.$('#postsPerPage').val(), - permalinks = this.$('#permalinks').is(':checked') ? '/:year/:month/:day/:slug/' : '/:slug/'; + permalinks = this.$('#permalinks').is(':checked') ? '/:year/:month/:day/:slug/' : '/:slug/', + validationErrors = []; - Ghost.Validate._errors = []; - Ghost.Validate - .check(title, {message: "Title is too long", el: $('#blog-title')}) - .len(0, 150); - Ghost.Validate - .check(description, {message: "Description is too long", el: $('#blog-description')}) - .len(0, 200); - Ghost.Validate - .check(email, {message: "Please supply a valid email address", el: $('#email-address')}) - .isEmail().len(0, 254); - Ghost.Validate - .check(postsPerPage, {message: "Please use a number less than 1000", el: $('postsPerPage')}) - .isInt().max(1000); - Ghost.Validate - .check(postsPerPage, {message: "Please use a number greater than 0", el: $('postsPerPage')}) - .isInt().min(0); + if (!validator.isLength(title, 0, 150)) { + validationErrors.push({message: "Title is too long", el: $('#blog-title')}); + } + + if (!validator.isLength(description, 0, 200)) { + validationErrors.push({message: "Description is too long", el: $('#blog-description')}); + } + + if (!validator.isEmail(email) || !validator.isLength(email, 0, 254)) { + validationErrors.push({message: "Please supply a valid email address", el: $('#email-address')}); + } + + if (!validator.isInt(postsPerPage) || postsPerPage > 1000) { + validationErrors.push({message: "Please use a number less than 1000", el: $('postsPerPage')}); + } + + if (!validator.isInt(postsPerPage) || postsPerPage < 0) { + validationErrors.push({message: "Please use a number greater than 0", el: $('postsPerPage')}); + } - if (Ghost.Validate._errors.length > 0) { - Ghost.Validate.handleErrors(); + if (validationErrors.length) { + validator.handleErrors(validationErrors); } else { this.model.save({ title: title, @@ -343,30 +347,33 @@ userEmail = this.$('#user-email').val(), userLocation = this.$('#user-location').val(), userWebsite = this.$('#user-website').val(), - userBio = this.$('#user-bio').val(); + userBio = this.$('#user-bio').val(), + validationErrors = []; - Ghost.Validate._errors = []; - Ghost.Validate - .check(userName, {message: "Name is too long", el: $('#user-name')}) - .len(0, 150); - Ghost.Validate - .check(userBio, {message: "Bio is too long", el: $('#user-bio')}) - .len(0, 200); - Ghost.Validate - .check(userEmail, {message: "Please supply a valid email address", el: $('#user-email')}) - .isEmail(); - Ghost.Validate - .check(userLocation, {message: "Location is too long", el: $('#user-location')}) - .len(0, 150); - if (userWebsite.length > 0) { - Ghost.Validate - .check(userWebsite, {message: "Please use a valid url", el: $('#user-website')}) - .isUrl() - .len(0, 2000); + if (!validator.isLength(userName, 0, 150)) { + validationErrors.push({message: "Name is too long", el: $('#user-name')}); } - if (Ghost.Validate._errors.length > 0) { - Ghost.Validate.handleErrors(); + if (!validator.isLength(userBio, 0, 200)) { + validationErrors.push({message: "Bio is too long", el: $('#user-bio')}); + } + + if (!validator.isEmail(userEmail)) { + validationErrors.push({message: "Please supply a valid email address", el: $('#user-email')}); + } + + if (!validator.isLength(userLocation, 0, 150)) { + validationErrors.push({message: "Location is too long", el: $('#user-location')}); + } + + if (userWebsite.length) { + if (!validator.isURL(userWebsite) || !validator.isLength(userWebsite, 0, 2000)) { + validationErrors.push({message: "Please use a valid url", el: $('#user-website')}); + } + } + + if (validationErrors.length) { + validator.handleErrors(validationErrors); } else { this.model.save({ @@ -389,16 +396,20 @@ var self = this, oldPassword = this.$('#user-password-old').val(), newPassword = this.$('#user-password-new').val(), - ne2Password = this.$('#user-new-password-verification').val(); + ne2Password = this.$('#user-new-password-verification').val(), + validationErrors = []; - Ghost.Validate._errors = []; - Ghost.Validate.check(newPassword, {message: 'Your new passwords do not match'}).equals(ne2Password); - Ghost.Validate.check(newPassword, {message: 'Your password is not long enough. It must be at least 8 characters long.'}).len(8); + if (!validator.equals(newPassword, ne2Password)) { + validationErrors.push("Your new passwords do not match"); + } - if (Ghost.Validate._errors.length > 0) { - Ghost.Validate.handleErrors(); + if (!validator.isLength(newPassword, 8)) { + validationErrors.push("Your password is not long enough. It must be at least 8 characters long."); + } + + if (validationErrors.length) { + validator.handleErrors(validationErrors); } else { - $.ajax({ url: Ghost.paths.subdir + '/ghost/changepw/', type: 'POST', diff --git a/core/server/data/default-settings.json b/core/server/data/default-settings.json index 68c714bc57..64b293ec69 100644 --- a/core/server/data/default-settings.json +++ b/core/server/data/default-settings.json @@ -23,7 +23,7 @@ "email": { "defaultValue": "ghost@example.com", "validations": { - "notNull": true, + "isNull": false, "isEmail": true } }, @@ -36,29 +36,29 @@ "defaultLang": { "defaultValue": "en_US", "validations": { - "notNull": true + "isNull": false } }, "postsPerPage": { "defaultValue": "6", "validations": { - "notNull": true, + "isNull": false, "isInt": true, - "max": 1000 + "isLength": [0, 1000] } }, "forceI18n": { "defaultValue": "true", "validations": { - "notNull": true, - "isIn": ["true", "false"] + "isNull": false, + "isIn": [["true", "false"]] } }, "permalinks": { "defaultValue": "/:slug/", "validations": { - "is": "^(\/:?[a-z0-9_-]+){1,5}\/$", - "regex": "(:id|:slug|:year|:month|:day)", + "matches": "^(\/:?[a-z0-9_-]+){1,5}\/$", + "matches": "(:id|:slug|:year|:month|:day)", "notContains": "/ghost/" } } diff --git a/core/server/data/schema.js b/core/server/data/schema.js index 1734d0d1f9..da96380043 100644 --- a/core/server/data/schema.js +++ b/core/server/data/schema.js @@ -8,7 +8,7 @@ var db = { html: {type: 'text', maxlength: 16777215, fieldtype: 'medium', nullable: true}, image: {type: 'text', maxlength: 2000, nullable: true}, featured: {type: 'bool', nullable: false, defaultTo: false}, - page: {type: 'bool', nullable: false, defaultTo: false, validations: {'isIn': ['true', 'false']}}, + page: {type: 'bool', nullable: false, defaultTo: false, validations: {'isIn': [['true', 'false']]}}, status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'draft'}, language: {type: 'string', maxlength: 6, nullable: false, defaultTo: 'en_US'}, meta_title: {type: 'string', maxlength: 150, nullable: true}, @@ -31,7 +31,7 @@ var db = { image: {type: 'text', maxlength: 2000, nullable: true}, cover: {type: 'text', maxlength: 2000, nullable: true}, bio: {type: 'string', maxlength: 200, nullable: true}, - website: {type: 'text', maxlength: 2000, nullable: true, validations: {'isUrl': true}}, + website: {type: 'text', maxlength: 2000, nullable: true, validations: {'isURL': true}}, location: {type: 'text', maxlength: 65535, nullable: true}, accessibility: {type: 'text', maxlength: 65535, nullable: true}, status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'active'}, @@ -91,7 +91,7 @@ var db = { uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}}, key: {type: 'string', maxlength: 150, nullable: false, unique: true}, value: {type: 'text', maxlength: 65535, nullable: true}, - type: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'core', validations: {'isIn': ['core', 'blog', 'theme', 'app', 'plugin']}}, + type: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'core', validations: {'isIn': [['core', 'blog', 'theme', 'app', 'plugin']]}}, created_at: {type: 'dateTime', nullable: false}, created_by: {type: 'integer', nullable: false}, updated_at: {type: 'dateTime', nullable: true}, diff --git a/core/server/data/validation/index.js b/core/server/data/validation/index.js index 306b959680..79e627cc17 100644 --- a/core/server/data/validation/index.js +++ b/core/server/data/validation/index.js @@ -6,6 +6,16 @@ var schema = require('../schema').tables, validateSettings, validate; +// Provide a few custom validators +// +validator.extend('empty', function (str) { + return _.isEmpty(str); +}); + +validator.extend('notContains', function (str, badString) { + return !_.contains(str, badString); +}); + // Validation validation against schema attributes // values are checked against the validation objects // form schema.js @@ -16,17 +26,18 @@ validateSchema = function (tableName, model) { // check nullable if (model.hasOwnProperty(columnKey) && schema[tableName][columnKey].hasOwnProperty('nullable') && schema[tableName][columnKey].nullable !== true) { - validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey + - '] cannot be blank.').notNull(); - validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey + - '] cannot be blank.').notEmpty(); + if (validator.isNull(model[columnKey]) || validator.empty(model[columnKey])) { + throw new Error('Value in [' + tableName + '.' + columnKey + '] cannot be blank.'); + } } // TODO: check if mandatory values should be enforced if (model[columnKey]) { // check length if (schema[tableName][columnKey].hasOwnProperty('maxlength')) { - validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey + - '] exceeds maximum length of %2 characters.').len(0, schema[tableName][columnKey].maxlength); + if (!validator.isLength(model[columnKey], 0, schema[tableName][columnKey].maxlength)) { + throw new Error('Value in [' + tableName + '.' + columnKey + + '] exceeds maximum length of ' + schema[tableName][columnKey].maxlength + ' characters.'); + } } //check validations objects @@ -36,9 +47,8 @@ validateSchema = function (tableName, model) { //check type if (schema[tableName][columnKey].hasOwnProperty('type')) { - if (schema[tableName][columnKey].type === 'integer') { - validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey + - '] is no valid integer.' + model[columnKey]).isInt(); + if (schema[tableName][columnKey].type === 'integer' && !validator.isInt(model[columnKey])) { + throw new Error('Value in [' + tableName + '.' + columnKey + '] is no valid integer.'); } } } @@ -57,29 +67,42 @@ validateSettings = function (defaultSettings, model) { } }; -// Validate using the validation module. -// Each validation's key is a name and its value is an array of options -// Use true (boolean) if options aren't applicable +// Validate default settings using the validator module. +// Each validation's key is a method name and its value is an array of options // // eg: -// validations: { isUrl: true, len: [20, 40] } +// validations: { isUrl: true, isLength: [20, 40] } // -// will validate that a values's length is a URL between 20 and 40 chars, -// available validators: https://github.com/chriso/node-validator#list-of-validation-methods +// will validate that a setting's length is a URL between 20 and 40 chars. +// +// If you pass a boolean as the value, it will specify the "good" result. By default +// the "good" result is assumed to be true. +// +// eg: +// validations: { isNull: false } // means the "good" result would +// // fail the `isNull` check, so +// // not null. +// +// available validators: https://github.com/chriso/validator.js#validators validate = function (value, key, validations) { _.each(validations, function (validationOptions, validationName) { - var validation = validator.check(value, 'Validation [' + validationName + '] of field [' + key + '] failed.'); + var goodResult = true; - if (validationOptions === true) { - validationOptions = null; - } - /* jshint ignore:start */ - if (typeof validationOptions !== 'array') { + if (_.isBoolean(validationOptions)) { + goodResult = validationOptions; + validationOptions = []; + } else if (!_.isArray(validationOptions)) { validationOptions = [validationOptions]; } - /* jshint ignore:end */ - // equivalent of validation.isSomething(option1, option2) - validation[validationName].apply(validation, validationOptions); + + validationOptions.unshift(value); + + // equivalent of validator.isSomething(option1, option2) + if (validator[validationName].apply(validator, validationOptions) !== goodResult) { + throw new Error('Settings validation (' + validationName + ') failed for ' + key); + } + + validationOptions.shift(); }, this); }; diff --git a/core/server/models/user.js b/core/server/models/user.js index cf6c544d58..b1308ca128 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -17,7 +17,9 @@ var _ = require('lodash'), function validatePasswordLength(password) { try { - validator.check(password, "Your password must be at least 8 characters long.").len(8); + if (!validator.isLength(password, 8)) { + throw new Error('Your password must be at least 8 characters long.'); + } } catch (error) { return when.reject(error); } diff --git a/core/test/functional/admin/editor_test.js b/core/test/functional/admin/editor_test.js index f4b0419778..5ae016b9ea 100644 --- a/core/test/functional/admin/editor_test.js +++ b/core/test/functional/admin/editor_test.js @@ -91,6 +91,27 @@ CasperTest.begin("Word count and plurality", 4, function suite(test) { }); }); +CasperTest.begin('Required Title', 4, function suite(test) { + casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + test.assertUrlMatch(/ghost\/editor\/$/, "Ghost doesn't require login this time"); + }); + + casper.waitForSelector('#entry-title', function then() { + test.assertEvalEquals(function() { + return document.getElementById('entry-title').value; + }, '', 'Title is empty'); + }); + + casper.thenClick('.js-publish-button'); // Safe to assume draft mode? + + casper.waitForSelectorTextChange('.notification-error', function onSuccess() { + test.assertSelectorHasText('.notification-error', 'must specify a title'); + }, function onTimeout() { + test.fail('Title required error did not appear'); + }, 2000); +}); + CasperTest.begin('Title Trimming', 2, function suite(test) { var untrimmedTitle = ' test title ', trimmedTitle = 'test title'; diff --git a/core/test/functional/admin/login_test.js b/core/test/functional/admin/login_test.js index 394532f71f..df429176eb 100644 --- a/core/test/functional/admin/login_test.js +++ b/core/test/functional/admin/login_test.js @@ -130,3 +130,24 @@ CasperTest.begin("Can login to Ghost", 4, function suite(test) { test.fail('Failed to load ghost/ resource'); }); }, true); + +CasperTest.begin('Ensure email field form validation', 1, function suite(test) { + casper.thenOpen(url + 'ghost/signin/'); + + casper.waitForOpaque(".js-login-box", + function then() { + this.fill("form.login-form", { + 'email': 'notanemail' + }, true); + }, + function onTimeout() { + test.fail('Login form didn\'t fade in.'); + }); + + casper.waitForSelectorTextChange('.notification-error', function onSuccess() { + test.assertSelectorHasText('.notification-error', 'Invalid Email'); + }, function onTimeout() { + test.fail('Email validation error did not appear'); + }, 2000); + +}, true); diff --git a/core/test/functional/admin/settings_test.js b/core/test/functional/admin/settings_test.js index 1b3fb0e55a..ea6bfa6b88 100644 --- a/core/test/functional/admin/settings_test.js +++ b/core/test/functional/admin/settings_test.js @@ -112,6 +112,111 @@ CasperTest.begin("Settings screen is correct", 18, function suite(test) { }); }); +CasperTest.begin('Ensure general blog title field length validation', 3, function suite(test) { + casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time"); + }); + + casper.waitForSelector('#general', function then() { + this.fill("form#settings-general", { + 'general[title]': new Array(152).join('a') + }); + }); + + casper.thenClick('#general .button-save'); + + casper.waitForSelectorTextChange('.notification-error', function onSuccess() { + test.assertSelectorHasText('.notification-error', 'too long'); + }, function onTimeout() { + test.fail('Blog title length error did not appear'); + }, 2000); +}); + +CasperTest.begin('Ensure general blog description field length validation', 3, function suite(test) { + casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time"); + }); + + casper.waitForSelector('#general', function then() { + this.fillSelectors("form#settings-general", { + '#blog-description': new Array(202).join('a') + }); + }); + + casper.thenClick('#general .button-save'); + + casper.waitForSelectorTextChange('.notification-error', function onSuccess() { + test.assertSelectorHasText('.notification-error', 'too long'); + }, function onTimeout() { + test.fail('Blog description length error did not appear'); + }, 2000); +}); + +CasperTest.begin('Ensure postsPerPage number field form validation', 3, function suite(test) { + casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time"); + }); + + casper.waitForSelector('#general', function then() { + this.fill("form#settings-general", { + 'general[postsPerPage]': 'notaninteger' + }); + }); + + casper.thenClick('#general .button-save'); + + casper.waitForSelectorTextChange('.notification-error', function onSuccess() { + test.assertSelectorHasText('.notification-error', 'use a number'); + }, function onTimeout() { + test.fail('postsPerPage error did not appear'); + }, 2000); +}); + +CasperTest.begin('Ensure postsPerPage max of 1000', 3, function suite(test) { + casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time"); + }); + + casper.waitForSelector('#general', function then() { + this.fill("form#settings-general", { + 'general[postsPerPage]': '1001' + }); + }); + + casper.thenClick('#general .button-save'); + + casper.waitForSelectorTextChange('.notification-error', function onSuccess() { + test.assertSelectorHasText('.notification-error', 'use a number less than 1000'); + }, function onTimeout() { + test.fail('postsPerPage max error did not appear'); + }, 2000); +}); + +CasperTest.begin('Ensure postsPerPage min of 0', 3, function suite(test) { + casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time"); + }); + + casper.waitForSelector('#general', function then() { + this.fill("form#settings-general", { + 'general[postsPerPage]': '-1' + }); + }); + + casper.thenClick('#general .button-save'); + + casper.waitForSelectorTextChange('.notification-error', function onSuccess() { + test.assertSelectorHasText('.notification-error', 'use a number greater than 0'); + }, function onTimeout() { + test.fail('postsPerPage min error did not appear'); + }, 2000); +}); + CasperTest.begin("User settings screen validates email", 6, function suite(test) { var email, brokenEmail; @@ -182,4 +287,67 @@ CasperTest.begin("User settings screen shows remaining characters for Bio proper casper.then(function checkCharacterCount() { test.assert(getRemainingBioCharacterCount() === '195', 'Bio remaining characters is 195'); }); -}); \ No newline at end of file +}); + +CasperTest.begin('Ensure user bio field length validation', 3, function suite(test) { + casper.thenOpen(url + "ghost/settings/user/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + test.assertUrlMatch(/ghost\/settings\/user\/$/, "Ghost doesn't require login this time"); + }); + + casper.waitForSelector('#user', function then() { + this.fillSelectors("form.user-profile", { + '#user-bio': new Array(202).join('a') + }); + }); + + casper.thenClick('#user .button-save'); + + casper.waitForSelectorTextChange('.notification-error', function onSuccess() { + test.assertSelectorHasText('.notification-error', 'is too long'); + }, function onTimeout() { + test.fail('Bio field length error did not appear'); + }, 2000); +}); + +CasperTest.begin('Ensure user url field validation', 3, function suite(test) { + casper.thenOpen(url + "ghost/settings/user/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + test.assertUrlMatch(/ghost\/settings\/user\/$/, "Ghost doesn't require login this time"); + }); + + casper.waitForSelector('#user', function then() { + this.fillSelectors("form.user-profile", { + '#user-website': 'notaurl' + }); + }); + + casper.thenClick('#user .button-save'); + + casper.waitForSelectorTextChange('.notification-error', function onSuccess() { + test.assertSelectorHasText('.notification-error', 'use a valid url'); + }, function onTimeout() { + test.fail('Url validation error did not appear'); + }, 2000); +}); + +CasperTest.begin('Ensure user location field length validation', 3, function suite(test) { + casper.thenOpen(url + "ghost/settings/user/", function testTitleAndUrl() { + test.assertTitle("Ghost Admin", "Ghost admin has no title"); + test.assertUrlMatch(/ghost\/settings\/user\/$/, "Ghost doesn't require login this time"); + }); + + casper.waitForSelector('#user', function then() { + this.fillSelectors("form.user-profile", { + '#user-location': new Array(1002).join('a') + }); + }); + + casper.thenClick('#user .button-save'); + + casper.waitForSelectorTextChange('.notification-error', function onSuccess() { + test.assertSelectorHasText('.notification-error', 'is too long'); + }, function onTimeout() { + test.fail('Location field length error did not appear'); + }, 2000); +}); diff --git a/package.json b/package.json index 4c9b37bc50..9f6881fcf1 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "showdown": "0.3.1", "sqlite3": "2.2.0", "unidecode": "0.1.3", - "validator": "1.4.0", + "validator": "3.4.0", "when": "2.7.0" }, "optionalDependencies": {