├── .gitignore ├── test ├── mocha.opts ├── express.test.js ├── form.test.js ├── more.test.js ├── filter.test.js └── validate.test.js ├── index.js ├── .travis.yml ├── Changelog.md ├── LICENSE ├── package.json ├── lib ├── utils.js ├── form.js └── field.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui exports 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/form"); 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - "4" 6 | - "6" 7 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # express-form changelog 2 | 3 | ## v0.12.6 (2016/06/30) 4 | * Guard against request body not inheriting from Object.prototype 5 | 6 | ## v0.12.5 (2016/03/23) 7 | * remove upper-bound for TLD length on email validation 8 | 9 | ## v0.12.4 (2014/11/27) 10 | * update dependencies 11 | 12 | ## v0.12.3 (2014/07/29) 13 | * fix stack size explosion when using `autoTrim` 14 | 15 | ## v0.12.2 (2014/04/16) 16 | * add express 4 support 17 | 18 | ## v0.12.1 (2014/04/09) 19 | * tighten up newly-added `isEmail()` regex 20 | 21 | ## v0.12.0 (2014/03/03) 22 | * simplify isEmail() regex (only checks for @); fixes ReDos vuln. 23 | 24 | ## v0.10.0 (2013/10/23) 25 | * add support for asynchronous custom validators/filters 26 | 27 | ## v0.8.1 (2013/02/21) 28 | * cast to string for string-specific filters to prevent errors 29 | 30 | ## v0.8.0 (2013/02/16) 31 | * use express 3.x for peer and dev dependencies 32 | 33 | ## v0.7.1 (2013/02/16) 34 | * use express 2.x as peer dependency 35 | 36 | ## v0.7.0 (2013/02/13) 37 | * add express 3.x support 38 | * add express as peer dependency 39 | * add isDate validator 40 | * upgrade validator module to 0.4.x 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2010 Dan Dean 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the 'Software'), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Dan Dean (http://dandean.com)", 3 | "name": "express-form", 4 | "description": "Form validation and data filtering for Express", 5 | "version": "0.12.6", 6 | "homepage": "http://dandean.github.com/express-form", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/freewil/express-form.git" 10 | }, 11 | "contributors": [ 12 | "Marc Harter ", 13 | "Sugarstack", 14 | "Sean Lavine " 15 | ], 16 | "keywords": [ 17 | "form", 18 | "validator", 19 | "validation", 20 | "express" 21 | ], 22 | "dependencies": { 23 | "async": "^0.9.0", 24 | "object-additions": "^0.5.1", 25 | "validator": "^2.1.0" 26 | }, 27 | "peerDependencies": { 28 | "express": ">=3.0.0" 29 | }, 30 | "devDependencies": { 31 | "body-parser": "1.x", 32 | "express": "4.x", 33 | "mocha": "^2.0.1", 34 | "request": "^2.48.0" 35 | }, 36 | "main": "index", 37 | "bugs": { 38 | "url": "https://github.com/freewil/express-form/issues" 39 | }, 40 | "scripts": { 41 | "test": "mocha" 42 | }, 43 | "engines": { 44 | "node": ">=0.10.0" 45 | }, 46 | "licenses": [ 47 | { 48 | "type": "MIT", 49 | "url": "https://github.com/freewil/express-form/raw/master/LICENSE" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | // Convert square-bracket to dot notation. 2 | var toDotNotation = exports.toDotNotation = function (str) { 3 | return str.replace(/\[((.)*?)\]/g, ".$1"); 4 | }; 5 | 6 | // Gets nested properties without throwing errors. 7 | var getProp = exports.getProp = function (property, obj) { 8 | var levels = toDotNotation(property).split("."); 9 | 10 | while (obj != null && levels[0]) { 11 | obj = obj[levels.shift()]; 12 | if (obj == null) obj = ""; 13 | } 14 | 15 | return obj; 16 | } 17 | 18 | // Sets nested properties. 19 | var setProp = exports.setProp = function (property, obj, value) { 20 | var levels = toDotNotation(property).split("."); 21 | 22 | while (levels[0]) { 23 | var p = levels.shift(); 24 | if (typeof obj[p] !== "object") obj[p] = {}; 25 | if (!levels.length) obj[p] = value; 26 | obj = obj[p]; 27 | } 28 | 29 | return obj; 30 | } 31 | 32 | var clone = exports.clone = function (obj) { 33 | // Untested, probably better:-> return Object.create(obj).__proto__; 34 | return JSON.parse(JSON.stringify(obj)); 35 | } 36 | 37 | /** 38 | * camelize(str): -> String 39 | * - str (String): The string to make camel-case. 40 | * 41 | * Converts dash-separated words into camelCase words. Cribbed from Prototype.js. 42 | * 43 | * field-name -> fieldName 44 | * -field-name -> FieldName 45 | **/ 46 | var camelize = exports.camelize = function (str) { 47 | return (str || "").replace(/-+(.)?/g, function(match, chr) { 48 | return chr ? chr.toUpperCase() : ''; 49 | }); 50 | } 51 | 52 | /* 53 | * Recursively merge properties of two objects 54 | * http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically/383245#383245 55 | */ 56 | var merge = exports.merge = function (obj1, obj2) { 57 | for (var p in obj2) { 58 | try { 59 | // Property in destination object set; update its value. 60 | if ( obj2[p].constructor==Object ) { 61 | obj1[p] = merge(obj1[p], obj2[p]); 62 | } else { 63 | obj1[p] = obj2[p]; 64 | } 65 | } catch (e) { 66 | // Property in destination object not set; create it and set its value. 67 | obj1[p] = obj2[p]; 68 | } 69 | } 70 | return obj1; 71 | } 72 | 73 | var hasValue = exports.hasValue = function (value) { 74 | return !(undefined === value || null === value || "" === value); 75 | } -------------------------------------------------------------------------------- /test/express.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | form = require("../index"), 3 | filter = form.filter, 4 | validate = form.validate, 5 | express = require("express"), 6 | http = require("http"), 7 | request = require("request"), 8 | bodyParser = require("body-parser"), 9 | app = express(); 10 | 11 | http.createServer(app).listen(3000); 12 | 13 | // some duct-tape to make assert.response work with express 3.x 14 | app.address = function() { 15 | return {port: 3000}; 16 | }; 17 | app.close = function() { 18 | process.exit(0); 19 | }; 20 | 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.urlencoded({ extended: true })); 23 | 24 | module.exports = { 25 | 'express : middleware : valid-form': function(done) { 26 | app.post( 27 | '/user', 28 | form( 29 | filter("username").trim(), 30 | validate("username").required().is(/^[a-z]+$/), 31 | filter("password").trim(), 32 | validate("password").required().is(/^[0-9]+$/) 33 | ), 34 | function(req, res){ 35 | assert.strictEqual(req.form.username, "dandean"); 36 | assert.strictEqual(req.form.password, "12345"); 37 | assert.strictEqual(req.form.isValid, true); 38 | assert.strictEqual(req.form.errors.length, 0); 39 | res.send(JSON.stringify(req.form)); 40 | } 41 | ); 42 | 43 | request.post({ 44 | url: 'http://localhost:3000/user', 45 | method: 'POST', 46 | body: JSON.stringify({ 47 | username: " dandean \n\n\t", 48 | password: " 12345 " 49 | }), 50 | headers: { 'Content-Type': 'application/json' } 51 | }, function(err, res, body) { 52 | assert.ifError(err); 53 | assert.strictEqual(res.statusCode, 200); 54 | done(); 55 | }); 56 | }, 57 | 58 | 'express : middleware : merged-data': function(done) { 59 | app.post( 60 | '/user/:id', 61 | form( 62 | filter("id").toInt(), 63 | filter("stuff").toUpper(), 64 | filter("rad").toUpper() 65 | ), 66 | function(req, res){ 67 | // Validate filtered form data 68 | assert.strictEqual(req.form.id, 5); // from param 69 | assert.equal(req.form.stuff, "THINGS"); // from query param 70 | assert.equal(req.form.rad, "COOL"); // from body 71 | 72 | // Check that originl values are still in place 73 | assert.ok(typeof req.params.id, "string"); 74 | assert.equal(req.query.stuff, "things"); 75 | assert.equal(req.body.rad, "cool"); 76 | 77 | res.send(JSON.stringify(req.form)); 78 | } 79 | ); 80 | 81 | request({ 82 | url: 'http://localhost:3000/user/5?stuff=things&id=overridden', 83 | method: 'POST', 84 | body: JSON.stringify({ 85 | id: "overridden by url param", 86 | stuff: "overridden by query param", 87 | rad: "cool" 88 | }), 89 | headers: { 'Content-Type': 'application/json' } 90 | }, function(err, res, body) { 91 | assert.ifError(err); 92 | assert.strictEqual(res.statusCode, 200); 93 | done(); 94 | }); 95 | } 96 | 97 | 98 | }; -------------------------------------------------------------------------------- /test/form.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | form = require("../index"), 3 | validate = form.validate, 4 | utils = require('../lib/utils'); 5 | 6 | module.exports = { 7 | 'form : isValid': function() { 8 | // Failure. 9 | var request = { body: { field: "fail" }}; 10 | form(validate("field").isEmail())(request, {}); 11 | assert.strictEqual(request.form.isValid, false); 12 | 13 | // Success 14 | var request = { body: { field: "me@dandean.com" }}; 15 | form(validate("field").isEmail())(request, {}); 16 | assert.strictEqual(request.form.isValid, true); 17 | 18 | // form.isValid is a getter only 19 | request.form.isValid = false; 20 | assert.strictEqual(request.form.isValid, true); 21 | }, 22 | 23 | 'form : getErrors': function() { 24 | var request = { 25 | body: { 26 | field0: "win", 27 | field1: "fail", 28 | field2: "fail", 29 | field3: "fail" 30 | } 31 | }; 32 | 33 | form( 34 | validate("field0").equals("win"), 35 | validate("field1").isEmail(), 36 | validate("field2").isEmail().isUrl(), 37 | validate("field3").isEmail().isUrl().isIP() 38 | )(request, {}); 39 | 40 | assert.equal(request.form.isValid, false); 41 | assert.equal(request.form.errors.length, 6); 42 | 43 | assert.equal(request.form.getErrors("field0").length, 0); 44 | assert.equal(request.form.getErrors("field1").length, 1); 45 | assert.equal(request.form.getErrors("field2").length, 2); 46 | assert.equal(request.form.getErrors("field3").length, 3); 47 | }, 48 | 49 | 'form : configure : dataSources': function() { 50 | form.configure({ dataSources: 'other' }); 51 | 52 | var request = { other: { field: "me@dandean.com" }}; 53 | form(validate("field").isEmail())(request, {}); 54 | assert.strictEqual(request.form.isValid, true); 55 | assert.equal(request.form.field, "me@dandean.com"); 56 | 57 | form.configure({ dataSources: ['body', "query", "params"] }); 58 | }, 59 | 60 | 'form : configure : autoTrim': function() { 61 | // request with username field containing a trailing space 62 | var request = { 63 | body: { 64 | username: 'myuser1 ' 65 | } 66 | }; 67 | 68 | var request2 = utils.clone(request); 69 | 70 | // alphanumeric 71 | var regex = /^[0-9A-Z]+$/i 72 | 73 | // autoTrim defaults to false, test results with it off 74 | assert.strictEqual(form._options.autoTrim, false); 75 | form(validate('username').is(regex))(request, {}); 76 | assert.strictEqual(request.form.isValid, false); 77 | 78 | // test results with autoTrim turned on 79 | form.configure({ autoTrim: true }); 80 | assert.strictEqual(form._options.autoTrim, true); 81 | form(validate('username').is(regex))(request2, {}); 82 | assert.strictEqual(request2.form.isValid, true); 83 | assert.strictEqual(request2.form.username, 'myuser1'); 84 | 85 | // turn autoTrim back off 86 | form.configure({ autoTrim: false }); 87 | assert.strictEqual(form._options.autoTrim, false); 88 | } 89 | 90 | }; 91 | -------------------------------------------------------------------------------- /lib/form.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Express - Form 3 | * Copyright(c) 2010 Dan Dean 4 | * MIT Licensed 5 | */ 6 | 7 | var async = require("async") 8 | , utils = require("./utils") 9 | , Field = require("./field"); 10 | 11 | function form() { 12 | var routines = Array.prototype.slice.call(arguments) 13 | , options = form._options; 14 | 15 | return function (req, res, next) { 16 | var map = {} 17 | , flashed = {} 18 | , mergedSource = {}; 19 | 20 | if (!req.form) req.form = {}; 21 | 22 | options.dataSources.forEach(function (source) { 23 | utils.merge(mergedSource, req[source]); 24 | }); 25 | 26 | if (options.passThrough) req.form = utils.clone(mergedSource); 27 | 28 | if (options.autoLocals) { 29 | for (var prop in req.body) { 30 | if (!Object.hasOwnProperty.call(req.body, prop)) continue; 31 | 32 | /* 33 | * express 1.x and 3.x 34 | * ------------------------ 35 | * res.locals.field = value 36 | * 37 | * express 2.x 38 | * ------------------------ 39 | * res.local(field, value) 40 | * res.locals({field: value}); 41 | */ 42 | if (typeof res.local === "function") { 43 | // express 2.x 44 | res.local(utils.camelize(prop), req.body[prop]); 45 | } else { 46 | // express 1.x and 3.x 47 | if (!res.locals) res.locals = {}; 48 | res.locals[utils.camelize(prop)] = req.body[prop]; 49 | } 50 | } 51 | } 52 | 53 | Object.defineProperties(req.form, { 54 | "errors": { 55 | value: [], 56 | enumerable: false 57 | }, 58 | "getErrors": { 59 | value: function (name) { 60 | if(!name) return map; 61 | 62 | return map[name] || []; 63 | }, 64 | enumerable: false 65 | }, 66 | "isValid": { 67 | get: function () { 68 | return this.errors.length === 0; 69 | }, 70 | enumerable: false 71 | }, 72 | "flashErrors": { 73 | value: function () { 74 | if (typeof req.flash !== "function") return; 75 | this.errors.forEach(function (error) { 76 | if (flashed[error]) return; 77 | 78 | flashed[error] = true; 79 | req.flash("error", error); 80 | }); 81 | }, 82 | enumerable: false 83 | } 84 | }); 85 | 86 | //routines.forEach(function (routine) { 87 | async.each(routines, function(routine, cb) { 88 | routine.run(mergedSource, req.form, options, function(err, result) { 89 | 90 | // return early if no errors 91 | if (!Array.isArray(result) || !result.length) return cb(null); 92 | 93 | var errors = req.form.errors = req.form.errors || [] 94 | , name = routine.name; 95 | 96 | map[name] = map[name] || []; 97 | 98 | result.forEach(function (error) { 99 | errors.push(error); 100 | map[name].push(error); 101 | }); 102 | 103 | cb(null); 104 | 105 | }); 106 | }, function(err) { 107 | 108 | if (options.flashErrors) req.form.flashErrors(); 109 | if (next) next(); 110 | 111 | }); 112 | } 113 | } 114 | 115 | form.field = function (property, label) { 116 | return new Field(property, label); 117 | }; 118 | 119 | form.filter = form.validate = form.field; 120 | 121 | form._options = { 122 | dataSources: ["body", "query", "params"], 123 | autoTrim: false, 124 | autoLocals: true, 125 | passThrough: false, 126 | flashErrors: true 127 | }; 128 | 129 | form.configure = function (options) { 130 | for (var p in options) { 131 | if (!Array.isArray(options[p]) && p === "dataSources") { 132 | options[p] = [options[p]]; 133 | } 134 | this._options[p] = options[p]; 135 | } 136 | return this; 137 | } 138 | 139 | module.exports = form; -------------------------------------------------------------------------------- /test/more.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert") 2 | , form = require("../index") 3 | , field = form.field; 4 | 5 | module.exports = { 6 | 7 | "field : arrays": function () { 8 | // Array transformations. 9 | var request = { 10 | body: { 11 | field1: "", 12 | field2: "Hello!", 13 | field3: ["Alpacas?", "Llamas!!?", "Vicunas!", "Guanacos!!!"] 14 | } 15 | }; 16 | form( 17 | field("fieldx").array(), 18 | field("field1").array(), 19 | field("field2").array(), 20 | field("field3").array() 21 | )(request, {}); 22 | assert.strictEqual(Array.isArray(request.form.fieldx), true); 23 | assert.strictEqual(request.form.fieldx.length, 0); 24 | assert.strictEqual(Array.isArray(request.form.field1), true); 25 | assert.strictEqual(request.form.field1.length, 0); 26 | assert.strictEqual(request.form.field2[0], "Hello!"); 27 | assert.strictEqual(request.form.field2.length, 1); 28 | assert.strictEqual(request.form.field3[0], "Alpacas?"); 29 | assert.strictEqual(request.form.field3[1], "Llamas!!?"); 30 | assert.strictEqual(request.form.field3[2], "Vicunas!"); 31 | assert.strictEqual(request.form.field3[3], "Guanacos!!!"); 32 | assert.strictEqual(request.form.field3.length, 4); 33 | 34 | // No array flag! 35 | var request = { body: { field: ["red", "blue"] } }; 36 | form(field("field"))(request, {}); 37 | assert.strictEqual(request.form.field, "red"); 38 | 39 | // Iterate and filter array. 40 | var request = { body: { field: ["david", "stephen", "greg"] } }; 41 | form(field("field").array().toUpper())(request, {}); 42 | assert.strictEqual(request.form.field[0], "DAVID"); 43 | assert.strictEqual(request.form.field[1], "STEPHEN"); 44 | assert.strictEqual(request.form.field[2], "GREG"); 45 | assert.strictEqual(request.form.field.length, 3); 46 | 47 | // Iterate and validate array 48 | var request = { body: { field: [1, 2, "f"] } }; 49 | form(field("field").array().isInt())(request, {}); 50 | assert.equal(request.form.errors.length, 1); 51 | assert.equal(request.form.errors[0], "field is not an integer"); 52 | }, 53 | "field : nesting": function () { 54 | // Nesting with dot notation 55 | var request = { 56 | body: { 57 | field: { 58 | nest: "wow", 59 | child: "4", 60 | gb: { 61 | a: "a", 62 | b: "aaaa", 63 | c: { 64 | fruit: "deeper", 65 | must: { 66 | go: "deeperrrr" 67 | } 68 | } 69 | } 70 | } 71 | 72 | } 73 | }; 74 | form( 75 | field("field.nest").toUpper(), 76 | field("field.child").toUpper(), 77 | field("field.gb.a").toUpper(), 78 | field("field.gb.b").toUpper(), 79 | field("field.gb.c.fruit").toUpper(), 80 | field("field.gb.c.must.go").toUpper() 81 | )(request, {}); 82 | assert.strictEqual(request.form.field.nest, "WOW"); 83 | assert.strictEqual(request.form.field.child, "4"); 84 | assert.strictEqual(request.form.field.gb.a, "A"); 85 | assert.strictEqual(request.form.field.gb.b, "AAAA"); 86 | assert.strictEqual(request.form.field.gb.c.fruit, "DEEPER"); 87 | assert.strictEqual(request.form.field.gb.c.must.go, "DEEPERRRR"); 88 | 89 | // Nesting with square-bracket notation 90 | var request = { 91 | body: { 92 | field: { 93 | nest: "wow", 94 | child: "4", 95 | gb: { 96 | a: "a", 97 | b: "aaaa", 98 | c: { 99 | fruit: "deeper", 100 | must: { 101 | go: "deeperrrr" 102 | } 103 | } 104 | } 105 | } 106 | 107 | } 108 | }; 109 | form( 110 | field("field[nest]").toUpper(), 111 | field("field[child]").toUpper(), 112 | field("field[gb][a]").toUpper(), 113 | field("field[gb][b]").toUpper(), 114 | field("field[gb][c][fruit]").toUpper(), 115 | field("field[gb][c][must][go]").toUpper() 116 | )(request, {}); 117 | assert.strictEqual(request.form.field.nest, "WOW"); 118 | assert.strictEqual(request.form.field.child, "4"); 119 | assert.strictEqual(request.form.field.gb.a, "A"); 120 | assert.strictEqual(request.form.field.gb.b, "AAAA"); 121 | assert.strictEqual(request.form.field.gb.c.fruit, "DEEPER"); 122 | assert.strictEqual(request.form.field.gb.c.must.go, "DEEPERRRR"); 123 | }, 124 | 125 | "field : filter/validate combo ordering": function () { 126 | // Can arrange filter and validate procs in any order. 127 | var request = { 128 | body: { 129 | field1: " whatever ", 130 | field2: " some thing " 131 | } 132 | }; 133 | form( 134 | field("field1").trim().toUpper().maxLength(5), 135 | field("field2").minLength(12).trim() 136 | )(request, {}); 137 | assert.strictEqual(request.form.field1, "WHATEVER"); 138 | assert.strictEqual(request.form.field2, "some thing"); 139 | assert.equal(request.form.errors.length, 1); 140 | assert.equal(request.form.errors[0], "field1 is too long"); 141 | }, 142 | 143 | "field : autoTrim": function () { 144 | // Auto-trim declared fields. 145 | form.configure({ autoTrim: true }); 146 | var request = { body: { field: " whatever " } }; 147 | form(field("field"))(request, {}); 148 | assert.strictEqual(request.form.field, "whatever"); 149 | form.configure({ autoTrim: false }); 150 | }, 151 | 152 | "field : passThrough": function () { 153 | // request.form gets all values from sources. 154 | form.configure({ passThrough: true }); 155 | var request = { 156 | body: { 157 | field1: "fdsa", 158 | field2: "asdf" 159 | } 160 | }; 161 | form(field("field1"))(request, {}); 162 | assert.strictEqual(request.form.field1, "fdsa"); 163 | assert.strictEqual(request.form.field2, "asdf"); 164 | 165 | // request.form only gets declared fields. 166 | form.configure({ passThrough: false }); 167 | var request = { body: { 168 | field1: "fdsa", 169 | field2: "asdf" 170 | } }; 171 | form(field("field1"))(request, {}); 172 | assert.strictEqual(request.form.field1, "fdsa"); 173 | assert.strictEqual(typeof request.form.field2, "undefined"); 174 | }, 175 | 176 | "form : getErrors() gives full map": function() { 177 | var request = { 178 | body: { 179 | field0: "win", 180 | field1: "fail", 181 | field2: "fail", 182 | field3: "fail" 183 | } 184 | }; 185 | form( 186 | field("field0").equals("win"), 187 | field("field1").isEmail(), 188 | field("field2").isEmail().isUrl(), 189 | field("field3").isEmail().isUrl().isIP() 190 | )(request, {}); 191 | assert.equal(request.form.isValid, false); 192 | assert.equal(request.form.errors.length, 6); 193 | assert.equal(typeof request.form.getErrors().field0, "undefined"); 194 | assert.equal(request.form.getErrors().field1.length, 1); 195 | assert.equal(request.form.getErrors().field2.length, 2); 196 | assert.equal(request.form.getErrors().field3.length, 3); 197 | } 198 | 199 | } -------------------------------------------------------------------------------- /test/filter.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | form = require("../index"), 3 | filter = form.filter; 4 | 5 | module.exports = { 6 | 'filter : trim': function() { 7 | var request = { body: { field: "\r\n value \t" }}; 8 | form(filter("field").trim())(request, {}); 9 | assert.equal(request.form.field, "value"); 10 | }, 11 | 12 | 'filter : ltrim': function() { 13 | var request = { body: { field: "\r\n value \t" }}; 14 | form(filter("field").ltrim())(request, {}); 15 | assert.equal(request.form.field, "value \t"); 16 | }, 17 | 18 | 'filter : rtrim': function() { 19 | var request = { body: { field: "\r\n value \t" }}; 20 | form(filter("field").rtrim())(request, {}); 21 | assert.equal(request.form.field, "\r\n value"); 22 | }, 23 | 24 | 'filter : ifNull': function() { 25 | // Replace missing value with "value" 26 | var request = { body: {} }; 27 | form(filter("field").ifNull("value"))(request, {}); 28 | assert.equal(request.form.field, "value"); 29 | 30 | // Replace empty string with value 31 | var request = { body: { field: "" }}; 32 | form(filter("field").ifNull("value"))(request, {}); 33 | assert.equal(request.form.field, "value"); 34 | 35 | // Replace NULL with value 36 | var request = { body: { field: null }}; 37 | form(filter("field").ifNull("value"))(request, {}); 38 | assert.equal(request.form.field, "value"); 39 | 40 | // Replace undefined with value 41 | var request = { body: { field: undefined }}; 42 | form(filter("field").ifNull("value"))(request, {}); 43 | assert.equal(request.form.field, "value"); 44 | 45 | // DO NOT replace false 46 | var request = { body: { field: false }}; 47 | form(filter("field").ifNull("value"))(request, {}); 48 | assert.equal(request.form.field, false); 49 | 50 | // DO NOT replace zero 51 | var request = { body: { field: 0 }}; 52 | form(filter("field").ifNull("value"))(request, {}); 53 | assert.equal(request.form.field, 0); 54 | }, 55 | 56 | 'filter : toFloat': function() { 57 | var request = { body: { field: "50.01" }}; 58 | form(filter("field").toFloat())(request, {}); 59 | assert.ok(typeof request.form.field == "number"); 60 | assert.equal(request.form.field, 50.01); 61 | 62 | var request = { body: { field: "fail" }}; 63 | form(filter("field").toFloat())(request, {}); 64 | assert.ok(typeof request.form.field == "number"); 65 | assert.ok(isNaN(request.form.field)); 66 | }, 67 | 68 | 'filter : toInt': function() { 69 | var request = { body: { field: "50.01" }}; 70 | form(filter("field").toInt())(request, {}); 71 | assert.ok(typeof request.form.field == "number"); 72 | assert.equal(request.form.field, 50); 73 | 74 | var request = { body: { field: "fail" }}; 75 | form(filter("field").toInt())(request, {}); 76 | assert.ok(typeof request.form.field == "number"); 77 | assert.ok(isNaN(request.form.field)); 78 | }, 79 | 80 | 'filter : toBoolean': function() { 81 | // Truthy values 82 | var request = { body: { 83 | field1: true, 84 | field2: "true", 85 | field3: "hi", 86 | field4: new Date(), 87 | field5: 50, 88 | field6: -1, 89 | field7: "3000" 90 | }}; 91 | form( 92 | filter("field1").toBoolean(), 93 | filter("field2").toBoolean(), 94 | filter("field3").toBoolean(), 95 | filter("field4").toBoolean(), 96 | filter("field5").toBoolean(), 97 | filter("field6").toBoolean(), 98 | filter("field7").toBoolean() 99 | )(request, {}); 100 | "1234567".split("").forEach(function(i) { 101 | var name = "field" + i; 102 | assert.strictEqual(typeof request.form[name], "boolean"); 103 | assert.strictEqual(request.form[name], true); 104 | }); 105 | 106 | // Falsy values 107 | var request = { body: { 108 | field1: false, 109 | field2: "false", 110 | field3: null, 111 | field4: undefined, 112 | field5: 0, 113 | field6: "0", 114 | field7: "" 115 | }}; 116 | form( 117 | filter("field1").toBoolean(), 118 | filter("field2").toBoolean(), 119 | filter("field3").toBoolean(), 120 | filter("field4").toBoolean(), 121 | filter("field5").toBoolean(), 122 | filter("field6").toBoolean(), 123 | filter("field7").toBoolean() 124 | )(request, {}); 125 | "1234567".split("").forEach(function(i) { 126 | var name = "field" + i; 127 | assert.strictEqual(typeof request.form[name], "boolean"); 128 | assert.strictEqual(request.form[name], false); 129 | }); 130 | }, 131 | 132 | 'filter : toBooleanStrict': function() { 133 | // Truthy values 134 | var request = { body: { 135 | field1: true, 136 | field2: "true", 137 | field3: 1, 138 | field4: "1" 139 | }}; 140 | form( 141 | filter("field1").toBooleanStrict(), 142 | filter("field2").toBooleanStrict(), 143 | filter("field3").toBooleanStrict(), 144 | filter("field4").toBooleanStrict() 145 | )(request, {}); 146 | "1234".split("").forEach(function(i) { 147 | var name = "field" + i; 148 | assert.strictEqual(typeof request.form[name], "boolean"); 149 | assert.strictEqual(request.form[name], true); 150 | }); 151 | 152 | // Falsy values 153 | var request = { body: { 154 | field1: false, 155 | field2: "false", 156 | field3: null, 157 | field4: undefined, 158 | field5: 0, 159 | field6: "0", 160 | field7: "", 161 | field8: new Date(), 162 | field9: 50, 163 | field0: -1, 164 | fielda: "3000" 165 | }}; 166 | form( 167 | filter("field1").toBooleanStrict(), 168 | filter("field2").toBooleanStrict(), 169 | filter("field3").toBooleanStrict(), 170 | filter("field4").toBooleanStrict(), 171 | filter("field5").toBooleanStrict(), 172 | filter("field6").toBooleanStrict(), 173 | filter("field7").toBooleanStrict(), 174 | filter("field8").toBooleanStrict(), 175 | filter("field9").toBooleanStrict(), 176 | filter("field0").toBooleanStrict(), 177 | filter("fielda").toBooleanStrict() 178 | )(request, {}); 179 | "1234567890a".split("").forEach(function(i) { 180 | var name = "field" + i; 181 | assert.strictEqual(typeof request.form[name], "boolean"); 182 | assert.strictEqual(request.form[name], false); 183 | }); 184 | }, 185 | 186 | 'filter : entityEncode': function() { 187 | // NOTE: single quotes are not encoded 188 | var request = { body: { field: "&\"<>hello!" }}; 189 | form(filter("field").entityEncode())(request, {}); 190 | assert.equal(request.form.field, "&"<>hello!"); 191 | }, 192 | 193 | 'filter : entityDecode': function() { 194 | var request = { body: { field: "&"<>hello!" }}; 195 | form(filter("field").entityDecode())(request, {}); 196 | assert.equal(request.form.field, "&\"<>hello!"); 197 | }, 198 | 199 | 'filter : toUpper': function() { 200 | var request = { body: { field: "hellö!" }}; 201 | form(filter("field").toUpper())(request, {}); 202 | assert.equal(request.form.field, "HELLÖ!"); 203 | }, 204 | 205 | 'filter : toUpper : object': function() { 206 | var request = { body: { email: { key: '1' }}}; 207 | form(filter("email").toUpper())(request, {}); 208 | assert.strictEqual(request.form.email, '[OBJECT OBJECT]'); 209 | }, 210 | 211 | 'filter : toUpper : array': function() { 212 | var request = { body: { email: ['MyEmaiL1@example.com', 'myemail2@example.org'] }}; 213 | form(filter("email").toUpper())(request, {}); 214 | assert.strictEqual(request.form.email, 'MYEMAIL1@EXAMPLE.COM'); 215 | }, 216 | 217 | 'filter : toLower': function() { 218 | var request = { body: { field: "HELLÖ!" }}; 219 | form(filter("field").toLower())(request, {}); 220 | assert.equal(request.form.field, "hellö!"); 221 | }, 222 | 223 | 'filter : toLower : object': function() { 224 | var request = { body: { email: { key: '1' }}}; 225 | form(filter("email").toLower())(request, {}); 226 | assert.strictEqual(request.form.email, '[object object]'); 227 | }, 228 | 229 | 'filter : toLower : array': function() { 230 | var request = { body: { email: ['MyEmaiL1@example.com', 'myemail2@example.org'] }}; 231 | form(filter("email").toLower())(request, {}); 232 | assert.strictEqual(request.form.email, 'myemail1@example.com'); 233 | }, 234 | 235 | 'filter : truncate': function() { 236 | var request = { body: { 237 | field1: "1234567890", 238 | field2: "", 239 | field3: "123", 240 | field4: "123456", 241 | field5: "1234567890" 242 | }}; 243 | form( 244 | filter("field1").truncate(3), // ... 245 | filter("field2").truncate(3), // EMPTY 246 | filter("field3").truncate(3), // 123 247 | filter("field4").truncate(5), // 12... 248 | filter("field5").truncate(7) // 1234... 249 | )(request, {}); 250 | assert.equal(request.form.field1, "..."); 251 | assert.equal(request.form.field2, ""); 252 | assert.equal(request.form.field3, "123"); 253 | assert.equal(request.form.field4, "12..."); 254 | assert.equal(request.form.field5, "1234..."); 255 | }, 256 | 257 | 'filter : truncate : object': function() { 258 | var request = { body: { email: { key: '1', length: 100 }}}; 259 | form(filter("email").truncate(10))(request, {}); 260 | assert.strictEqual(request.form.email, '[object...'); 261 | }, 262 | 263 | 'filter : truncate : array': function() { 264 | var request = { body: { email: ['myemail1@example.com', 'myemail2@example.org'] }}; 265 | form(filter("email").truncate(11))(request, {}); 266 | assert.strictEqual(request.form.email, 'myemail1...'); 267 | }, 268 | 269 | 'filter : custom': function() { 270 | var request = { body: { field: "value!" }}; 271 | form(filter("field").custom(function(value) { 272 | return "!!!"; 273 | }))(request, {}); 274 | assert.equal(request.form.field, "!!!"); 275 | } 276 | 277 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-form [![Build Status](https://travis-ci.org/freewil/express-form.svg?branch=master)](https://travis-ci.org/freewil/express-form) 2 | 3 | Express Form provides data filtering and validation as route middleware to your Express applications. 4 | 5 | ## Install 6 | 7 | `npm install express-form --save` 8 | 9 | ## Usage 10 | 11 | ```js 12 | var express = require('express'), 13 | bodyParser = require('body-parser'), 14 | form = require('express-form'), 15 | field = form.field; 16 | 17 | var app = express(); 18 | app.use(bodyParser()); 19 | 20 | app.post( 21 | 22 | // Route 23 | '/user', 24 | 25 | // Form filter and validation middleware 26 | form( 27 | field("username").trim().required().is(/^[a-z]+$/), 28 | field("password").trim().required().is(/^[0-9]+$/), 29 | field("email").trim().isEmail() 30 | ), 31 | 32 | // Express request-handler now receives filtered and validated data 33 | function(req, res){ 34 | if (!req.form.isValid) { 35 | // Handle errors 36 | console.log(req.form.errors); 37 | 38 | } else { 39 | // Or, use filtered form data from the form object: 40 | console.log("Username:", req.form.username); 41 | console.log("Password:", req.form.password); 42 | console.log("Email:", req.form.email); 43 | } 44 | } 45 | ); 46 | 47 | app.listen(3000); 48 | ``` 49 | 50 | ## Documentation 51 | 52 | ### Module 53 | 54 | `express-form` returns an `express` [Route Middleware](http://expressjs.com/guide.html#Route-Middleware) function. 55 | You specify filtering and validation by passing filters and validators as 56 | arguments to the main module function. For example: 57 | 58 | ```js 59 | var form = require("express-form"); 60 | 61 | app.post('/user', 62 | 63 | // Express Form Route Middleware: trims whitespace off of 64 | // the `username` field. 65 | form(form.field("username").trim()), 66 | 67 | // standard Express handler 68 | function(req, res) { 69 | // ... 70 | } 71 | ); 72 | ``` 73 | 74 | ### Fields 75 | 76 | The `field` property of the module creates a filter/validator object tied to a specific field. 77 | 78 | ``` 79 | field(fieldname[, label]); 80 | ``` 81 | 82 | You can access nested properties with either dot or square-bracket notation. 83 | 84 | ```js 85 | field("post.content").minLength(50), 86 | field("post[user][id]").isInt(), 87 | field("post.super.nested.property").required() 88 | ``` 89 | 90 | Simply specifying a property like this, makes sure it exists. So, even if `req.body.post` was undefined, 91 | `req.form.post.content` would be defined. This helps avoid any unwanted errors in your code. 92 | 93 | The API is chainable, so you can keep calling filter/validator methods one after the other: 94 | 95 | ```js 96 | filter("username") 97 | .required() 98 | .trim() 99 | .toLower() 100 | .truncate(5) 101 | .isAlphanumeric() 102 | ``` 103 | 104 | ### Filter API: 105 | 106 | Type Coercion 107 | 108 | toFloat() -> Number 109 | 110 | toInt() -> Number, rounded down 111 | 112 | toBoolean() -> Boolean from truthy and falsy values 113 | 114 | toBooleanStrict() -> Only true, "true", 1 and "1" are `true` 115 | 116 | ifNull(replacement) -> "", undefined and null get replaced by `replacement` 117 | 118 | 119 | HTML Encoding for `& " < >` 120 | 121 | entityEncode() -> encodes HTML entities 122 | 123 | entityDecode() -> decodes HTML entities 124 | 125 | 126 | String Transformations 127 | 128 | trim(chars) -> `chars` defaults to whitespace 129 | 130 | ltrim(chars) 131 | 132 | rtrim(chars) 133 | 134 | toLower() / toLowerCase() 135 | 136 | toUpper() / toUpperCase() 137 | 138 | truncate(length) -> Chops value at (length - 3), appends `...` 139 | 140 | 141 | ### Validator API: 142 | 143 | **Validation messages**: each validator has its own default validation message. 144 | These can easily be overridden at runtime by passing a custom validation message 145 | to the validator. The custom message is always the **last** argument passed to 146 | the validator. `required()` allows you to set a placeholder (or default value) 147 | that your form contains when originally presented to the user. This prevents the 148 | placeholder value from passing the `required()` check. 149 | 150 | Use "%s" in the message to have the field name or label printed in the message: 151 | 152 | validate("username").required() 153 | // -> "username is required" 154 | 155 | validate("username").required("Type your desired username", "What is your %s?") 156 | // -> "What is your username?" 157 | 158 | validate("username", "Username").required("", "What is your %s?") 159 | // -> "What is your Username?" 160 | 161 | 162 | **Validation Methods** 163 | 164 | *By Regular Expressions* 165 | 166 | regex(pattern[, modifiers[, message]]) 167 | - pattern (RegExp|String): RegExp (with flags) or String pattern. 168 | - modifiers (String): Optional, and only if `pattern` is a String. 169 | - message (String): Optional validation message. 170 | 171 | alias: is 172 | 173 | Checks that the value matches the given regular expression. 174 | 175 | Example: 176 | 177 | validate("username").is("[a-z]", "i", "Only letters are valid in %s") 178 | validate("username").is(/[a-z]/i, "Only letters are valid in %s") 179 | 180 | 181 | notRegex(pattern[, modifiers[, message]]) 182 | - pattern (RegExp|String): RegExp (with flags) or String pattern. 183 | - modifiers (String): Optional, and only if `pattern` is a String. 184 | - message (String): Optional validation message. 185 | 186 | alias: not 187 | 188 | Checks that the value does NOT match the given regular expression. 189 | 190 | Example: 191 | 192 | validate("username").not("[a-z]", "i", "Letters are not valid in %s") 193 | validate("username").not(/[a-z]/i, "Letters are not valid in %s") 194 | 195 | 196 | *By Type* 197 | 198 | isNumeric([message]) 199 | 200 | isInt([message]) 201 | 202 | isDecimal([message]) 203 | 204 | isFloat([message]) 205 | 206 | 207 | *By Format* 208 | 209 | isDate([message]) 210 | 211 | isEmail([message]) 212 | 213 | isUrl([message]) 214 | 215 | isIP([message]) 216 | 217 | isAlpha([message]) 218 | 219 | isAlphanumeric([message]) 220 | 221 | isLowercase([message]) 222 | 223 | isUppercase([message]) 224 | 225 | 226 | *By Content* 227 | 228 | notEmpty([message]) 229 | 230 | Checks if the value is not just whitespace. 231 | 232 | 233 | equals( value [, message] ) 234 | - value (String): A value that should match the field value OR a fieldname 235 | token to match another field, ie, `field::password`. 236 | 237 | Compares the field to `value`. 238 | 239 | Example: 240 | validate("username").equals("admin") 241 | 242 | validate("password").is(/^\w{6,20}$/) 243 | validate("password_confirmation").equals("field::password") 244 | 245 | 246 | contains(value[, message]) 247 | - value (String): The value to test for. 248 | 249 | Checks if the field contains `value`. 250 | 251 | 252 | notContains(string[, message]) 253 | - value (String): A value that should not exist in the field. 254 | 255 | Checks if the field does NOT contain `value`. 256 | 257 | minLength(length[, message]) 258 | - length (integer): The min character to test for. 259 | 260 | Checks the field value min length. 261 | 262 | maxLength(length[, message]) 263 | - length (integer): The max character to test for. 264 | 265 | Checks the field value max length. 266 | 267 | 268 | *Other* 269 | 270 | required([message]) 271 | 272 | Checks that the field is present in form data, and has a value. 273 | 274 | ### Array Method 275 | 276 | array() 277 | Using the array() flag means that field always gives an array. If the field value is an array, but there is no flag, then the first value in that array is used instead. 278 | 279 | This means that you don't have to worry about unexpected post data that might break your code. Eg/ when you call an array method on what is actually a string. 280 | 281 | field("project.users").array(), 282 | // undefined => [], "" => [], "q" => ["q"], ["a", "b"] => ["a", "b"] 283 | 284 | field("project.block"), 285 | // project.block: ["a", "b"] => "a". No "array()", so only first value used. 286 | 287 | In addition, any other methods called with the array method, are applied to every value within the array. 288 | 289 | field("post.users").array().toUpper() 290 | // post.users: ["one", "two", "three"] => ["ONE", "TWO", "THREE"] 291 | 292 | ### Custom Methods 293 | 294 | custom(function[, message]) 295 | - function (Function): A custom filter or validation function. 296 | 297 | This method can be utilised as either a filter or validator method. 298 | 299 | If the function throws an error, then an error is added to the form. (If `message` is not provided, the thrown error message is used.) 300 | 301 | If the function returns a value, then it is considered a filter method, with the field then becoming the returned value. 302 | 303 | If the function returns undefined, then the method has no effect on the field. 304 | 305 | Examples: 306 | 307 | If the `name` field has a value of "hello there", this would 308 | transform it to "hello-there". 309 | 310 | field("name").custom(function(value) { 311 | return value.replace(/\s+/g, "-"); 312 | }); 313 | 314 | Throws an error if `username` field does not have value "admin". 315 | 316 | field("username").custom(function(value) { 317 | if (value !== "admin") { 318 | throw new Error("%s must be 'admin'."); 319 | } 320 | }); 321 | 322 | Validator based value on another field of the incoming source being validated 323 | 324 | field("sport", "favorite sport").custom(function(value, source) { 325 | if (!source.country) { 326 | throw new Error('unable to validate %s'); 327 | } 328 | 329 | switch (source.country) { 330 | case 'US': 331 | if (value !=== 'baseball') { 332 | throw new Error('America likes baseball'); 333 | } 334 | break; 335 | 336 | case 'UK': 337 | if (value !=== 'football') { 338 | throw new Error('UK likes football'); 339 | } 340 | break; 341 | } 342 | 343 | }); 344 | 345 | Asynchronous custom validator (3 argument function signature) 346 | 347 | form.field('username').custom(function(value, source, callback) { 348 | username.check(value, function(err) { 349 | if (err) return callback(new Error('Invalid %s')); 350 | callback(null); 351 | }); 352 | }); 353 | 354 | 355 | ### http.ServerRequest.prototype.form 356 | 357 | Express Form adds a `form` object with various properties to the request. 358 | 359 | isValid -> Boolean 360 | 361 | errors -> Array 362 | 363 | flashErrors(name) -> undefined 364 | 365 | Flashes all errors. Configurable, enabled by default. 366 | 367 | getErrors(name) -> Array or Object if no name given 368 | - fieldname (String): The name of the field 369 | 370 | Gets all errors for the field with the given name. 371 | 372 | You can also call this method with no parameters to get a map of errors for all of the fields. 373 | 374 | Example request handler: 375 | 376 | function(req, res) { 377 | if (!req.form.isValid) { 378 | console.log(req.form.errors); 379 | console.log(req.form.getErrors("username")); 380 | console.log(req.form.getErrors()); 381 | } 382 | } 383 | 384 | ### Configuration 385 | 386 | Express Form has various configuration options, but aims for sensible defaults for a typical Express application. 387 | 388 | form.configure(options) -> self 389 | - options (Object): An object with configuration options. 390 | 391 | flashErrors (Boolean): If validation errors should be automatically passed to Express’ flash() method. Default: true. 392 | 393 | autoLocals (Boolean): If field values from Express’ request.body should be passed into Express’ response.locals object. This is helpful when a form is invalid an you want to repopulate the form elements with their submitted values. Default: true. 394 | 395 | Note: if a field name dash-separated, the name used for the locals object will be in camelCase. 396 | 397 | dataSources (Array): An array of Express request properties to use as data sources when filtering and validating data. Default: ["body", "query", "params"]. 398 | 399 | autoTrim (Boolean): If true, all fields will be automatically trimmed. Default: false. 400 | 401 | passThrough (Boolean): If true, all data sources will be merged with `req.form`. Default: false. 402 | 403 | 404 | ### Credits 405 | 406 | Currently, Express Form uses many of the validation and filtering functions provided by Chris O'Hara's [node-validator](https://github.com/chriso/node-validator). 407 | -------------------------------------------------------------------------------- /lib/field.js: -------------------------------------------------------------------------------- 1 | var validator = require("validator") 2 | , FilterPrototype = validator.Filter.prototype 3 | , ValidatorPrototype = validator.Validator.prototype 4 | , externalFilter = new validator.Filter() 5 | , externalValidator = new validator.Validator() 6 | , object = require("object-additions").object 7 | , async = require("async") 8 | , utils = require("./utils"); 9 | 10 | function Field(property, label) { 11 | var stack = [] 12 | , isArray = false 13 | , fieldLabel = label || property; 14 | 15 | this.name = property; 16 | this.__required = false; 17 | this.__trimmed = false; 18 | 19 | this.add = function(func) { 20 | stack.push(func); 21 | return this; 22 | }; 23 | 24 | this.array = function() { 25 | isArray = true; 26 | return this; 27 | }; 28 | 29 | this.run = function (source, form, options, cb) { 30 | var self = this 31 | , errors = [] 32 | , value = utils.getProp(property, form) || utils.getProp(property, source); 33 | 34 | if (options.autoTrim && !self.__trimmed) { 35 | self.__trimmed = true; 36 | stack.unshift(function (value) { 37 | if (object.isString(value)) { 38 | return FilterPrototype.trim.apply(externalFilter.sanitize(value)); 39 | } 40 | return value; 41 | }); 42 | } 43 | 44 | function runStack(foo, cb) { 45 | 46 | async.eachSeries(stack, function(proc, cb) { 47 | 48 | if (proc.length == 3) { 49 | // run the async validator/filter 50 | return proc(foo, source, function(err, result) { 51 | if (err) { 52 | errors.push(err.message.replace("%s", fieldLabel)); 53 | return cb(null); 54 | } 55 | 56 | // filters return values 57 | if (result != null) { 58 | foo = result 59 | } 60 | 61 | cb(null); 62 | 63 | }); 64 | } 65 | 66 | // run the sync validator/filter 67 | var result = proc(foo, source); 68 | if (result.valid) return cb(null); 69 | if (result.error) { 70 | // If this field is not required and it doesn't have a value, ignore error. 71 | if (!utils.hasValue(value) && !self.__required) return cb(null); 72 | 73 | errors.push(result.error.replace("%s", fieldLabel)); 74 | return cb(null); 75 | } 76 | foo = result; 77 | cb(null); 78 | 79 | }, function(err) { 80 | cb(null, foo); 81 | }); 82 | } 83 | 84 | if (isArray) { 85 | if (!utils.hasValue(value)) value = []; 86 | if (!Array.isArray(value)) value = [value]; 87 | async.mapSeries(value, runStack, function(err, value) { 88 | utils.setProp(property, form, value); 89 | cb(null, errors); 90 | }); 91 | 92 | 93 | } else { 94 | if (Array.isArray(value)) value = value[0]; 95 | runStack(value, function(err, value) { 96 | utils.setProp(property, form, value); 97 | cb(null, errors); 98 | }); 99 | } 100 | }; 101 | } 102 | 103 | // ARRAY METHODS 104 | 105 | Field.prototype.array = function () { 106 | return this.array(); 107 | }; 108 | 109 | Field.prototype.arrLength = function (from, to) { 110 | return this.add(function (arr) { 111 | if (value.length < from) { 112 | return { error: message || e.message || "%s is too short" }; 113 | } 114 | if (value.length > to) { 115 | return { error: message || e.message || "%s is too long" }; 116 | } 117 | return { valid: true }; 118 | }); 119 | } 120 | 121 | // HYBRID METHODS 122 | 123 | Field.prototype.custom = function(func, message) { 124 | 125 | // custom function is async 126 | if (func.length == 3) { 127 | return this.add(function(value, source, cb) { 128 | func(value, source, function(err, result) { 129 | if (err) return cb(new Error(message || err.message || "%s is invalid")); 130 | 131 | // functions that return values are filters 132 | if (result != null) return cb(null, result); 133 | 134 | // value passed validator 135 | cb(null, null); 136 | }); 137 | }); 138 | } 139 | 140 | // custom function is sync 141 | return this.add(function (value, source) { 142 | 143 | try { 144 | var result = func(value, source); 145 | } catch (e) { 146 | return { error: message || e.message || "%s is invalid" }; 147 | } 148 | // Functions that return values are filters. 149 | if (result != null) return result; 150 | 151 | // value passed validator 152 | return { valid: true }; 153 | 154 | }); 155 | }; 156 | 157 | // FILTER METHODS 158 | 159 | Object.keys(FilterPrototype).forEach(function (name) { 160 | if (name.match(/^ifNull$/)) return; 161 | 162 | Field.prototype[name] = function () { 163 | var args = arguments; 164 | return this.add(function (value) { 165 | var a = FilterPrototype[name].apply(externalFilter.sanitize(value), args); 166 | return a; 167 | }); 168 | }; 169 | }); 170 | 171 | Field.prototype.ifNull = function (replacement) { 172 | return this.add(function (value) { 173 | if (object.isUndefined(value) || null === value || '' === value) { 174 | return replacement; 175 | } 176 | return value; 177 | }); 178 | }; 179 | 180 | Field.prototype.toUpper = Field.prototype.toUpperCase = function () { 181 | return this.add(function (value) { 182 | return value.toString().toUpperCase(); 183 | }); 184 | }; 185 | 186 | Field.prototype.toLower = Field.prototype.toLowerCase = function () { 187 | return this.add(function (value) { 188 | return value.toString().toLowerCase(); 189 | }); 190 | }; 191 | 192 | Field.prototype.truncate = function (length) { 193 | return this.add(function (value) { 194 | value = value.toString(); 195 | if (value.length <= length) { 196 | return value; 197 | } 198 | 199 | if (length <= 3) return "..."; 200 | 201 | if (value.length > length - 3) { 202 | return value.substr(0,length - 3) + "..."; 203 | } 204 | 205 | return value; 206 | }); 207 | }; 208 | 209 | Field.prototype.customFilter = function (func) { 210 | return this.add(func); 211 | }; 212 | 213 | // VALIDATE METHODS 214 | 215 | var MESSAGES = { 216 | isDate: "%s is not a date", 217 | isUrl: "%s is not a URL", 218 | isIP: "%s is not an IP address", 219 | isAlpha: "%s contains non-letter characters", 220 | isAlphanumeric: "%s contains non alpha-numeric characters", 221 | isNumeric: "%s is not numeric", 222 | isLowercase: "%s contains uppercase letters", 223 | isUppercase: "%s contains lowercase letters", 224 | isInt: "%s is not an integer", 225 | notEmpty: "%s has no value or is only whitespace" 226 | }; 227 | 228 | Object.keys(ValidatorPrototype).forEach(function (name) { 229 | if (name.match(/^(contains|notContains|equals|check|validate|assert|error|len|isNumeric|isDecimal|isEmail|isFloat|regex|notRegex|is|not|notNull|isNull)$/)) { 230 | return; 231 | } 232 | 233 | Field.prototype[name] = function (message) { 234 | var args = arguments; 235 | message = message || MESSAGES[name]; 236 | 237 | return this.add(function(value) { 238 | try { 239 | ValidatorPrototype[name].apply(externalValidator.check(value, message), args); 240 | } catch (e) { 241 | return { error: e.message || e.toString() }; 242 | } 243 | return { valid: true }; 244 | }); 245 | }; 246 | }); 247 | 248 | Field.prototype.contains = function (test, message) { 249 | return this.add(function(value) { 250 | try { 251 | ValidatorPrototype.contains.call(externalValidator.check(value, message), test); 252 | } catch (e) { 253 | return { error: message || "%s does not contain required characters" }; 254 | } 255 | return { valid: true }; 256 | }); 257 | }; 258 | 259 | Field.prototype.notContains = function (test, message) { 260 | return this.add(function (value) { 261 | try { 262 | ValidatorPrototype.notContains.call(externalValidator.check(value, message), test); 263 | } catch (e) { 264 | return { error: message || "%s contains invalid characters" }; 265 | } 266 | return { valid: true }; 267 | }); 268 | }; 269 | 270 | 271 | Field.prototype.equals = function (other, message) { 272 | if (object.isString(other) && other.match(/^field::/)) { 273 | this.__required = true; 274 | } 275 | 276 | return this.add(function (value, source) { 277 | // If other is a field token (field::fieldname), grab the value of fieldname 278 | // and use that as the OTHER value. 279 | var test = other; 280 | if (object.isString(other) && other.match(/^field::/)) { 281 | test = utils.getProp(other.replace(/^field::/, ""), source); 282 | } 283 | if (value != test) { 284 | return { error: message || "%s does not equal " + String(test) }; 285 | } 286 | return { valid: true }; 287 | }); 288 | }; 289 | 290 | // node-validator's numeric validator seems unintuitive. All numeric values should be valid, not just int. 291 | Field.prototype.isNumeric = function (message) { 292 | return this.add(function (value) { 293 | if (object.isNumber(value) || (object.isString(value) && value.match(/^[-+]?[0-9]*\.?[0-9]+$/))) { 294 | } else { 295 | return { error: message || "%s is not a number" }; 296 | } 297 | return { valid: true }; 298 | }); 299 | }; 300 | 301 | // node-validator's decimal/float validator incorrectly thinks Ints are valid. 302 | Field.prototype.isFloat = Field.prototype.isDecimal = function (message) { 303 | return this.add(function (value) { 304 | if ((object.isNumber(value) && value % 1 == 0) || (object.isString(value) && value.match(/^[-+]?[0-9]*\.[0-9]+$/))) { 305 | } else { 306 | return { error: message || "%s is not a decimal" }; 307 | } 308 | return { valid: true }; 309 | }); 310 | }; 311 | 312 | // super simple email validation 313 | Field.prototype.isEmail = function (message) { 314 | return this.add(function (value) { 315 | if (typeof value != 'string' || !(/^[\-0-9a-zA-Z\.\+_]+@[\-0-9a-zA-Z\.\+_]+\.[a-zA-Z]{2,}$/).test(value)) { 316 | return { error: message || "%s is not an email address" }; 317 | } 318 | return { valid: true }; 319 | }); 320 | }; 321 | 322 | Field.prototype.isString = function (message) { 323 | return this.add(function (value) { 324 | if (!object.isString(value)) { 325 | return { error: message || "%s is not a string" }; 326 | } 327 | return { valid: true }; 328 | }); 329 | }; 330 | 331 | Field.prototype.regex = Field.prototype.is = function (pattern, modifiers, message) { 332 | // regex(/pattern/) 333 | // regex(/pattern/, "message") 334 | // regex("pattern") 335 | // regex("pattern", "modifiers") 336 | // regex("pattern", "message") 337 | // regex("pattern", "modifiers", "message") 338 | 339 | if (pattern instanceof RegExp) { 340 | if (object.isString(modifiers) && modifiers.match(/^[gimy]+$/)) { 341 | throw new Error("Invalid arguments: `modifiers` can only be passed in if `pattern` is a string."); 342 | } 343 | 344 | message = modifiers; 345 | modifiers = undefined; 346 | 347 | } else if (object.isString(pattern)) { 348 | if (arguments.length == 2 && !modifiers.match(/^[gimy]+$/)) { 349 | // 2nd arg doesn't look like modifier flags, it's the message (might also be undefined) 350 | message = modifiers; 351 | modifiers = undefined; 352 | } 353 | pattern = new RegExp(pattern, modifiers); 354 | } 355 | 356 | return this.add(function (value) { 357 | if (pattern.test(value) === false) { 358 | return { error: message || "%s has invalid characters" }; 359 | } 360 | return { valid: true }; 361 | }); 362 | }; 363 | 364 | Field.prototype.notRegex = Field.prototype.not = function(pattern, modifiers, message) { 365 | // notRegex(/pattern/) 366 | // notRegex(/pattern/, "message") 367 | // notRegex("pattern") 368 | // notRegex("pattern", "modifiers") 369 | // notRegex("pattern", "message") 370 | // notRegex("pattern", "modifiers", "message") 371 | 372 | if (pattern instanceof RegExp) { 373 | if (object.isString(modifiers) && modifiers.match(/^[gimy]+$/)) { 374 | throw new Error("Invalid arguments: `modifiers` can only be passed in if `pattern` is a string."); 375 | } 376 | 377 | message = modifiers; 378 | modifiers = undefined; 379 | 380 | } else if (object.isString(pattern)) { 381 | if (arguments.length == 2 && !modifiers.match(/^[gimy]+$/)) { 382 | // 2nd arg doesn't look like modifier flags, it's the message (might also be undefined) 383 | message = modifiers; 384 | modifiers = undefined; 385 | } 386 | pattern = new RegExp(pattern, modifiers); 387 | } 388 | 389 | return this.add(function(value) { 390 | if (pattern.test(value) === true) { 391 | return { error: message || "%s has invalid characters" }; 392 | } 393 | return { valid: true }; 394 | }); 395 | }; 396 | 397 | Field.prototype.required = function (placeholderValue, message) { 398 | this.__required = true; 399 | return this.add(function (value) { 400 | if (!utils.hasValue(value) || value == placeholderValue) { 401 | return { error: message || "%s is required" }; 402 | } 403 | return { valid: true }; 404 | }); 405 | }; 406 | 407 | Field.prototype.minLength = function (length, message) { 408 | return this.add(function(value) { 409 | if (value.toString().length < length) { 410 | return { error: message || "%s is too short" }; 411 | } 412 | return { valid: true }; 413 | }); 414 | }; 415 | 416 | Field.prototype.maxLength = function (length, message) { 417 | return this.add(function(value) { 418 | if (value.toString().length > length) { 419 | return { error: message || "%s is too long" }; 420 | } 421 | return { valid: true }; 422 | }); 423 | }; 424 | 425 | Field.prototype.customValidator = function(func, message) { 426 | return this.add(function(value, source) { 427 | try { 428 | func(value, source); 429 | } catch (e) { 430 | return { error: message || e.message || "%s is invalid" }; 431 | } 432 | return { valid: true }; 433 | }); 434 | }; 435 | 436 | module.exports = Field; 437 | -------------------------------------------------------------------------------- /test/validate.test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | form = require("../index"), 3 | validate = form.validate; 4 | 5 | module.exports = { 6 | 'validate : isDate': function() { 7 | // Skip validating empty values 8 | var request = { body: {} }; 9 | form(validate("field").isDate())(request, {}); 10 | assert.equal(request.form.errors.length, 0); 11 | 12 | // Failure. 13 | var request = { body: { field: "fail" }}; 14 | form(validate("field").isDate())(request, {}); 15 | assert.equal(request.form.errors.length, 1); 16 | assert.equal(request.form.errors[0], "field is not a date"); 17 | 18 | // Failure w/ custom message. 19 | var request = { body: { field: "fail" }}; 20 | form(validate("field").isDate("!!! %s !!!"))(request, {}); 21 | assert.equal(request.form.errors.length, 1); 22 | assert.equal(request.form.errors[0], "!!! field !!!"); 23 | 24 | // Success 25 | var request = { body: { field: "01/29/2012" }}; 26 | form(validate("field").isDate())(request, {}); 27 | assert.equal(request.form.errors.length, 0); 28 | }, 29 | 30 | 'validate : isEmail': function() { 31 | // Skip validating empty values 32 | var request = { body: {} }; 33 | form(validate("field").isEmail())(request, {}); 34 | assert.equal(request.form.errors.length, 0); 35 | 36 | // Failure. 37 | var request = { body: { field: "fail" }}; 38 | form(validate("field").isEmail())(request, {}); 39 | assert.equal(request.form.errors.length, 1); 40 | assert.equal(request.form.errors[0], "field is not an email address"); 41 | 42 | // Failure w/ custom message. 43 | var request = { body: { field: "fail" }}; 44 | form(validate("field").isEmail("!!! %s !!!"))(request, {}); 45 | assert.equal(request.form.errors.length, 1); 46 | assert.equal(request.form.errors[0], "!!! field !!!"); 47 | 48 | // Success 49 | var request = { body: { field: "me@dandean.com" }}; 50 | form(validate("field").isEmail())(request, {}); 51 | assert.equal(request.form.errors.length, 0); 52 | 53 | var validEmails = [ 54 | "user@host.com", 55 | "user@host.info", 56 | "user@host.co.uk", 57 | "user+service@host.co.uk", 58 | "user-ok.yes+tag@host.k12.mi.us", 59 | "FirstNameLastName2000@hotmail.com", 60 | "FooBarEmail@foo.apartments" 61 | ]; 62 | 63 | for (var i in validEmails) { 64 | var request = { body: { field: validEmails[i] }}; 65 | form(validate("field").isEmail())(request, {}); 66 | assert.equal(request.form.errors.length, 0, 'failed to validate email: ' + validEmails[i]); 67 | } 68 | 69 | var badEmails = [ 70 | "dontvalidateme", 71 | "nope@", 72 | "someUser", 73 | "