UNPKG

12.1 kBJavaScriptView Raw
1var validator = require('validator');
2var _ = require('lodash');
3var utils = require('./utils');
4var check = require('../check/check');
5var selectFields = require('../check/select-fields');
6var validationResult = require('../check/validation-result');
7
8var validatorChainSymbol = Symbol('express-validator.validatorChain');
9var sanitizerSymbol = Symbol('express-validator.sanitizer');
10
11// When validator upgraded to v5, they removed automatic string coercion
12// The next few methods (up to validator.init()) restores that functionality
13// so that express-validator can continue to function normally
14validator.extend = function(name, fn) {
15 validator[name] = function() {
16 var args = Array.prototype.slice.call(arguments);
17 args[0] = validator.toString(args[0]);
18 return fn.apply(validator, args);
19 };
20};
21
22validator.init = function() {
23 for (var name in validator) {
24 if (typeof validator[name] !== 'function' || name === 'toString' ||
25 name === 'toDate' || name === 'extend' || name === 'init' ||
26 name === 'isServerSide') {
27 continue;
28 }
29 validator.extend(name, validator[name]);
30 }
31};
32
33validator.toString = function(input) {
34 if (typeof input === 'object' && input !== null && input.toString) {
35 input = input.toString();
36 } else if (input === null || typeof input === 'undefined' || (isNaN(input) && !input.length)) {
37 input = '';
38 }
39 return '' + input;
40};
41
42validator.init();
43
44// validators and sanitizers not prefixed with is/to
45var additionalSanitizers = ['trim', 'ltrim', 'rtrim', 'escape', 'unescape', 'stripLow', 'whitelist', 'blacklist', 'normalizeEmail'];
46
47var allLocations = ['params', 'query', 'body', 'headers', 'cookies'];
48
49/**
50 * Adds validation methods to request object via express middleware
51 *
52 * @method expressValidator
53 * @param {object} options
54 * @return {function} middleware
55 */
56
57var expressValidator = function(options) {
58 options = options || {};
59 var defaults = {
60 customValidators: {},
61 customSanitizers: {},
62 errorFormatter: function(param, msg, value, location) {
63 return {
64 location,
65 param: param,
66 msg: msg,
67 value: value
68 };
69 }
70 };
71
72 _.defaults(options, defaults);
73
74 /**
75 * Initializes a sanitizer
76 *
77 * @class
78 * @param {(string|string[])} param path to property to sanitize
79 * @param {[type]} req request to sanitize
80 * @param {[string]} locations request property to find value
81 */
82
83 function Sanitizer(param, req, locations) {
84 this.values = locations.map(function(location) {
85 return _.get(req[location], param);
86 });
87
88 this.req = req;
89 this.param = param;
90 this.locations = locations;
91
92 req[sanitizerSymbol].forEach(customSanitizers => {
93 utils.mapAndExtend(customSanitizers, Sanitizer.prototype, utils.makeSanitizer);
94 });
95
96 return this;
97 }
98
99 function createValidationChain(field, location, message, req, contexts) {
100 const chain = check([field], Array.isArray(location) ? location : [location], message);
101 contexts.push(chain._context);
102
103 chain.notEmpty = () => chain.isLength({ min: 1 });
104 chain.len = chain.isLength;
105
106 req[validatorChainSymbol].forEach(customValidators => {
107 Object.keys(customValidators).forEach(name => {
108 chain[name] = (...options) => {
109 chain._context.validators.push({
110 options,
111 negated: chain._context.negateNext,
112 validator: customValidators[name]
113 });
114 chain._context.negateNext = false;
115 return chain;
116 };
117 });
118 });
119
120 req[sanitizerSymbol].forEach(customSanitizers => {
121 Object.keys(customSanitizers).forEach(name => {
122 chain[name] = (...options) => {
123 chain._context.sanitizers.push({
124 options,
125 sanitizer: customSanitizers[name]
126 });
127 return chain;
128 };
129 });
130 });
131
132 return chain;
133 }
134
135 /**
136 * validate an object using a schema, using following format:
137 *
138 * {
139 * paramName: {
140 * validatorName: true,
141 * validator2Name: true
142 * }
143 * }
144 *
145 * Pass options or a custom error message:
146 *
147 * {
148 * paramName: {
149 * validatorName: {
150 * options: ['', ''],
151 * errorMessage: 'An Error Message'
152 * }
153 * }
154 * }
155 *
156 * @method validateSchema
157 * @param {Object} schema schema of validations
158 * @param {Request} req request to attach validation errors
159 * @param {string} loc request property to find value (body, params, query, etc.)
160 * @return {object[]} array of errors
161 */
162
163 function validateSchema(schema, req, loc, contexts) {
164 var currentLoc = loc;
165
166 for (var param in schema) {
167 const paramCfg = schema[param];
168
169 // check if schema has defined location
170 if (paramCfg.hasOwnProperty('in')) {
171 if (allLocations.indexOf(paramCfg.in) !== -1) {
172 currentLoc = paramCfg.in;
173 } else {
174 // skip params where defined location is not supported
175 continue;
176 }
177 } else {
178 currentLoc = loc === 'any' ? allLocations : currentLoc;
179 }
180
181 const paramErrorMessage = paramCfg.errorMessage;
182 var validator = createValidationChain(param, currentLoc, paramErrorMessage, req, contexts);
183
184 for (var methodName in paramCfg) {
185 const methodCfg = paramCfg[methodName];
186
187 if (methodName === 'in') {
188 /* Skip method if this is location definition, do not validate it.
189 * Restore also the original location that was changed only for this particular param.
190 * Without it everything after param with in field would be validated against wrong location.
191 */
192 currentLoc = loc;
193 continue;
194 }
195
196 if (methodName === 'errorMessage' || !methodCfg) {
197 // Also do not validate if methodName represent parameter error message
198 // or if the value is falsy
199 continue;
200 }
201
202 if (typeof validator[methodName] !== 'function') {
203 console.warn('express-validator: a validator with name ' + methodName + ' does not exist');
204 continue;
205 }
206
207 let opts = methodCfg.options || [];
208 if (opts != null && !Array.isArray(opts)) {
209 opts = [opts];
210 }
211
212 validator[methodName](...opts);
213 methodName !== 'optional' && validator.withMessage(methodCfg.errorMessage);
214 }
215 }
216 }
217
218 /**
219 * Error formatter delegator to the legacy format
220 * @param {*} error
221 */
222 function errorFormatter({ param, msg, value, location }) {
223 return options.errorFormatter(param, msg, value, location);
224 }
225
226 // _.set sanitizers as prototype methods on corresponding chains
227 _.forEach(validator, function(method, methodName) {
228 if (methodName.match(/^to/) || _.includes(additionalSanitizers, methodName)) {
229 Sanitizer.prototype[methodName] = utils.makeSanitizer(methodName, validator);
230 }
231 });
232
233 utils.mapAndExtend(options.customSanitizers, Sanitizer.prototype, utils.makeSanitizer);
234
235 return function(req, res, next) {
236 const contexts = [];
237 function runContexts() {
238 contexts.filter(context => !context.promise).forEach(context => {
239 const field = selectFields(req, context)[0];
240 if (!field) {
241 context.promise = Promise.resolve();
242 return;
243 }
244
245 const promises = context.validators.map(validatorCfg => {
246 const result = validatorCfg.validator(field.value, ...validatorCfg.options);
247 const errorObj = {
248 location: field.location,
249 value: field.value,
250 param: utils.formatParamOutput(field.path),
251 msg: utils.replaceArgs(
252 validatorCfg.message || context.message || 'Invalid value',
253 [field.value, ...validatorCfg.options]
254 )
255 };
256
257 if (result && result.then) {
258 req._asyncValidationErrors.push(result.then(() => {
259 validatorCfg.negated && req._validationErrors.push(errorObj);
260 }, () => {
261 !validatorCfg.negated && req._validationErrors.push(errorObj);
262 }));
263 } else if ((!validatorCfg.negated && !result) || (validatorCfg.negated && result)) {
264 req._validationErrors.push(errorObj);
265 }
266 });
267
268 context.promise = Promise.all(promises);
269 });
270 }
271
272 var locations = ['body', 'params', 'query', 'cookies'];
273
274 // Extend existing validators. Fixes bug #341
275 req[validatorChainSymbol] = req[validatorChainSymbol] || [];
276 req[validatorChainSymbol].push(options.customValidators);
277
278 // Extend existing sanitizer. Fixes bug #341
279 req[sanitizerSymbol] = req[sanitizerSymbol] || [];
280 req[sanitizerSymbol].push(options.customSanitizers);
281
282 req._validationErrors = [];
283 req._asyncValidationErrors = [];
284 req.validationErrors = function(mapped) {
285 runContexts();
286
287 var result = validationResult(req).formatWith(errorFormatter);
288 if (result.isEmpty()) {
289 return false;
290 }
291
292 return mapped ? result.mapped() : result.array();
293 };
294
295 req.asyncValidationErrors = function(mapped) {
296 runContexts();
297 return Promise.all(req._asyncValidationErrors).then(() => {
298 if (req._validationErrors.length > 0) {
299 return Promise.reject(req.validationErrors(mapped, true));
300 }
301
302 return Promise.resolve();
303 });
304 };
305
306 req.getValidationResult = function() {
307 runContexts();
308 return Promise.all(req._asyncValidationErrors).then(() => {
309 return validationResult(req).formatWith(errorFormatter);
310 });
311 };
312
313 locations.forEach(function(location) {
314 /**
315 * @name req.sanitizeQuery
316 * @see sanitize
317 * @param param
318 */
319 /**
320 * @name req.sanitizeParams
321 * @see sanitize
322 * @param param
323 */
324 /**
325 * @name req.sanitizeBody
326 * @see sanitize
327 * @param param
328 */
329 req['sanitize' + _.capitalize(location)] = function(param) {
330 return new Sanitizer(param, req, [location]);
331 };
332 });
333
334 req.sanitizeHeaders = function(param) {
335 if (param === 'referrer') {
336 param = 'referer';
337 }
338
339 return new Sanitizer(param.toLowerCase(), req, ['headers']);
340 };
341
342 req.sanitize = function(param) {
343 return new Sanitizer(param, req, locations);
344 };
345
346 locations.forEach(function(location) {
347 /**
348 * @name req.checkQuery
349 * @see check
350 * @param param
351 * @param [failMsg]
352 */
353 /**
354 * @name req.checkParams
355 * @see check
356 * @param param
357 * @param [failMsg]
358 */
359 /**
360 * @name req.checkBody
361 * @see check
362 * @param param
363 * @param [failMsg]
364 */
365 /**
366 * @name req.checkCookies
367 * @see check
368 * @param param
369 * @param [failMsg]
370 */
371 req['check' + _.capitalize(location)] = function(param, failMsg) {
372 if (_.isPlainObject(param)) {
373 return validateSchema(param, req, location, contexts);
374 }
375 return createValidationChain(param, location, failMsg, req, contexts);
376 };
377 });
378
379 req.checkHeaders = function(param, failMsg) {
380 if (_.isPlainObject(param)) {
381 return validateSchema(param, req, 'headers', contexts);
382 }
383
384 if (param === 'referrer') {
385 param = 'referer';
386 }
387
388 return createValidationChain(param.toLowerCase(), 'headers', failMsg, req, contexts);
389 };
390
391 req.check = function(param, failMsg) {
392 if (_.isPlainObject(param)) {
393 return validateSchema(param, req, 'any', contexts);
394 }
395 return createValidationChain(param, allLocations, failMsg, req, contexts);
396 };
397
398 req.filter = req.sanitize;
399 req.assert = req.check;
400 req.validate = req.check;
401
402 next();
403 };
404};
405
406module.exports = expressValidator;
407module.exports.validator = validator;
408module.exports.utils = utils;