Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add parseJsonFields option #1037

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## 1.4.5-lts.1

- No changes
## 1.4.4-lts.1

- Bugfix: Bump busboy to fix CVE-2022-24434 (#1097)
- Breaking: Require Node.js 6.0.0 or later (#1097)

## 1.4.4 - 2021-12-07

- Bugfix: Handle missing field names (#913)
Expand Down
8 changes: 8 additions & 0 deletions README.md
Expand Up @@ -138,6 +138,7 @@ Key | Description
`dest` or `storage` | Where to store the files
`fileFilter` | Function to control which files are accepted
`limits` | Limits of the uploaded data
`parseJsonFields` | Parse fields that have the content-type `application/json`
`preservePath` | Keep the full path of files instead of just the base name

In an average web app, only `dest` might be required, and configured as shown in
Expand Down Expand Up @@ -299,6 +300,13 @@ function fileFilter (req, file, cb) {
}
```

### `parseJsonFields`

Fields may also have a `Content-Type` header. If you set `parseJsonFields` to
`true` these fields will be parsed using `JSON.parse()` instead of handled as
plain text strings. This way you don't need to unroll complex JSON structures
that are transmitted alongside uploaded files as url-encoded fields.

## Error handling

When encountering an error, Multer will delegate the error to Express. You can
Expand Down
7 changes: 5 additions & 2 deletions index.js
Expand Up @@ -20,6 +20,7 @@ function Multer (options) {
this.limits = options.limits
this.preservePath = options.preservePath
this.fileFilter = options.fileFilter || allowAll
this.parseJsonFields = !!options.parseJsonFields
}

Multer.prototype._makeMiddleware = function (fields, fileStrategy) {
Expand Down Expand Up @@ -49,7 +50,8 @@ Multer.prototype._makeMiddleware = function (fields, fileStrategy) {
preservePath: this.preservePath,
storage: this.storage,
fileFilter: wrappedFileFilter,
fileStrategy: fileStrategy
fileStrategy: fileStrategy,
parseJsonFields: this.parseJsonFields
}
}

Expand Down Expand Up @@ -79,7 +81,8 @@ Multer.prototype.any = function () {
preservePath: this.preservePath,
storage: this.storage,
fileFilter: this.fileFilter,
fileStrategy: 'ARRAY'
fileStrategy: 'ARRAY',
parseJsonFields: this.parseJsonFields
}
}

Expand Down
31 changes: 16 additions & 15 deletions lib/make-middleware.js
@@ -1,18 +1,13 @@
var is = require('type-is')
var Busboy = require('busboy')
var extend = require('xtend')
var onFinished = require('on-finished')
var appendField = require('append-field')

var Counter = require('./counter')
var MulterError = require('./multer-error')
var FileAppender = require('./file-appender')
var removeUploadedFiles = require('./remove-uploaded-files')

function drainStream (stream) {
stream.on('readable', stream.read.bind(stream))
}

function makeMiddleware (setup) {
return function multerMiddleware (req, res, next) {
if (!is(req, ['multipart'])) return next()
Expand All @@ -24,13 +19,14 @@ function makeMiddleware (setup) {
var fileFilter = options.fileFilter
var fileStrategy = options.fileStrategy
var preservePath = options.preservePath
var parseJsonFields = options.parseJsonFields

req.body = Object.create(null)

var busboy

try {
busboy = new Busboy({ headers: req.headers, limits: limits, preservePath: preservePath })
busboy = Busboy({ headers: req.headers, limits: limits, preservePath: preservePath })
} catch (err) {
return next(err)
}
Expand All @@ -45,12 +41,9 @@ function makeMiddleware (setup) {
function done (err) {
if (isDone) return
isDone = true

req.unpipe(busboy)
drainStream(req)
busboy.removeAllListeners()

onFinished(req, function () { next(err) })
next(err)
}

function indicateDone () {
Expand Down Expand Up @@ -80,21 +73,29 @@ function makeMiddleware (setup) {
}

// handle text field data
busboy.on('field', function (fieldname, value, fieldnameTruncated, valueTruncated) {
busboy.on('field', function (fieldname, value, { nameTruncated, valueTruncated, mimeType }) {
if (fieldname == null) return abortWithCode('MISSING_FIELD_NAME')
if (fieldnameTruncated) return abortWithCode('LIMIT_FIELD_KEY')
if (nameTruncated) return abortWithCode('LIMIT_FIELD_KEY')
if (valueTruncated) return abortWithCode('LIMIT_FIELD_VALUE', fieldname)

// Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6)
if (limits && Object.prototype.hasOwnProperty.call(limits, 'fieldNameSize')) {
if (fieldname.length > limits.fieldNameSize) return abortWithCode('LIMIT_FIELD_KEY')
}

if (parseJsonFields && mimeType === 'application/json') {
try {
value = JSON.parse(value)
} catch (error) {
return abortWithError(error)
}
}

appendField(req.body, fieldname, value)
})

// handle files
busboy.on('file', function (fieldname, fileStream, filename, encoding, mimetype) {
busboy.on('file', function (fieldname, fileStream, { filename, encoding, mimeType }) {
// don't attach to the files object, if there is no file
if (!filename) return fileStream.resume()

Expand All @@ -107,7 +108,7 @@ function makeMiddleware (setup) {
fieldname: fieldname,
originalname: filename,
encoding: encoding,
mimetype: mimetype
mimetype: mimeType
}

var placeholder = appender.insertPlaceholder(file)
Expand Down Expand Up @@ -169,7 +170,7 @@ function makeMiddleware (setup) {
busboy.on('partsLimit', function () { abortWithCode('LIMIT_PART_COUNT') })
busboy.on('filesLimit', function () { abortWithCode('LIMIT_FILE_COUNT') })
busboy.on('fieldsLimit', function () { abortWithCode('LIMIT_FIELD_COUNT') })
busboy.on('finish', function () {
busboy.on('close', function () {
readFinished = true
indicateDone()
})
Expand Down
7 changes: 3 additions & 4 deletions package.json
@@ -1,7 +1,7 @@
{
"name": "multer",
"description": "Middleware for handling `multipart/form-data`.",
"version": "1.4.4",
"version": "1.4.5-lts.1",
"contributors": [
"Hage Yaapa <captain@hacksparrow.com> (http://www.hacksparrow.com)",
"Jaret Pfluger <https://github.com/jpfluger>",
Expand All @@ -20,11 +20,10 @@
],
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"on-finished": "^2.3.0",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
Expand All @@ -39,7 +38,7 @@
"testdata-w3c-json-form": "^1.0.0"
},
"engines": {
"node": ">= 0.10.0"
"node": ">= 6.0.0"
},
"files": [
"LICENSE",
Expand Down
8 changes: 1 addition & 7 deletions test/_util.js
@@ -1,7 +1,6 @@
var fs = require('fs')
var path = require('path')
var stream = require('stream')
var onFinished = require('on-finished')

exports.file = function file (name) {
return fs.createReadStream(path.join(__dirname, 'files', name))
Expand All @@ -17,19 +16,14 @@ exports.submitForm = function submitForm (multer, form, cb) {

var req = new stream.PassThrough()

req.complete = false
form.once('end', function () {
req.complete = true
})

form.pipe(req)
req.headers = {
'content-type': 'multipart/form-data; boundary=' + form.getBoundary(),
'content-length': length
}

multer(req, null, function (err) {
onFinished(req, function () { cb(err, req) })
cb(err, req)
})
})
}
2 changes: 1 addition & 1 deletion test/error-handling.js
Expand Up @@ -244,7 +244,7 @@ describe('Error Handling', function () {
req.end(body)

upload(req, null, function (err) {
assert.strictEqual(err.message, 'Unexpected end of multipart data')
assert.strictEqual(err.message, 'Unexpected end of form')
done()
})
})
Expand Down
3 changes: 1 addition & 2 deletions test/express-integration.js
Expand Up @@ -8,7 +8,6 @@ var util = require('./_util')
var express = require('express')
var FormData = require('form-data')
var concat = require('concat-stream')
var onFinished = require('on-finished')

var port = 34279

Expand All @@ -27,7 +26,7 @@ describe('Express Integration', function () {
req.on('response', function (res) {
res.on('error', cb)
res.pipe(concat({ encoding: 'buffer' }, function (body) {
onFinished(req, function () { cb(null, res, body) })
cb(null, res, body)
}))
})
}
Expand Down
36 changes: 21 additions & 15 deletions test/unicode.js
Expand Up @@ -2,12 +2,10 @@

var assert = require('assert')

var path = require('path')
var util = require('./_util')
var multer = require('../')
var temp = require('fs-temp')
var rimraf = require('rimraf')
var FormData = require('form-data')
var stream = require('stream')

describe('Unicode', function () {
var uploadDir, upload
Expand All @@ -34,21 +32,29 @@ describe('Unicode', function () {
})

it('should handle unicode filenames', function (done) {
var form = new FormData()
var parser = upload.single('small0')
var filename = '\ud83d\udca9.dat'

form.append('small0', util.file('small0.dat'), { filename: filename })

util.submitForm(parser, form, function (err, req) {
var req = new stream.PassThrough()
var boundary = 'AaB03x'
var body = [
'--' + boundary,
'Content-Disposition: form-data; name="small0"; filename="poo.dat"; filename*=utf-8\'\'%F0%9F%92%A9.dat',
'Content-Type: text/plain',
'',
'test with unicode filename',
'--' + boundary + '--'
].join('\r\n')

req.headers = {
'content-type': 'multipart/form-data; boundary=' + boundary,
'content-length': body.length
}

req.end(body)

upload.single('small0')(req, null, function (err) {
assert.ifError(err)

assert.strictEqual(path.basename(req.file.path), filename)
assert.strictEqual(req.file.originalname, filename)

assert.strictEqual(req.file.originalname, '\ud83d\udca9.dat')
assert.strictEqual(req.file.fieldname, 'small0')
assert.strictEqual(req.file.size, 1778)
assert.strictEqual(util.fileSize(req.file.path), 1778)

done()
})
Expand Down