Skip to content

Commit

Permalink
Merge 097fc97 into 480b1cf
Browse files Browse the repository at this point in the history
  • Loading branch information
papandreou committed Oct 26, 2020
2 parents 480b1cf + 097fc97 commit 14559c2
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 96 deletions.
27 changes: 21 additions & 6 deletions README.md
Expand Up @@ -232,12 +232,10 @@ any of the following keys:

##### extended

The `extended` option allows to choose between parsing the URL-encoded data
with the `querystring` library (when `false`) or the `qs` library (when
`true`). The "extended" syntax allows for rich objects and arrays to be
encoded into the URL-encoded format, allowing for a JSON-like experience
with URL-encoded. For more information, please
[see the qs library](https://www.npmjs.org/package/qs#readme).
The "extended" syntax allows for rich objects and arrays to be encoded into the
URL-encoded format, allowing for a JSON-like experience with URL-encoded. For
more information, please [see the qs
library](https://www.npmjs.org/package/qs#readme).

Defaults to `true`, but using the default has been deprecated. Please
research into the difference between `qs` and `querystring` and choose the
Expand Down Expand Up @@ -279,6 +277,23 @@ The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`
where `buf` is a `Buffer` of the raw request body and `encoding` is the
encoding of the request. The parsing can be aborted by throwing an error.

##### defaultCharset

The default charset to parse as, if not specified in content-type. Must be
either `utf-8` or `iso-8859-1`.

##### charsetSentinel

Whether to let the value of the `utf8` parameter take precedence as the charset
selector. It requires the form to contain a parameter named `utf8` with a value
of ``. Defaults to `false`.

##### interpretNumericEntities

Whether to decode numeric entities such as `☺` when parsing an iso-8859-1
form. Defaults to `false`.


## Errors

The middlewares provided by this module create errors using the
Expand Down
175 changes: 86 additions & 89 deletions lib/types/urlencoded.js
Expand Up @@ -19,18 +19,76 @@ var debug = require('debug')('body-parser:urlencoded')
var deprecate = require('depd')('body-parser')
var read = require('../read')
var typeis = require('type-is')
var qs = require('qs')

/**
* Module exports.
*/

module.exports = urlencoded

var charsetBySentinel = {
// This is what browsers will submit when the ✓ character occurs in an
// application/x-www-form-urlencoded body and the encoding of the page containing
// the form is iso-8859-1, or when the submitted form has an accept-charset
// attribute of iso-8859-1. Presumably also with other charsets that do not contain
// the ✓ character, such as us-ascii.
'%26%2310003%3B': 'iso-8859-1', // encodeURIComponent('✓')
// These are the percent-encoded utf-8 octets representing a checkmark, indicating
// that the request actually is utf-8 encoded.
'%E2%9C%93': 'utf-8' // encodeURIComponent('✓')
}

/**
* Cache of parser modules.
* Helper for creating a decoder function that interprets percent-encoded octets
* in a certain charset
*/
function getDecoder (charset, interpretNumericEntities) {
return function decoder (str) {
var decodedStr = str.replace(/\+/g, ' ')
if (charset === 'iso-8859-1') {
// unescape never throws, no try...catch needed:
return decodedStr.replace(/%[0-9a-f]{2}/gi, unescape)
} else {
// utf-8
try {
decodedStr = decodeURIComponent(decodedStr)
} catch (e) {
// URIError, keep encoded
}
}
if (interpretNumericEntities) {
decodedStr = decodedStr.replace(/&#(\d+);/g, function ($0, numberStr) {
return String.fromCharCode(parseInt(numberStr, 10))
})
}
return decodedStr
}
}

var parsers = Object.create(null)
/**
* Helper for creating a decoder for the application/x-www-url-encoded body given
* the parsing options
*/
function createBodyDecoder (queryparse, charset, charsetSentinel, interpretNumericEntities) {
var correctedCharset = charset
return function bodyDecoder (body) {
var modifiedBody = body
if (charsetSentinel) {
modifiedBody = modifiedBody.replace(/(^|&)utf8=([^&]+)($|&)/, function ($0, ampBefore, value, ampAfter) {
if (charsetBySentinel[value]) {
correctedCharset = charsetBySentinel[value]
}
// Make sure that we only leave an ampersand when replacing in the middle of the query string
// as the simple parser will add an empty string parameter if it gets &&
return ampBefore && ampAfter ? '&' : ''
})
}
return modifiedBody.length
? queryparse(modifiedBody, getDecoder(correctedCharset, interpretNumericEntities))
: {}
}
}

/**
* Create a middleware to parse urlencoded bodies.
Expand All @@ -55,27 +113,26 @@ function urlencoded (options) {
: opts.limit
var type = opts.type || 'application/x-www-form-urlencoded'
var verify = opts.verify || false
var charsetSentinel = opts.charsetSentinel
var interpretNumericEntities = opts.interpretNumericEntities

if (verify !== false && typeof verify !== 'function') {
throw new TypeError('option verify must be function')
}

var defaultCharset = opts.defaultCharset || 'utf-8'
if (defaultCharset !== 'utf-8' && defaultCharset !== 'iso-8859-1') {
throw new TypeError('option defaultCharset must be either utf-8 or iso-8859-1')
}

// create the appropriate query parser
var queryparse = extended
? extendedparser(opts)
: simpleparser(opts)
var queryparse = createQueryParser(opts, extended)

// create the appropriate type checking function
var shouldParse = typeof type !== 'function'
? typeChecker(type)
: type

function parse (body) {
return body.length
? queryparse(body)
: {}
}

return function urlencodedParser (req, res, next) {
if (req._body) {
debug('body already parsed')
Expand All @@ -102,8 +159,8 @@ function urlencoded (options) {
}

// assert charset
var charset = getCharset(req) || 'utf-8'
if (charset !== 'utf-8') {
var charset = getCharset(req) || defaultCharset
if (charset !== 'utf-8' && charset !== 'iso-8859-1') {
debug('invalid charset')
next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
charset: charset,
Expand All @@ -113,8 +170,7 @@ function urlencoded (options) {
}

// read
read(req, res, next, parse, debug, {
debug: debug,
read(req, res, next, createBodyDecoder(queryparse, charset, charsetSentinel, interpretNumericEntities), debug, {
encoding: charset,
inflate: inflate,
limit: limit,
Expand All @@ -129,11 +185,12 @@ function urlencoded (options) {
* @param {object} options
*/

function extendedparser (options) {
function createQueryParser (options, extended) {
var parameterLimit = options.parameterLimit !== undefined
? options.parameterLimit
: 1000
var parse = parser('qs')
var charsetSentinel = options.charsetSentinel
var interpretNumericEntities = options.interpretNumericEntities

if (isNaN(parameterLimit) || parameterLimit < 1) {
throw new TypeError('option parameterLimit must be a positive number')
Expand All @@ -143,7 +200,9 @@ function extendedparser (options) {
parameterLimit = parameterLimit | 0
}

return function queryparse (body) {
var depth = extended ? Infinity : 0

return function queryparse (body, decoder) {
var paramCount = parameterCount(body, parameterLimit)

if (paramCount === undefined) {
Expand All @@ -153,14 +212,18 @@ function extendedparser (options) {
})
}

var arrayLimit = Math.max(100, paramCount)
var arrayLimit = extended ? Math.max(100, paramCount) : 0

debug('parse extended urlencoding')
return parse(body, {
debug('parse ' + (extended ? 'extended ' : '') + 'urlencoding')

return qs.parse(body, {
allowPrototypes: true,
arrayLimit: arrayLimit,
depth: Infinity,
parameterLimit: parameterLimit
depth: depth,
parameterLimit: parameterLimit,
decoder: decoder,
charsetSentinel: charsetSentinel,
interpretNumericEntities: interpretNumericEntities
})
}
}
Expand Down Expand Up @@ -204,72 +267,6 @@ function parameterCount (body, limit) {
return count
}

/**
* Get parser for module name dynamically.
*
* @param {string} name
* @return {function}
* @api private
*/

function parser (name) {
var mod = parsers[name]

if (mod !== undefined) {
return mod.parse
}

// this uses a switch for static require analysis
switch (name) {
case 'qs':
mod = require('qs')
break
case 'querystring':
mod = require('querystring')
break
}

// store to prevent invoking require()
parsers[name] = mod

return mod.parse
}

/**
* Get the simple query parser.
*
* @param {object} options
*/

function simpleparser (options) {
var parameterLimit = options.parameterLimit !== undefined
? options.parameterLimit
: 1000
var parse = parser('querystring')

if (isNaN(parameterLimit) || parameterLimit < 1) {
throw new TypeError('option parameterLimit must be a positive number')
}

if (isFinite(parameterLimit)) {
parameterLimit = parameterLimit | 0
}

return function queryparse (body) {
var paramCount = parameterCount(body, parameterLimit)

if (paramCount === undefined) {
debug('too many parameters')
throw createError(413, 'too many parameters', {
type: 'parameters.too.many'
})
}

debug('parse urlencoding')
return parse(body, undefined, undefined, { maxKeys: parameterLimit })
}
}

/**
* Get the simple type checker.
*
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -16,7 +16,7 @@
"http-errors": "1.7.3",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.9.3",
"qs": "6.9.4",
"raw-body": "2.4.1",
"type-is": "~1.6.18"
},
Expand Down
68 changes: 68 additions & 0 deletions test/urlencoded.js
Expand Up @@ -42,6 +42,74 @@ describe('bodyParser.urlencoded()', function () {
.expect(200, '{}', done)
})

var extendedValues = [true, false]
extendedValues.forEach(function (extended) {
describe('in ' + (extended ? 'extended' : 'simple') + ' mode', function () {
it('should parse x-www-form-urlencoded with an explicit iso-8859-1 encoding', function (done) {
var server = createServer({ extended: extended })
request(server)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded; charset=iso-8859-1')
.send('%A2=%BD')
.expect(200, '{"¢":"½"}', done)
})

it('should parse x-www-form-urlencoded with unspecified iso-8859-1 encoding when the defaultCharset is set to iso-8859-1', function (done) {
var server = createServer({ defaultCharset: 'iso-8859-1', extended: extended })
request(server)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send('%A2=%BD')
.expect(200, '{"¢":"½"}', done)
})

it('should parse x-www-form-urlencoded with an unspecified iso-8859-1 encoding when the utf8 sentinel has a value of %26%2310003%3B', function (done) {
var server = createServer({ charsetSentinel: true, extended: extended })
request(server)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send('utf8=%26%2310003%3B&user=%C3%B8')
.expect(200, '{"user":"ø"}', done)
})

it('should parse x-www-form-urlencoded with an unspecified utf-8 encoding when the utf8 sentinel has a value of %E2%9C%93 and the defaultCharset is iso-8859-1', function (done) {
var server = createServer({ charsetSentinel: true, extended: extended })
request(server)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send('utf8=%E2%9C%93&user=%C3%B8')
.expect(200, '{"user":"ø"}', done)
})

it('should not leave an empty string parameter when removing the utf8 sentinel from the start of the string', function (done) {
var server = createServer({ charsetSentinel: true, extended: extended })
request(server)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send('utf8=%E2%9C%93&foo=bar')
.expect(200, '{"foo":"bar"}', done)
})

it('should not leave an empty string parameter when removing the utf8 sentinel from the middle of the string', function (done) {
var server = createServer({ charsetSentinel: true, extended: extended })
request(server)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send('foo=bar&utf8=%E2%9C%93&baz=quux')
.expect(200, '{"foo":"bar","baz":"quux"}', done)
})

it('should not leave an empty string parameter when removing the utf8 sentinel from the end of the string', function (done) {
var server = createServer({ charsetSentinel: true, extended: extended })
request(server)
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send('foo=bar&baz=quux&utf8=%E2%9C%93')
.expect(200, '{"foo":"bar","baz":"quux"}', done)
})
})
})

it('should handle empty message-body', function (done) {
request(createServer({ limit: '1kb' }))
.post('/')
Expand Down

0 comments on commit 14559c2

Please sign in to comment.