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 <cohara87@gmail.com>
+ * Copyright (c) 2014 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
@@ -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 = {
-        '&nbsp;': '\u00a0',
-        '&iexcl;': '\u00a1',
-        '&cent;': '\u00a2',
-        '&pound;': '\u00a3',
-        '&curren;': '\u20ac',
-        '&yen;': '\u00a5',
-        '&brvbar;': '\u0160',
-        '&sect;': '\u00a7',
-        '&uml;': '\u0161',
-        '&copy;': '\u00a9',
-        '&ordf;': '\u00aa',
-        '&laquo;': '\u00ab',
-        '&not;': '\u00ac',
-        '&shy;': '\u00ad',
-        '&reg;': '\u00ae',
-        '&macr;': '\u00af',
-        '&deg;': '\u00b0',
-        '&plusmn;': '\u00b1',
-        '&sup2;': '\u00b2',
-        '&sup3;': '\u00b3',
-        '&acute;': '\u017d',
-        '&micro;': '\u00b5',
-        '&para;': '\u00b6',
-        '&middot;': '\u00b7',
-        '&cedil;': '\u017e',
-        '&sup1;': '\u00b9',
-        '&ordm;': '\u00ba',
-        '&raquo;': '\u00bb',
-        '&frac14;': '\u0152',
-        '&frac12;': '\u0153',
-        '&frac34;': '\u0178',
-        '&iquest;': '\u00bf',
-        '&Agrave;': '\u00c0',
-        '&Aacute;': '\u00c1',
-        '&Acirc;': '\u00c2',
-        '&Atilde;': '\u00c3',
-        '&Auml;': '\u00c4',
-        '&Aring;': '\u00c5',
-        '&AElig;': '\u00c6',
-        '&Ccedil;': '\u00c7',
-        '&Egrave;': '\u00c8',
-        '&Eacute;': '\u00c9',
-        '&Ecirc;': '\u00ca',
-        '&Euml;': '\u00cb',
-        '&Igrave;': '\u00cc',
-        '&Iacute;': '\u00cd',
-        '&Icirc;': '\u00ce',
-        '&Iuml;': '\u00cf',
-        '&ETH;': '\u00d0',
-        '&Ntilde;': '\u00d1',
-        '&Ograve;': '\u00d2',
-        '&Oacute;': '\u00d3',
-        '&Ocirc;': '\u00d4',
-        '&Otilde;': '\u00d5',
-        '&Ouml;': '\u00d6',
-        '&times;': '\u00d7',
-        '&Oslash;': '\u00d8',
-        '&Ugrave;': '\u00d9',
-        '&Uacute;': '\u00da',
-        '&Ucirc;': '\u00db',
-        '&Uuml;': '\u00dc',
-        '&Yacute;': '\u00dd',
-        '&THORN;': '\u00de',
-        '&szlig;': '\u00df',
-        '&agrave;': '\u00e0',
-        '&aacute;': '\u00e1',
-        '&acirc;': '\u00e2',
-        '&atilde;': '\u00e3',
-        '&auml;': '\u00e4',
-        '&aring;': '\u00e5',
-        '&aelig;': '\u00e6',
-        '&ccedil;': '\u00e7',
-        '&egrave;': '\u00e8',
-        '&eacute;': '\u00e9',
-        '&ecirc;': '\u00ea',
-        '&euml;': '\u00eb',
-        '&igrave;': '\u00ec',
-        '&iacute;': '\u00ed',
-        '&icirc;': '\u00ee',
-        '&iuml;': '\u00ef',
-        '&eth;': '\u00f0',
-        '&ntilde;': '\u00f1',
-        '&ograve;': '\u00f2',
-        '&oacute;': '\u00f3',
-        '&ocirc;': '\u00f4',
-        '&otilde;': '\u00f5',
-        '&ouml;': '\u00f6',
-        '&divide;': '\u00f7',
-        '&oslash;': '\u00f8',
-        '&ugrave;': '\u00f9',
-        '&uacute;': '\u00fa',
-        '&ucirc;': '\u00fb',
-        '&uuml;': '\u00fc',
-        '&yacute;': '\u00fd',
-        '&thorn;': '\u00fe',
-        '&yuml;': '\u00ff',
-        '&quot;': '\u0022',
-        '&lt;': '\u003c',
-        '&gt;': '\u003e',
-        '&apos;': '\u0027',
-        '&minus;': '\u2212',
-        '&circ;': '\u02c6',
-        '&tilde;': '\u02dc',
-        '&Scaron;': '\u0160',
-        '&lsaquo;': '\u2039',
-        '&OElig;': '\u0152',
-        '&lsquo;': '\u2018',
-        '&rsquo;': '\u2019',
-        '&ldquo;': '\u201c',
-        '&rdquo;': '\u201d',
-        '&bull;': '\u2022',
-        '&ndash;': '\u2013',
-        '&mdash;': '\u2014',
-        '&trade;': '\u2122',
-        '&scaron;': '\u0161',
-        '&rsaquo;': '\u203a',
-        '&oelig;': '\u0153',
-        '&Yuml;': '\u0178',
-        '&fnof;': '\u0192',
-        '&Alpha;': '\u0391',
-        '&Beta;': '\u0392',
-        '&Gamma;': '\u0393',
-        '&Delta;': '\u0394',
-        '&Epsilon;': '\u0395',
-        '&Zeta;': '\u0396',
-        '&Eta;': '\u0397',
-        '&Theta;': '\u0398',
-        '&Iota;': '\u0399',
-        '&Kappa;': '\u039a',
-        '&Lambda;': '\u039b',
-        '&Mu;': '\u039c',
-        '&Nu;': '\u039d',
-        '&Xi;': '\u039e',
-        '&Omicron;': '\u039f',
-        '&Pi;': '\u03a0',
-        '&Rho;': '\u03a1',
-        '&Sigma;': '\u03a3',
-        '&Tau;': '\u03a4',
-        '&Upsilon;': '\u03a5',
-        '&Phi;': '\u03a6',
-        '&Chi;': '\u03a7',
-        '&Psi;': '\u03a8',
-        '&Omega;': '\u03a9',
-        '&alpha;': '\u03b1',
-        '&beta;': '\u03b2',
-        '&gamma;': '\u03b3',
-        '&delta;': '\u03b4',
-        '&epsilon;': '\u03b5',
-        '&zeta;': '\u03b6',
-        '&eta;': '\u03b7',
-        '&theta;': '\u03b8',
-        '&iota;': '\u03b9',
-        '&kappa;': '\u03ba',
-        '&lambda;': '\u03bb',
-        '&mu;': '\u03bc',
-        '&nu;': '\u03bd',
-        '&xi;': '\u03be',
-        '&omicron;': '\u03bf',
-        '&pi;': '\u03c0',
-        '&rho;': '\u03c1',
-        '&sigmaf;': '\u03c2',
-        '&sigma;': '\u03c3',
-        '&tau;': '\u03c4',
-        '&upsilon;': '\u03c5',
-        '&phi;': '\u03c6',
-        '&chi;': '\u03c7',
-        '&psi;': '\u03c8',
-        '&omega;': '\u03c9',
-        '&thetasym;': '\u03d1',
-        '&upsih;': '\u03d2',
-        '&piv;': '\u03d6',
-        '&ensp;': '\u2002',
-        '&emsp;': '\u2003',
-        '&thinsp;': '\u2009',
-        '&zwnj;': '\u200c',
-        '&zwj;': '\u200d',
-        '&lrm;': '\u200e',
-        '&rlm;': '\u200f',
-        '&sbquo;': '\u201a',
-        '&bdquo;': '\u201e',
-        '&dagger;': '\u2020',
-        '&Dagger;': '\u2021',
-        '&hellip;': '\u2026',
-        '&permil;': '\u2030',
-        '&prime;': '\u2032',
-        '&Prime;': '\u2033',
-        '&oline;': '\u203e',
-        '&frasl;': '\u2044',
-        '&euro;': '\u20ac',
-        '&image;': '\u2111',
-        '&weierp;': '\u2118',
-        '&real;': '\u211c',
-        '&alefsym;': '\u2135',
-        '&larr;': '\u2190',
-        '&uarr;': '\u2191',
-        '&rarr;': '\u2192',
-        '&darr;': '\u2193',
-        '&harr;': '\u2194',
-        '&crarr;': '\u21b5',
-        '&lArr;': '\u21d0',
-        '&uArr;': '\u21d1',
-        '&rArr;': '\u21d2',
-        '&dArr;': '\u21d3',
-        '&hArr;': '\u21d4',
-        '&forall;': '\u2200',
-        '&part;': '\u2202',
-        '&exist;': '\u2203',
-        '&empty;': '\u2205',
-        '&nabla;': '\u2207',
-        '&isin;': '\u2208',
-        '&notin;': '\u2209',
-        '&ni;': '\u220b',
-        '&prod;': '\u220f',
-        '&sum;': '\u2211',
-        '&lowast;': '\u2217',
-        '&radic;': '\u221a',
-        '&prop;': '\u221d',
-        '&infin;': '\u221e',
-        '&ang;': '\u2220',
-        '&and;': '\u2227',
-        '&or;': '\u2228',
-        '&cap;': '\u2229',
-        '&cup;': '\u222a',
-        '&int;': '\u222b',
-        '&there4;': '\u2234',
-        '&sim;': '\u223c',
-        '&cong;': '\u2245',
-        '&asymp;': '\u2248',
-        '&ne;': '\u2260',
-        '&equiv;': '\u2261',
-        '&le;': '\u2264',
-        '&ge;': '\u2265',
-        '&sub;': '\u2282',
-        '&sup;': '\u2283',
-        '&nsub;': '\u2284',
-        '&sube;': '\u2286',
-        '&supe;': '\u2287',
-        '&oplus;': '\u2295',
-        '&otimes;': '\u2297',
-        '&perp;': '\u22a5',
-        '&sdot;': '\u22c5',
-        '&lceil;': '\u2308',
-        '&rceil;': '\u2309',
-        '&lfloor;': '\u230a',
-        '&rfloor;': '\u230b',
-        '&lang;': '\u2329',
-        '&rang;': '\u232a',
-        '&loz;': '\u25ca',
-        '&spades;': '\u2660',
-        '&clubs;': '\u2663',
-        '&hearts;': '\u2665',
-        '&diams;': '\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(/&amp;/g, '&');
-
-        return str;
-    }
-
-    var encode = function (str) {
-        str = str.replace(/&/g, '&amp;');
-
-        //IE doesn't accept &apos;
-        str = str.replace(/'/g, '&#39;');
-
-        //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':                 '',
-        '<!--':                         '&lt;!--',
-        '-->':                          '--&gt;',
-        '<![CDATA[':                    '&lt;![CDATA['
+    validator.extend = function (name, fn) {
+        validator[name] = function () {
+            var args = Array.prototype.slice.call(arguments);
+            args[0] = validator.toString(args[0]);
+            return fn.apply(validator, args);
+        };
     };
 
-    var never_allowed_regex = {
-        'javascript\\s*:':              '',
-        'expression\\s*(\\(|&\\#40;)':  '',
-        'vbscript\\s*:':                '',
-        'Redirect\\s+302':              ''
+    validator.noCoerce = ['toString', 'toDate', 'extend', 'init', 'flatten', 'merge'];
+
+    //Right before exporting the validator object, pass each of the builtins
+    //through extend() so that their first argument is coerced to a string
+    validator.init = function () {
+        for (var name in validator) {
+            if (typeof validator[name] !== 'function' || validator.noCoerce.indexOf(name) >= 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:
-        //<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: &lt;blink&gt;
-        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 '&lt;' + a + b + c + d.replace('>','&gt;').replace('<','&lt;');
-        });
-
-        //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&#40;'some code'&#41;
-        str = str.replace(/(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)/gi, '$1$2&#40;$3&#41;');
-
-        //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('>','&gt;').replace('<','&lt;').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, '&amp;')
-                            .replace(/"/g, '&quot;')
-                            .replace(/</g, '&lt;')
-                            .replace(/>/g, '&gt;'));
-        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, '&amp;')
+            .replace(/"/g, '&quot;')
+            .replace(/</g, '&lt;')
+            .replace(/>/g, '&gt;'));
+    };
+
+    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": {