formidable
Advanced tools
Comparing version 2.0.0-canary.20200212.1 to 2.0.0-canary.20200226.1
@@ -23,2 +23,4 @@ ### Unreleased (`canary` & `dev` dist-tags) | ||
* fix: update docs and examples [#544](https://github.com/node-formidable/node-formidable/pull/544) ([#248](https://github.com/node-formidable/node-formidable/issues/248), [#335](https://github.com/node-formidable/node-formidable/issues/335), [#371](https://github.com/node-formidable/node-formidable/issues/371), [#372](https://github.com/node-formidable/node-formidable/issues/372), [#387](https://github.com/node-formidable/node-formidable/issues/387), partly [#471](https://github.com/node-formidable/node-formidable/issues/471), [#535](https://github.com/node-formidable/node-formidable/issues/535)) | ||
* feat: introduce Plugins API, fix silent failing tests ([#545](https://github.com/node-formidable/node-formidable/pull/545), [#391](https://github.com/node-formidable/node-formidable/pull/391), [#407](https://github.com/node-formidable/node-formidable/pull/407), [#386](https://github.com/node-formidable/node-formidable/pull/386), [#374](https://github.com/node-formidable/node-formidable/pull/374), [#521](https://github.com/node-formidable/node-formidable/pull/521), [#267](https://github.com/node-formidable/node-formidable/pull/267)) | ||
* respect form hash option on incoming octect/stream requests ([#407](https://github.com/node-formidable/node-formidable/pull/407)) | ||
* fix: exposing file writable stream errors ([#520](https://github.com/node-formidable/node-formidable/pull/520), [#316](https://github.com/node-formidable/node-formidable/pull/316), [#469](https://github.com/node-formidable/node-formidable/pull/469), [#470](https://github.com/node-formidable/node-formidable/pull/470)) | ||
@@ -25,0 +27,0 @@ |
154
package.json
{ | ||
"name": "formidable", | ||
"version": "2.0.0-canary.20200212.1", | ||
"license": "MIT", | ||
"description": "A node.js module for parsing form data, especially file uploads.", | ||
"homepage": "https://github.com/node-formidable/node-formidable", | ||
"funding": "https://ko-fi.com/tunnckoCore/commissions", | ||
"repository": "node-formidable/node-formidable", | ||
"main": "./src/index.js", | ||
"files": [ | ||
"src" | ||
], | ||
"publishConfig": { | ||
"access": "public", | ||
"tag": "canary" | ||
}, | ||
"scripts": { | ||
"bench": "node benchmark", | ||
"fmt": "yarn run fmt:prepare '**/*'", | ||
"fmt:prepare": "prettier --write", | ||
"lint": "yarn run lint:prepare .", | ||
"lint:prepare": "eslint --cache --fix --quiet --format codeframe", | ||
"reinstall": "rm -rf node_modules yarn.lock && yarn", | ||
"pretest": "rm -rf test/tmp && mkdir test/tmp", | ||
"test": "node test/run.js", | ||
"pretest:ci": "yarn pretest", | ||
"test:ci": "nyc node test/run.js" | ||
}, | ||
"devDependencies": { | ||
"@commitlint/cli": "^8.3.5", | ||
"@commitlint/config-conventional": "^8.3.4", | ||
"@tunnckocore/prettier-config": "^1.3.3", | ||
"eslint": "^6.8.0", | ||
"eslint-config-airbnb-base": "^14.0.0", | ||
"eslint-config-prettier": "^6.10.0", | ||
"eslint-plugin-import": "^2.20.1", | ||
"eslint-plugin-prettier": "^3.1.2", | ||
"husky": "^4.2.2", | ||
"jest": "^25.1.0", | ||
"lint-staged": "^10.0.7", | ||
"nyc": "^15.0.0", | ||
"prettier": "^1.19.1", | ||
"prettier-plugin-pkgjson": "^0.2.3", | ||
"request": "^2.88.2", | ||
"urun": "^0.0.8", | ||
"utest": "^0.0.8" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "git status --porcelain && yarn lint-staged", | ||
"commit-msg": "yarn commitlint -E HUSKY_GIT_PARAMS" | ||
} | ||
}, | ||
"commitlint": { | ||
"extends": [ | ||
"@commitlint/config-conventional" | ||
] | ||
}, | ||
"lint-staged": { | ||
"!*.{js,jsx,ts,tsx}": [ | ||
"yarn run fmt:prepare" | ||
], | ||
"*.{js,jsx,ts,tsx}": [ | ||
"yarn run lint" | ||
] | ||
}, | ||
"renovate": { | ||
"extends": [ | ||
"@tunnckocore" | ||
] | ||
} | ||
"name": "formidable", | ||
"version": "2.0.0-canary.20200226.1", | ||
"license": "MIT", | ||
"description": "A node.js module for parsing form data, especially file uploads.", | ||
"homepage": "https://github.com/node-formidable/formidable", | ||
"funding": "https://ko-fi.com/tunnckoCore/commissions", | ||
"repository": "node-formidable/formidable", | ||
"main": "./src/index.js", | ||
"files": [ | ||
"src", | ||
"test" | ||
], | ||
"publishConfig": { | ||
"access": "public", | ||
"tag": "canary" | ||
}, | ||
"scripts": { | ||
"bench": "node benchmark", | ||
"fmt": "yarn run fmt:prepare '**/*'", | ||
"fmt:prepare": "prettier --write", | ||
"lint": "yarn run lint:prepare .", | ||
"lint:prepare": "eslint --cache --fix --quiet --format codeframe", | ||
"pretest": "yarn del ./test/tmp", | ||
"postpretest": "yarn make-dir ./test/tmp", | ||
"reinstall": "yarn del ./node_modules ./yarn.lock", | ||
"postreinstall": "yarn setup", | ||
"setup": "yarn", | ||
"test": "yarn node test/run.js", | ||
"pretest:ci": "yarn pretest", | ||
"test:ci": "nyc node test/run.js", | ||
"test:jest": "jest --coverage" | ||
}, | ||
"dependencies": { | ||
"dezalgo": "^1.0.3", | ||
"once": "^1.4.0" | ||
}, | ||
"devDependencies": { | ||
"@commitlint/cli": "^8.3.5", | ||
"@commitlint/config-conventional": "^8.3.4", | ||
"@tunnckocore/prettier-config": "^1.3.3", | ||
"del-cli": "^3.0.0", | ||
"eslint": "^6.8.0", | ||
"eslint-config-airbnb-base": "^14.0.0", | ||
"eslint-config-prettier": "^6.10.0", | ||
"eslint-plugin-import": "^2.20.1", | ||
"eslint-plugin-prettier": "^3.1.2", | ||
"express": "^4.17.1", | ||
"husky": "^4.2.2", | ||
"jest": "^25.1.0", | ||
"koa": "^2.11.0", | ||
"lint-staged": "^10.0.7", | ||
"make-dir-cli": "^2.0.0", | ||
"nyc": "^15.0.0", | ||
"prettier": "^1.19.1", | ||
"prettier-plugin-pkgjson": "^0.2.3", | ||
"request": "^2.88.2", | ||
"supertest": "^4.0.2", | ||
"urun": "^0.0.8", | ||
"utest": "^0.0.8" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "git status --porcelain && yarn lint-staged", | ||
"commit-msg": "yarn commitlint -E HUSKY_GIT_PARAMS" | ||
} | ||
}, | ||
"commitlint": { | ||
"extends": [ | ||
"@commitlint/config-conventional" | ||
] | ||
}, | ||
"lint-staged": { | ||
"!*.{js,jsx,ts,tsx}": [ | ||
"yarn run fmt:prepare" | ||
], | ||
"*.{js,jsx,ts,tsx}": [ | ||
"yarn run lint" | ||
] | ||
}, | ||
"renovate": { | ||
"extends": [ | ||
"@tunnckocore" | ||
] | ||
} | ||
} |
330
README.md
<p align="center"> | ||
<img alt="node formidable logo" src="./logo.png" /> | ||
<img alt="npm formidable package logo" src="https://raw.githubusercontent.com/node-formidable/formidable/master/logo.png" /> | ||
</p> | ||
# formidable [![npm version][npmv-img]][npmv-url] [![MIT license][license-img]][license-url] [![Libera Manifesto][libera-manifesto-img]][libera-manifesto-url] | ||
# formidable [![npm version][npmv-img]][npmv-url] [![MIT license][license-img]][license-url] [![Libera Manifesto][libera-manifesto-img]][libera-manifesto-url] [![Twitter][twitter-img]][twitter-url] | ||
@@ -10,12 +10,35 @@ > A Node.js module for parsing form data, especially file uploads. | ||
[![Code style][codestyle-img]][codestyle-url] | ||
[![build status][build-img]][build-url] | ||
[![codecoverage][codecov-img]][codecov-url] | ||
[![linux build status][linux-build-img]][build-url] | ||
[![windows build status][windows-build-img]][build-url] | ||
[![macos build status][macos-build-img]][build-url] | ||
If you have any _how-to_ kind of questions, please read the [Contributing | ||
Guide][contributing-url] and [Code of Conduct][code_of_conduct-url] | ||
documents.<br /> For bugs reports and feature requests, [please create an | ||
issue][open-issue-url] or ping [@tunnckoCore](https://twitter.com/tunnckoCore) | ||
at Twitter. | ||
[![Conventional Commits][ccommits-img]][ccommits-url] | ||
[![Minimum Required Nodejs][nodejs-img]][npmv-url] | ||
[![Tidelift Subcsription][tidelift-img]][tidelift-url] | ||
[![Buy me a Kofi][kofi-img]][kofi-url] | ||
[![Renovate App Status][renovateapp-img]][renovateapp-url] | ||
[![Make A Pull Request][prs-welcome-img]][prs-welcome-url] | ||
[![Twitter][twitter-img]][twitter-url] | ||
This project is [semantically versioned](https://semver.org) and available as | ||
part of the [Tidelift Subscription][tidelift-url] for professional grade | ||
assurances, enhanced support and security. | ||
[Learn more.](https://tidelift.com/subscription/pkg/npm-formidable?utm_source=npm-formidable&utm_medium=referral&utm_campaign=enterprise) | ||
_The maintainers of `formidable` and thousands of other packages are working | ||
with Tidelift to deliver commercial support and maintenance for the Open Source | ||
dependencies you use to build your applications. Save time, reduce risk, and | ||
improve code health, while paying the maintainers of the exact dependencies you | ||
use._ | ||
[![][npm-weekly-img]][npmv-url] [![][npm-monthly-img]][npmv-url] | ||
[![][npm-yearly-img]][npmv-url] [![][npm-alltime-img]][npmv-url] | ||
## Status: Maintained [![npm version][npmv-canary-img]][npmv-url] [![npm version][npmv-dev-img]][npmv-url] | ||
## Status: Maintained [![npm version][npmv-canary-img]][npmv-url] | ||
@@ -31,21 +54,18 @@ This module was initially developed by | ||
are always welcome! :heart: Jump on | ||
[issue #412](https://github.com/felixge/node-formidable/issues/412) if you are | ||
interested. | ||
[issue #412](https://github.com/felixge/node-formidable/issues/412) which is | ||
closed, but if you are interested we can discuss it and add you after strict | ||
rules, like enabling Two-Factor Auth in your npm and GitHub accounts. | ||
_**Note:** Master is a "canary" branch - try it with `npm i formidable@canary`. | ||
Do not expect (for now) things from it to be inside the`latest`"dist-tag" in the | ||
Npm. The`formidable@latest`is the`v1.2.1` version and probably it will be the | ||
last`v1` release!_ | ||
_**Note:** The github `master` branch is a "canary" branch - try it with | ||
`npm i formidable@canary`. Do not expect (for now) things from it to be inside | ||
the`latest` "dist-tag" in the Npm. The`formidable@latest`is the`v1.2.1` version | ||
and probably it will be the last`v1` release!_ | ||
_**Note: v2 is coming soon!**_ | ||
You can try the | ||
[Plugins API](https://github.com/felixge/node-formidable/tree/plugins-api) | ||
([#545](https://github.com/felixge/node-formidable/pull/545)), which is | ||
available through `formidable@dev`. | ||
## Highlights | ||
- Fast (~900-2500 mb/sec), streaming multipart parser | ||
- Automatically writing file uploads to disk | ||
- [Fast (~900-2500 mb/sec)](#benchmarks) & streaming multipart parser | ||
- Automatically writing file uploads to disk (soon optionally) | ||
- [Plugins API](#useplugin-plugin) - allowing custom parsers and plugins | ||
- Low memory footprint | ||
@@ -57,2 +77,6 @@ - Graceful error handling | ||
This project requires `Node.js >= 10.13`. Install it using | ||
[yarn](https://yarnpkg.com) or [npm](https://npmjs.com).<br /> _We highly | ||
recommend to use Yarn when you think to contribute to this project._ | ||
```sh | ||
@@ -72,51 +96,160 @@ npm install formidable | ||
This is a low-level package, and if you're using a high-level framework it may | ||
already be included. | ||
This is a low-level package, and if you're using a high-level framework it _may_ | ||
already be included. Check the examples below and the `examples/` folder. | ||
However, [Express v4](http://expressjs.com) does not include any multipart | ||
handling, nor does [body-parser](https://github.com/expressjs/body-parser). | ||
## Examples | ||
For `koa` there is [koa-better-body](https://ghub.now.sh/koa-better-body) which | ||
can handle ANY type of body / form-data - JSON, urlencoded, multpart and so on. | ||
A new major release is coming there too. | ||
For more examples look at the `examples/` directory. | ||
## Example | ||
### with Node.js http module | ||
Parse an incoming file upload. | ||
Parse an incoming file upload, with the | ||
[Node.js's built-in `http` module](https://nodejs.org/api/http.html). | ||
```js | ||
const http = require('http'); | ||
const util = require('util'); | ||
const formidable = require('formidable'); | ||
http | ||
.createServer((req, res) => { | ||
if (req.url === '/upload' && req.method.toLowerCase() === 'post') { | ||
// parse a file upload | ||
const form = formidable(); | ||
const server = http.createServer((req, res) => { | ||
if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') { | ||
// parse a file upload | ||
const form = formidable({ multiples: true }); | ||
form.parse(req, (err, fields, files) => { | ||
res.writeHead(200, { 'content-type': 'text/plain' }); | ||
res.write('received upload:\n\n'); | ||
res.end(util.inspect({ fields: fields, files: files })); | ||
}); | ||
form.parse(req, (err, fields, files) => { | ||
res.writeHead(200, { 'content-type': 'application/json' }); | ||
res.end(JSON.stringify({ fields, files }, null, 2)); | ||
}); | ||
return; | ||
} | ||
// show a file upload form | ||
res.writeHead(200, { 'content-type': 'text/html' }); | ||
res.end(` | ||
<h2>With Node.js <code>"http"</code> module</h2> | ||
<form action="/api/upload" enctype="multipart/form-data" method="post"> | ||
<div>Text field title: <input type="text" name="title" /></div> | ||
<div>File: <input type="file" name="multipleFiles" multiple="multiple" /></div> | ||
<input type="submit" value="Upload" /> | ||
</form> | ||
`); | ||
}); | ||
server.listen(8080, () => { | ||
console.log('Server listening on http://localhost:8080/ ...'); | ||
}); | ||
``` | ||
### with Express.js | ||
There are multiple variants to do this, but Formidable just need Node.js Request | ||
stream, so something like the following example should work just fine, without | ||
any third-party [Express.js](https://ghub.now.sh/express) middleware. | ||
Or try the | ||
[examples/with-express.js](https://github.com/node-formidable/node-formidable/blob/master/examples/with-express.js) | ||
```js | ||
const express = require('express'); | ||
const formidable = require('formidable'); | ||
const app = express(); | ||
app.get('/', (req, res) => { | ||
res.send(` | ||
<h2>With <code>"express"</code> npm package</h2> | ||
<form action="/api/upload" enctype="multipart/form-data" method="post"> | ||
<div>Text field title: <input type="text" name="title" /></div> | ||
<div>File: <input type="file" name="someExpressFiles" multiple="multiple" /></div> | ||
<input type="submit" value="Upload" /> | ||
</form> | ||
`); | ||
}); | ||
app.post('/api/upload', (req, res, next) => { | ||
const form = formidable({ multiples: true }); | ||
form.parse(req, (err, fields, files) => { | ||
if (err) { | ||
next(err); | ||
return; | ||
} | ||
res.json({ fields, files }); | ||
}); | ||
}); | ||
// show a file upload form | ||
res.writeHead(200, { 'content-type': 'text/html' }); | ||
res.end(` | ||
<form action="/upload" enctype="multipart/form-data" method="post"> | ||
<input type="text" name="title" /><br/> | ||
<input type="file" name="upload" multiple="multiple" /><br/> | ||
<input type="submit" value="Upload" /> | ||
</form> | ||
`); | ||
}) | ||
.listen(8080, () => { | ||
console.log('Server listening on http://localhost:8080/ ...'); | ||
}); | ||
app.listen(3000, () => { | ||
console.log('Server listening on http://localhost:3000 ...'); | ||
}); | ||
``` | ||
### with Koa and Formidable | ||
Of course, with [Koa v1, v2 or future v3](https://ghub.now.sh/koa) the things | ||
are very similar. You can use `formidable` manually as shown below or through | ||
the [koa-better-body](https://ghub.now.sh/koa-better-body) package which is | ||
using `formidable` under the hood and support more features and different | ||
request bodies, check its documentation for more info. | ||
_Note: this example is assuming Koa v2. Be aware that you should pass `ctx.req` | ||
which is Node.js's Request, and **NOT** the `ctx.request` which is Koa's Request | ||
object - there is a difference._ | ||
```js | ||
const Koa = require('koa'); | ||
const formidable = require('formidable'); | ||
const app = new Koa(); | ||
app.on('error', (err) => { | ||
console.error('server error', err); | ||
}); | ||
app.use(async (ctx, next) => { | ||
if (ctx.url === '/api/upload' && ctx.method.toLowerCase() === 'post') { | ||
const form = formidable({ multiples: true }); | ||
// not very elegant, but that's for now if you don't want touse `koa-better-body` | ||
// or other middlewares. | ||
await new Promise((resolve, reject) => { | ||
form.parse(ctx.req, (err, fields, files) => { | ||
if (err) { | ||
reject(err); | ||
return; | ||
} | ||
ctx.set('Content-Type', 'application/json'); | ||
ctx.status = 200; | ||
ctx.state = { fields, files }; | ||
ctx.body = JSON.stringify(ctx.state, null, 2); | ||
resolve(); | ||
}); | ||
}); | ||
await next(); | ||
return; | ||
} | ||
// show a file upload form | ||
ctx.set('Content-Type', 'text/html'); | ||
ctx.status = 200; | ||
ctx.body = ` | ||
<h2>With <code>"koa"</code> npm package</h2> | ||
<form action="/api/upload" enctype="multipart/form-data" method="post"> | ||
<div>Text field title: <input type="text" name="title" /></div> | ||
<div>File: <input type="file" name="koaFiles" multiple="multiple" /></div> | ||
<input type="submit" value="Upload" /> | ||
</form> | ||
`; | ||
}); | ||
app.use((ctx) => { | ||
console.log('The next middleware is called'); | ||
console.log('Results:', ctx.state); | ||
}); | ||
app.listen(3000, () => { | ||
console.log('Server listening on http://localhost:3000 ...'); | ||
}); | ||
``` | ||
## Benchmarks | ||
@@ -162,4 +295,4 @@ | ||
_Please pass [`options`](#options) to the function/constructor, not by passing | ||
assigning them to the instance `form`_ | ||
_Please pass [`options`](#options) to the function/constructor, not by assigning | ||
them to the instance `form`_ | ||
@@ -211,3 +344,4 @@ ```js | ||
_**Note:** If this value is exceeded, an `'error'` event is emitted._ | ||
_**Note:** If this size of combined fields, or size of some file is exceeded, an | ||
`'error'` event is fired._ | ||
@@ -299,2 +433,64 @@ ```js | ||
### .use(plugin: Plugin) | ||
A method that allows you to extend the Formidable library. By default we include | ||
4 plugins, which esentially are adapters to plug the different built-in parsers. | ||
**The plugins added by this method are always enabled.** | ||
_See [src/plugins/](./src/plugins/) for more detailed look on default plugins._ | ||
The `plugin` param has such signature: | ||
```typescript | ||
function(formidable: Formidable, options: Options): void; | ||
``` | ||
The architecture is simple. The `plugin` is a function that is passed with the | ||
Formidable instance (the `form` across the README examples) and the options. | ||
**Note:** the plugin function's `this` context is also the same instance. | ||
```js | ||
const formidable = require('formidable'); | ||
const form = formidable({ keepExtensions: true }); | ||
form.use((self, options) => { | ||
// self === this === form | ||
console.log('woohoo, custom plugin'); | ||
// do your stuff; check `src/plugins` for inspiration | ||
}); | ||
form.parse(req, (error, fields, files) => { | ||
console.log('done!'); | ||
}); | ||
``` | ||
**Important to note**, is that inside plugin `this.options`, `self.options` and | ||
`options` MAY or MAY NOT be the same. General best practice is to always use the | ||
`this`, so you can later test your plugin independently and more easily. | ||
If you want to disable some parsing capabilities of Formidable, you can disable | ||
the plugin which corresponds to the parser. For example, if you want to disable | ||
multipart parsing (so the [src/parsers/Multipart.js](./src/parsers/Multipart.js) | ||
which is used in [src/plugins/multipart.js](./src/plugins/multipart.js)), then | ||
you can remove it from the `options.enabledPlugins`, like so | ||
```js | ||
const { Formidable } = require('formidable'); | ||
const form = new Formidable({ | ||
hash: 'sha1', | ||
enabledPlugins: ['octetstream', 'querystring', 'json'], | ||
}); | ||
``` | ||
**Be aware** that the order _MAY_ be important too. The names corresponds 1:1 to | ||
files in [src/plugins/](./src/plugins) folder. | ||
Pull requests for new built-in plugins MAY be accepted - for example, more | ||
advanced querystring parser. Add your plugin as a new file in `src/plugins/` | ||
folder (lowercased) and follow how the other plugins are made. | ||
### form.onPart | ||
@@ -477,2 +673,3 @@ | ||
<td align="center"><a href="https://github.com/dmolim"><img src="https://avatars2.githubusercontent.com/u/7090374?v=4" width="100px;" alt=""/><br /><sub><b>Dmitry Ivonin</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/commits?author=dmolim" title="Documentation">📖</a></td> | ||
<td align="center"><a href="https://audiobox.fm"><img src="https://avatars1.githubusercontent.com/u/12844?v=4" width="100px;" alt=""/><br /><sub><b>Claudio Poli</b></sub></a><br /><a href="https://github.com/node-formidable/node-formidable/commits?author=masterkain" title="Code">💻</a></td> | ||
</tr> | ||
@@ -497,4 +694,2 @@ </table> | ||
[codecov-img]: https://badgen.net/codecov/c/github/node-formidable/node-formidable/master?icon=codecov | ||
[build-img]: https://badgen.net/github/checks/node-formidable/node-formidable?label=build&icon=github | ||
[build-url]: https://github.com/node-formidable/node-formidable/actions?query=workflow%3Anodejs | ||
[npmv-canary-img]: https://badgen.net/npm/v/formidable/canary?icon=npm | ||
@@ -520,3 +715,24 @@ [npmv-dev-img]: https://badgen.net/npm/v/formidable/dev?icon=npm | ||
[npm-yearly-img]: https://badgen.net/npm/dy/formidable?icon=npm&cache=300 | ||
[npm-alltime-img]: https://badgen.net/npm/dt/formidable?icon=npm&cache=300 | ||
[npm-alltime-img]: https://badgen.net/npm/dt/formidable?icon=npm&cache=300&label=total%20downloads | ||
[nodejs-img]: https://badgen.net/badge/node/>=%2010.13/green?cache=300 | ||
[ccommits-url]: https://conventionalcommits.org/ | ||
[ccommits-img]: https://badgen.net/badge/conventional%20commits/v1.0.0/green?cache=300 | ||
[contributing-url]: https://github.com/node-formidable/node-formidable/blob/master/CONTRIBUTING.md | ||
[code_of_conduct-url]: https://github.com/node-formidable/node-formidable/blob/master/CODE_OF_CONDUCT.md | ||
[open-issue-url]: https://github.com/node-formidable/node-formidable/issues/new | ||
[tidelift-url]: https://tidelift.com/subscription/pkg/npm-formidable?utm_source=npm-formidable&utm_medium=referral&utm_campaign=enterprise | ||
[tidelift-img]: https://badgen.net/badge/tidelift/subscription/4B5168?labelColor=F6914D | ||
[kofi-url]: https://ko-fi.com/tunnckoCore/commissions | ||
[kofi-img]: https://badgen.net/badge/ko-fi/support/29abe0c2?cache=300&icon=https://rawcdn.githack.com/tunnckoCore/badgen-icons/f8264c6414e0bec449dd86f2241d50a9b89a1203/icons/kofi.svg | ||
[linux-build-img]: https://badgen-net.charlike.now.sh/github/checks/node-formidable/node-formidable?label=linux%20build&icon=github | ||
[macos-build-img]: https://badgen-net.charlike.now.sh/github/checks/node-formidable/node-formidable?label=macos%20build&icon=github | ||
[windows-build-img]: https://badgen-net.charlike.now.sh/github/checks/node-formidable/node-formidable?label=windows%20build&icon=github | ||
[build-url]: https://github.com/node-formidable/node-formidable/actions?query=workflow%3Anodejs | ||
<!-- prettier-ignore-end --> |
@@ -10,3 +10,4 @@ /* eslint-disable class-methods-use-this */ | ||
const crypto = require('crypto'); | ||
const { Stream } = require('stream'); | ||
const once = require('once'); | ||
const dezalgo = require('dezalgo'); | ||
const { EventEmitter } = require('events'); | ||
@@ -23,12 +24,8 @@ const { StringDecoder } = require('string_decoder'); | ||
multiples: false, | ||
enabledPlugins: ['octetstream', 'querystring', 'multipart', 'json'], | ||
}; | ||
const File = require('./File'); | ||
/** Parsers */ | ||
const JSONParser = require('./parsers/JSON'); | ||
const DummyParser = require('./parsers/Dummy'); | ||
const OctetParser = require('./parsers/OctetStream'); | ||
const MultipartParser = require('./parsers/Multipart'); | ||
const QuerystringParser = require('./parsers/Querystring'); | ||
@@ -45,3 +42,3 @@ function hasOwnProp(obj, key) { | ||
Object.assign(this, DEFAULT_OPTIONS, options); | ||
this.options = { ...DEFAULT_OPTIONS, ...options }; | ||
this.uploadDir = this.uploadDir || os.tmpdir(); | ||
@@ -59,5 +56,30 @@ | ||
this._fileSize = 0; | ||
this._plugins = []; | ||
this.openedFiles = []; | ||
const enabledPlugins = [] | ||
.concat(this.options.enabledPlugins) | ||
.filter(Boolean); | ||
if (enabledPlugins.length === 0) { | ||
throw new Error( | ||
'expect at least 1 enabled builtin plugin, see options.enabledPlugins', | ||
); | ||
} | ||
this.options.enabledPlugins.forEach((pluginName) => { | ||
const plgName = pluginName.toLowerCase(); | ||
// eslint-disable-next-line import/no-dynamic-require, global-require | ||
this.use(require(path.join(__dirname, 'plugins', `${plgName}.js`))); | ||
}); | ||
} | ||
use(plugin) { | ||
if (typeof plugin !== 'function') { | ||
throw new Error('.use: expect `plugin` to be a function'); | ||
} | ||
this._plugins.push(plugin.bind(this)); | ||
return this; | ||
} | ||
parse(req, cb) { | ||
@@ -95,2 +117,3 @@ this.pause = () => { | ||
if (cb) { | ||
const callback = once(dezalgo(cb)); | ||
const fields = {}; | ||
@@ -101,3 +124,3 @@ const files = {}; | ||
// TODO: too much nesting | ||
if (this.multiples && name.slice(-2) === '[]') { | ||
if (this.options.multiples && name.slice(-2) === '[]') { | ||
const realName = name.slice(0, name.length - 2); | ||
@@ -115,10 +138,6 @@ if (hasOwnProp(fields, realName)) { | ||
} | ||
// if (name === 'simple') { | ||
// console.log('fields name!!', name); | ||
// console.log('fields value!!', value); | ||
// } | ||
}); | ||
this.on('file', (name, file) => { | ||
// TODO: too much nesting | ||
if (this.multiples) { | ||
if (this.options.multiples) { | ||
if (hasOwnProp(files, name)) { | ||
@@ -135,13 +154,8 @@ if (!Array.isArray(files[name])) { | ||
} | ||
// console.log('files!!', files); | ||
// if (name === 'simple') { | ||
// console.log('files name!!', name); | ||
// console.log('files value!!', file); | ||
// } | ||
}); | ||
this.on('error', (err) => { | ||
cb(err, fields, files); | ||
callback(err, fields, files); | ||
}); | ||
this.on('end', () => { | ||
cb(null, fields, files); | ||
callback(null, fields, files); | ||
}); | ||
@@ -173,4 +187,6 @@ } | ||
} | ||
this._parser.end(); | ||
if (this._parser) { | ||
this._parser.end(); | ||
} | ||
this._maybeEnd(); | ||
}); | ||
@@ -185,2 +201,8 @@ | ||
this._parseContentType(); | ||
if (!this._parser) { | ||
this._error(new Error('not parser found')); | ||
return; | ||
} | ||
this._parser.once('error', (error) => { | ||
@@ -220,6 +242,6 @@ this._error(error); | ||
// this method can be overwritten by the user | ||
this.handlePart(part); | ||
this._handlePart(part); | ||
} | ||
handlePart(part) { | ||
_handlePart(part) { | ||
if (part.filename && typeof part.filename !== 'string') { | ||
@@ -242,10 +264,12 @@ this._error(new Error(`the part.filename should be string when exists`)); | ||
let value = ''; | ||
const decoder = new StringDecoder(part.transferEncoding || this.encoding); | ||
const decoder = new StringDecoder( | ||
part.transferEncoding || this.options.encoding, | ||
); | ||
part.on('data', (buffer) => { | ||
this._fieldsSize += buffer.length; | ||
if (this._fieldsSize > this.maxFieldsSize) { | ||
if (this._fieldsSize > this.options.maxFieldsSize) { | ||
this._error( | ||
new Error( | ||
`maxFieldsSize exceeded, received ${this._fieldsSize} bytes of field data`, | ||
`options.maxFieldsSize (${this.options.maxFieldsSize} bytes) exceeded, received ${this._fieldsSize} bytes of field data`, | ||
), | ||
@@ -270,3 +294,3 @@ ); | ||
type: part.mime, | ||
hash: this.hash, | ||
hash: this.options.hash, | ||
}); | ||
@@ -283,6 +307,6 @@ file.on('error', (err) => { | ||
this._fileSize += buffer.length; | ||
if (this._fileSize > this.maxFileSize) { | ||
if (this._fileSize > this.options.maxFileSize) { | ||
this._error( | ||
new Error( | ||
`maxFileSize exceeded, received ${this._fileSize} bytes of file data`, | ||
`options.maxFileSize (${this.options.maxFileSize} bytes) exceeded, received ${this._fileSize} bytes of file data`, | ||
), | ||
@@ -313,3 +337,3 @@ ); | ||
if (this.bytesExpected === 0) { | ||
this._parser = new DummyParser(this); | ||
this._parser = new DummyParser(this, this.options); | ||
return; | ||
@@ -323,39 +347,47 @@ } | ||
if (this.headers['content-type'].match(/octet-stream/i)) { | ||
this._initOctetStream(); | ||
return; | ||
} | ||
const results = []; | ||
const _dummyParser = new DummyParser(this, this.options); | ||
if (this.headers['content-type'].match(/urlencoded/i)) { | ||
this._initUrlencoded(); | ||
return; | ||
} | ||
// eslint-disable-next-line no-plusplus | ||
for (let idx = 0; idx < this._plugins.length; idx++) { | ||
const plugin = this._plugins[idx]; | ||
if (this.headers['content-type'].match(/multipart/i)) { | ||
const m = this.headers['content-type'].match( | ||
/boundary=(?:"([^"]+)"|([^;]+))/i, | ||
); | ||
if (m) { | ||
this._initMultipart(m[1] || m[2]); | ||
} else { | ||
this._error( | ||
new Error('bad content-type header, no multipart boundary'), | ||
let pluginReturn = null; | ||
try { | ||
pluginReturn = plugin(this, this.options) || this; | ||
} catch (err) { | ||
// directly throw from the `form.parse` method; | ||
// there is no other better way, except a handle through options | ||
const error = new Error( | ||
`plugin on index ${idx} failed with: ${err.message}`, | ||
); | ||
error.idx = idx; | ||
throw error; | ||
} | ||
return; | ||
} | ||
if (this.headers['content-type'].match(/json/i)) { | ||
this._initJSONencoded(); | ||
return; | ||
Object.assign(this, pluginReturn); | ||
// todo: use Set/Map and pass plugin name instead of the `idx` index | ||
this.emit('plugin', idx, pluginReturn); | ||
results.push(pluginReturn); | ||
} | ||
this._error( | ||
new Error( | ||
`bad content-type header, unknown content-type: ${this.headers['content-type']}`, | ||
), | ||
); | ||
this.emit('pluginsResults', results); | ||
// NOTE: probably not needed, because we check options.enabledPlugins in the constructor | ||
// if (results.length === 0 /* && results.length !== this._plugins.length */) { | ||
// this._error( | ||
// new Error( | ||
// `bad content-type header, unknown content-type: ${this.headers['content-type']}`, | ||
// ), | ||
// ); | ||
// } | ||
} | ||
_error(err) { | ||
_error(err, eventName = 'error') { | ||
// if (!err && this.error) { | ||
// this.emit('error', this.error); | ||
// return; | ||
// } | ||
if (this.error || this.ended) { | ||
@@ -366,3 +398,3 @@ return; | ||
this.error = err; | ||
this.emit('error', err); | ||
this.emit(eventName, err); | ||
@@ -391,133 +423,9 @@ if (Array.isArray(this.openedFiles)) { | ||
_newParser() { | ||
return new MultipartParser(); | ||
return new MultipartParser(this.options); | ||
} | ||
_initMultipart(boundary) { | ||
this.type = 'multipart'; | ||
const parser = new MultipartParser(); | ||
let headerField; | ||
let headerValue; | ||
let part; | ||
parser.initWithBoundary(boundary); | ||
// eslint-disable-next-line max-statements, consistent-return | ||
parser.on('data', ({ name, buffer, start, end }) => { | ||
if (name === 'partBegin') { | ||
part = new Stream(); | ||
part.readable = true; | ||
part.headers = {}; | ||
part.name = null; | ||
part.filename = null; | ||
part.mime = null; | ||
part.transferEncoding = 'binary'; | ||
part.transferBuffer = ''; | ||
headerField = ''; | ||
headerValue = ''; | ||
} else if (name === 'headerField') { | ||
headerField += buffer.toString(this.encoding, start, end); | ||
} else if (name === 'headerValue') { | ||
headerValue += buffer.toString(this.encoding, start, end); | ||
} else if (name === 'headerEnd') { | ||
headerField = headerField.toLowerCase(); | ||
part.headers[headerField] = headerValue; | ||
// matches either a quoted-string or a token (RFC 2616 section 19.5.1) | ||
const m = headerValue.match( | ||
// eslint-disable-next-line no-useless-escape | ||
/\bname=("([^"]*)"|([^\(\)<>@,;:\\"\/\[\]\?=\{\}\s\t/]+))/i, | ||
); | ||
if (headerField === 'content-disposition') { | ||
if (m) { | ||
part.name = m[2] || m[3] || ''; | ||
} | ||
part.filename = this._fileName(headerValue); | ||
} else if (headerField === 'content-type') { | ||
part.mime = headerValue; | ||
} else if (headerField === 'content-transfer-encoding') { | ||
part.transferEncoding = headerValue.toLowerCase(); | ||
} | ||
headerField = ''; | ||
headerValue = ''; | ||
} else if (name === 'headersEnd') { | ||
switch (part.transferEncoding) { | ||
case 'binary': | ||
case '7bit': | ||
case '8bit': { | ||
const dataPropagation = (ctx) => { | ||
if (ctx.name === 'partData') { | ||
part.emit('data', ctx.buffer.slice(ctx.start, ctx.end)); | ||
} | ||
}; | ||
const dataStopPropagation = (ctx) => { | ||
if (ctx.name === 'partEnd') { | ||
part.emit('end'); | ||
parser.off('data', dataPropagation); | ||
parser.off('data', dataStopPropagation); | ||
} | ||
}; | ||
parser.on('data', dataPropagation); | ||
parser.on('data', dataStopPropagation); | ||
break; | ||
} | ||
case 'base64': { | ||
const dataPropagation = (ctx) => { | ||
if (ctx.name === 'partData') { | ||
part.transferBuffer += ctx.buffer | ||
.slice(ctx.start, ctx.end) | ||
.toString('ascii'); | ||
/* | ||
four bytes (chars) in base64 converts to three bytes in binary | ||
encoding. So we should always work with a number of bytes that | ||
can be divided by 4, it will result in a number of buytes that | ||
can be divided vy 3. | ||
*/ | ||
const offset = parseInt(part.transferBuffer.length / 4, 10) * 4; | ||
part.emit( | ||
'data', | ||
Buffer.from( | ||
part.transferBuffer.substring(0, offset), | ||
'base64', | ||
), | ||
); | ||
part.transferBuffer = part.transferBuffer.substring(offset); | ||
} | ||
}; | ||
const dataStopPropagation = (ctx) => { | ||
if (ctx.name === 'partEnd') { | ||
part.emit('data', Buffer.from(part.transferBuffer, 'base64')); | ||
part.emit('end'); | ||
parser.off('data', dataPropagation); | ||
parser.off('data', dataStopPropagation); | ||
} | ||
}; | ||
parser.on('data', dataPropagation); | ||
parser.on('data', dataStopPropagation); | ||
break; | ||
} | ||
default: | ||
return this._error(new Error('unknown transfer-encoding')); | ||
} | ||
this.onPart(part); | ||
} else if (name === 'end') { | ||
this.ended = true; | ||
this._maybeEnd(); | ||
} | ||
}); | ||
this._parser = parser; | ||
} | ||
_fileName(headerValue) { | ||
_getFileName(headerValue) { | ||
// matches either a quoted-string or a token (RFC 2616 section 19.5.1) | ||
const m = headerValue.match( | ||
// eslint-disable-next-line no-useless-escape | ||
/\bfilename=("(.*?)"|([^\(\)<>@,;:\\"\/\[\]\?=\{\}\s\t/]+))($|;\s)/i, | ||
/\bfilename=("(.*?)"|([^()<>{}[\]@,;:"?=\s/\t]+))($|;\s)/i, | ||
); | ||
@@ -535,94 +443,2 @@ if (!m) return null; | ||
_initUrlencoded() { | ||
this.type = 'urlencoded'; | ||
const parser = new QuerystringParser(this.maxFields); | ||
parser.on('data', ({ key, value }) => { | ||
this.emit('field', key, value); | ||
}); | ||
parser.once('end', () => { | ||
this.ended = true; | ||
this._maybeEnd(); | ||
}); | ||
this._parser = parser; | ||
} | ||
_initOctetStream() { | ||
this.type = 'octet-stream'; | ||
const filename = this.headers['x-file-name']; | ||
const mime = this.headers['content-type']; | ||
const file = new File({ | ||
path: this._uploadPath(filename), | ||
name: filename, | ||
type: mime, | ||
}); | ||
file.on('error', (err) => { | ||
this._error(err); | ||
}); | ||
this.emit('fileBegin', filename, file); | ||
file.open(); | ||
this.openedFiles.push(file); | ||
this._flushing += 1; | ||
this._parser = new OctetParser(); | ||
// Keep track of writes that haven't finished so we don't emit the file before it's done being written | ||
let outstandingWrites = 0; | ||
this._parser.on('data', (buffer) => { | ||
this.pause(); | ||
outstandingWrites += 1; | ||
file.write(buffer, () => { | ||
outstandingWrites -= 1; | ||
this.resume(); | ||
if (this.ended) { | ||
this._parser.emit('doneWritingFile'); | ||
} | ||
}); | ||
}); | ||
this._parser.on('end', () => { | ||
this._flushing -= 1; | ||
this.ended = true; | ||
const done = () => { | ||
file.end(() => { | ||
this.emit('file', 'file', file); | ||
this._maybeEnd(); | ||
}); | ||
}; | ||
if (outstandingWrites === 0) { | ||
done(); | ||
} else { | ||
this._parser.once('doneWritingFile', done); | ||
} | ||
}); | ||
} | ||
_initJSONencoded() { | ||
this.type = 'json'; | ||
const parser = new JSONParser(); | ||
parser.on('data', ({ key, value }) => { | ||
this.emit('field', key, value); | ||
}); | ||
parser.once('end', () => { | ||
this.ended = true; | ||
this._maybeEnd(); | ||
}); | ||
this._parser = parser; | ||
} | ||
_uploadPath(filename) { | ||
@@ -632,3 +448,3 @@ const buf = crypto.randomBytes(16); | ||
if (this.keepExtensions) { | ||
if (this.options.keepExtensions) { | ||
let ext = path.extname(filename); | ||
@@ -655,2 +471,3 @@ ext = ext.replace(/(\.[a-z0-9]+).*/i, '$1'); | ||
IncomingForm.DEFAULT_OPTIONS = DEFAULT_OPTIONS; | ||
module.exports = IncomingForm; |
@@ -6,7 +6,4 @@ 'use strict'; | ||
const JSONParser = require('./parsers/JSON'); | ||
const DummyParser = require('./parsers/Dummy'); | ||
const MultipartParser = require('./parsers/Multipart'); | ||
const OctetStreamParser = require('./parsers/OctetStream'); | ||
const QuerystringParser = require('./parsers/Querystring'); | ||
const plugins = require('./plugins/index'); | ||
const parsers = require('./parsers/index'); | ||
@@ -26,11 +23,13 @@ // make it available without requiring the `new` keyword | ||
// parsers | ||
JSONParser, | ||
DummyParser, | ||
MultipartParser, | ||
OctetStreamParser, | ||
QuerystringParser, | ||
...parsers, | ||
parsers, | ||
// typo aliases | ||
OctetstreamParser: OctetStreamParser, | ||
QueryStringParser: QuerystringParser, | ||
// misc | ||
defaultOptions: Formidable.DEFAULT_OPTIONS, | ||
enabledPlugins: Formidable.DEFAULT_OPTIONS.enabledPlugins, | ||
// plugins | ||
plugins: { | ||
...plugins, | ||
}, | ||
}); |
@@ -8,4 +8,5 @@ /* eslint-disable no-underscore-dangle */ | ||
class DummyParser extends Transform { | ||
constructor(incomingForm) { | ||
constructor(incomingForm, options = {}) { | ||
super(); | ||
this.globalOptions = { ...options }; | ||
this.incomingForm = incomingForm; | ||
@@ -12,0 +13,0 @@ } |
@@ -8,5 +8,6 @@ /* eslint-disable no-underscore-dangle */ | ||
class JSONParser extends Transform { | ||
constructor() { | ||
constructor(options = {}) { | ||
super({ readableObjectMode: true }); | ||
this.chunks = []; | ||
this.globalOptions = { ...options }; | ||
} | ||
@@ -13,0 +14,0 @@ |
@@ -49,3 +49,3 @@ /* eslint-disable no-fallthrough */ | ||
class MultipartParser extends Transform { | ||
constructor() { | ||
constructor(options = {}) { | ||
super({ readableObjectMode: true }); | ||
@@ -58,2 +58,3 @@ this.boundary = null; | ||
this.globalOptions = { ...options }; | ||
this.index = null; | ||
@@ -60,0 +61,0 @@ this.flags = 0; |
@@ -5,4 +5,9 @@ 'use strict'; | ||
class OctetStreamParser extends PassThrough {} | ||
class OctetStreamParser extends PassThrough { | ||
constructor(options = {}) { | ||
super(); | ||
this.globalOptions = { ...options }; | ||
} | ||
} | ||
module.exports = OctetStreamParser; |
@@ -11,5 +11,6 @@ /* eslint-disable no-underscore-dangle */ | ||
class QuerystringParser extends Transform { | ||
constructor(maxKeys) { | ||
constructor(options = {}) { | ||
super({ readableObjectMode: true }); | ||
this.maxKeys = maxKeys; | ||
this.globalOptions = { ...options }; | ||
this.maxKeys = this.globalOptions.maxFields; | ||
this.buffer = ''; | ||
@@ -16,0 +17,0 @@ this.bufferLength = 0; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
2081534
71
2123
729
2
22
6
12
+ Addeddezalgo@^1.0.3
+ Addedonce@^1.4.0
+ Addedasap@2.0.6(transitive)
+ Addeddezalgo@1.0.4(transitive)
+ Addedonce@1.4.0(transitive)
+ Addedwrappy@1.0.2(transitive)