├── test ├── tmp │ └── .empty ├── fixture │ ├── file │ │ ├── plain.txt │ │ └── funkyfilename.txt │ ├── multi_video.upload │ ├── js │ │ ├── no-filename.js │ │ └── special-chars-in-filename.js │ ├── http │ │ ├── special-chars-in-filename │ │ │ ├── info.md │ │ │ ├── xp-ie-7.http │ │ │ ├── xp-ie-8.http │ │ │ ├── xp-safari-5.http │ │ │ ├── osx-safari-5.http │ │ │ ├── xp-chrome-12.http │ │ │ ├── osx-firefox-3.6.http │ │ │ └── osx-chrome-13.http │ │ └── no-filename │ │ │ └── generic.http │ └── multipart.js ├── run.js ├── common.js ├── legacy │ ├── common.js │ ├── simple │ │ ├── test-querystring-parser.js │ │ ├── test-multipart-parser.js │ │ ├── test-file.js │ │ └── test-incoming-form.js │ ├── integration │ │ └── test-multipart-parser.js │ └── system │ │ └── test-multi-video-upload.js ├── unit │ └── test-incoming-form.js └── integration │ └── test-fixtures.js ├── index.js ├── .npmignore ├── .gitignore ├── .travis.yml ├── lib ├── index.js ├── util.js ├── querystring_parser.js ├── file.js ├── multipart_parser.js └── incoming_form.js ├── TODO ├── Makefile ├── package.json ├── example ├── post.js └── upload.js ├── tool └── record.js ├── benchmark └── bench-multipart-parser.js └── Readme.md /test/tmp/.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/formidable'); -------------------------------------------------------------------------------- /test/fixture/file/plain.txt: -------------------------------------------------------------------------------- 1 | I am a plain text file 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test/tmp/ 2 | *.upload 3 | *.un~ 4 | *.http 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /test/tmp 2 | *.upload 3 | *.un~ 4 | /node_modules 5 | -------------------------------------------------------------------------------- /test/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('urun')(__dirname) 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.4 4 | - 0.6 5 | -------------------------------------------------------------------------------- /test/fixture/file/funkyfilename.txt: -------------------------------------------------------------------------------- 1 | I am a text file with a funky name! 2 | -------------------------------------------------------------------------------- /test/fixture/multi_video.upload: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/node-formidable/master/test/fixture/multi_video.upload -------------------------------------------------------------------------------- /test/fixture/js/no-filename.js: -------------------------------------------------------------------------------- 1 | module.exports['generic.http'] = [ 2 | {type: 'file', name: 'upload', filename: '', fixture: 'plain.txt'}, 3 | ]; 4 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var IncomingForm = require('./incoming_form').IncomingForm; 2 | IncomingForm.IncomingForm = IncomingForm; 3 | module.exports = IncomingForm; 4 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | // Backwards compatibility ... 2 | try { 3 | module.exports = require('util'); 4 | } catch (e) { 5 | module.exports = require('sys'); 6 | } 7 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - Better bufferMaxSize handling approach 2 | - Add tests for JSON parser pull request and merge it 3 | - Implement QuerystringParser the same way as MultipartParser 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | test: 4 | @./test/run.js 5 | 6 | build: npm test 7 | 8 | npm: 9 | npm install . 10 | 11 | clean: 12 | rm test/tmp/* 13 | 14 | .PHONY: test clean build 15 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/info.md: -------------------------------------------------------------------------------- 1 | * Opera does not allow submitting this file, it shows a warning to the 2 | user that the file could not be found instead. Tested in 9.8, 11.51 on OSX. 3 | Reported to Opera on 08.09.2011 (tracking email DSK-346009@bugs.opera.com). 4 | -------------------------------------------------------------------------------- /test/fixture/http/no-filename/generic.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytyE4wkKlZ5CQJVTG 4 | Content-Length: 1000 5 | 6 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG 7 | Content-Disposition: form-data; name="upload"; filename="" 8 | Content-Type: text/plain 9 | 10 | I am a plain text file 11 | 12 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG-- 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "formidable", 3 | "version": "1.0.8", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "gently": "0.8.0", 7 | "findit": "0.1.1", 8 | "hashish": "0.0.4", 9 | "urun": "0.0.4", 10 | "utest": "0.0.3" 11 | }, 12 | "directories": { 13 | "lib": "./lib" 14 | }, 15 | "main": "./lib/index", 16 | "scripts": { 17 | "test": "make test" 18 | }, 19 | "engines": { 20 | "node": "*" 21 | } 22 | } -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | var mysql = require('..'); 2 | var path = require('path'); 3 | 4 | var root = path.join(__dirname, '../'); 5 | exports.dir = { 6 | root : root, 7 | lib : root + '/lib', 8 | fixture : root + '/test/fixture', 9 | tmp : root + '/test/tmp', 10 | }; 11 | 12 | exports.port = 13532; 13 | 14 | exports.formidable = require('..'); 15 | exports.assert = require('assert'); 16 | 17 | exports.require = function(lib) { 18 | return require(exports.dir.lib + '/' + lib); 19 | }; 20 | -------------------------------------------------------------------------------- /lib/querystring_parser.js: -------------------------------------------------------------------------------- 1 | if (global.GENTLY) require = GENTLY.hijack(require); 2 | 3 | // This is a buffering parser, not quite as nice as the multipart one. 4 | // If I find time I'll rewrite this to be fully streaming as well 5 | var querystring = require('querystring'); 6 | 7 | function QuerystringParser() { 8 | this.buffer = ''; 9 | }; 10 | exports.QuerystringParser = QuerystringParser; 11 | 12 | QuerystringParser.prototype.write = function(buffer) { 13 | this.buffer += buffer.toString('ascii'); 14 | return buffer.length; 15 | }; 16 | 17 | QuerystringParser.prototype.end = function() { 18 | var fields = querystring.parse(this.buffer); 19 | for (var field in fields) { 20 | this.onField(field, fields[field]); 21 | } 22 | this.buffer = ''; 23 | 24 | this.onEnd(); 25 | }; -------------------------------------------------------------------------------- /test/legacy/common.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | fs = require('fs'); 3 | 4 | try { 5 | global.Gently = require('gently'); 6 | } catch (e) { 7 | throw new Error('this test suite requires node-gently'); 8 | } 9 | 10 | exports.lib = path.join(__dirname, '../../lib'); 11 | 12 | global.GENTLY = new Gently(); 13 | 14 | global.assert = require('assert'); 15 | global.TEST_PORT = 13532; 16 | global.TEST_FIXTURES = path.join(__dirname, '../fixture'); 17 | global.TEST_TMP = path.join(__dirname, '../tmp'); 18 | 19 | // Stupid new feature in node that complains about gently attaching too many 20 | // listeners to process 'exit'. This is a workaround until I can think of a 21 | // better way to deal with this. 22 | if (process.setMaxListeners) { 23 | process.setMaxListeners(10000); 24 | } 25 | -------------------------------------------------------------------------------- /test/fixture/js/special-chars-in-filename.js: -------------------------------------------------------------------------------- 1 | var properFilename = 'funkyfilename.txt'; 2 | 3 | function expect(filename) { 4 | return [ 5 | {type: 'field', name: 'title', value: 'Weird filename'}, 6 | {type: 'file', name: 'upload', filename: filename, fixture: properFilename}, 7 | ]; 8 | }; 9 | 10 | var webkit = " ? % * | \" < > . ? ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt"; 11 | var ffOrIe = " ? % * | \" < > . ☃ ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt"; 12 | 13 | module.exports = { 14 | 'osx-chrome-13.http' : expect(webkit), 15 | 'osx-firefox-3.6.http' : expect(ffOrIe), 16 | 'osx-safari-5.http' : expect(webkit), 17 | 'xp-chrome-12.http' : expect(webkit), 18 | 'xp-ie-7.http' : expect(ffOrIe), 19 | 'xp-ie-8.http' : expect(ffOrIe), 20 | 'xp-safari-5.http' : expect(webkit), 21 | }; 22 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/xp-ie-7.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, */* 3 | Referer: http://192.168.56.1:8080/ 4 | Accept-Language: de 5 | User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1) 6 | Content-Type: multipart/form-data; boundary=---------------------------7db1fe232017c 7 | Accept-Encoding: gzip, deflate 8 | Host: 192.168.56.1:8080 9 | Content-Length: 368 10 | Connection: Keep-Alive 11 | Cache-Control: no-cache 12 | 13 | -----------------------------7db1fe232017c 14 | Content-Disposition: form-data; name="title" 15 | 16 | Weird filename 17 | -----------------------------7db1fe232017c 18 | Content-Disposition: form-data; name="upload"; filename=" ? % * | " < > . ☃ ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 19 | Content-Type: application/octet-stream 20 | 21 | 22 | -----------------------------7db1fe232017c-- 23 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/xp-ie-8.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, */* 3 | Referer: http://192.168.56.1:8080/ 4 | Accept-Language: de 5 | User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0) 6 | Content-Type: multipart/form-data; boundary=---------------------------7db3a8372017c 7 | Accept-Encoding: gzip, deflate 8 | Host: 192.168.56.1:8080 9 | Content-Length: 368 10 | Connection: Keep-Alive 11 | Cache-Control: no-cache 12 | 13 | -----------------------------7db3a8372017c 14 | Content-Disposition: form-data; name="title" 15 | 16 | Weird filename 17 | -----------------------------7db3a8372017c 18 | Content-Disposition: form-data; name="upload"; filename=" ? % * | " < > . ☃ ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 19 | Content-Type: application/octet-stream 20 | 21 | 22 | -----------------------------7db3a8372017c-- 23 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/xp-safari-5.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: 192.168.56.1:8080 3 | Referer: http://192.168.56.1:8080/ 4 | Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 5 | Accept-Language: en-US 6 | Origin: http://192.168.56.1:8080 7 | User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 8 | Accept-Encoding: gzip, deflate 9 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundarykmaWSUbu697WN9TM 10 | Content-Length: 344 11 | Connection: keep-alive 12 | 13 | ------WebKitFormBoundarykmaWSUbu697WN9TM 14 | Content-Disposition: form-data; name="title" 15 | 16 | Weird filename 17 | ------WebKitFormBoundarykmaWSUbu697WN9TM 18 | Content-Disposition: form-data; name="upload"; filename=" ? % * | %22 < > . ? ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 19 | Content-Type: text/plain 20 | 21 | 22 | ------WebKitFormBoundarykmaWSUbu697WN9TM-- 23 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/osx-safari-5.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Origin: http://localhost:8080 4 | Content-Length: 383 5 | User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1 6 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQJZ1gvhvdgfisJPJ 7 | Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5 8 | Referer: http://localhost:8080/ 9 | Accept-Language: en-us 10 | Accept-Encoding: gzip, deflate 11 | Connection: keep-alive 12 | 13 | ------WebKitFormBoundaryQJZ1gvhvdgfisJPJ 14 | Content-Disposition: form-data; name="title" 15 | 16 | Weird filename 17 | ------WebKitFormBoundaryQJZ1gvhvdgfisJPJ 18 | Content-Disposition: form-data; name="upload"; filename=": \ ? % * | %22 < > . ? ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 19 | Content-Type: text/plain 20 | 21 | I am a text file with a funky name! 22 | 23 | ------WebKitFormBoundaryQJZ1gvhvdgfisJPJ-- 24 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/xp-chrome-12.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: 192.168.56.1:8080 3 | Connection: keep-alive 4 | Referer: http://192.168.56.1:8080/ 5 | Content-Length: 344 6 | Cache-Control: max-age=0 7 | Origin: http://192.168.56.1:8080 8 | User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.122 Safari/534.30 9 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryEvqBNplR3ByrwQPa 10 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 11 | Accept-Encoding: gzip,deflate,sdch 12 | Accept-Language: de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4 13 | Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3 14 | 15 | ------WebKitFormBoundaryEvqBNplR3ByrwQPa 16 | Content-Disposition: form-data; name="title" 17 | 18 | Weird filename 19 | ------WebKitFormBoundaryEvqBNplR3ByrwQPa 20 | Content-Disposition: form-data; name="upload"; filename=" ? % * | %22 < > . ? ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 21 | Content-Type: text/plain 22 | 23 | 24 | ------WebKitFormBoundaryEvqBNplR3ByrwQPa-- 25 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/osx-firefox-3.6.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.22) Gecko/20110902 Firefox/3.6.22 4 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 5 | Accept-Language: en-us,en;q=0.5 6 | Accept-Encoding: gzip,deflate 7 | Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 8 | Keep-Alive: 115 9 | Connection: keep-alive 10 | Referer: http://localhost:8080/ 11 | Content-Type: multipart/form-data; boundary=---------------------------9849436581144108930470211272 12 | Content-Length: 438 13 | 14 | -----------------------------9849436581144108930470211272 15 | Content-Disposition: form-data; name="title" 16 | 17 | Weird filename 18 | -----------------------------9849436581144108930470211272 19 | Content-Disposition: form-data; name="upload"; filename=": \ ? % * | " < > . ☃ ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 20 | Content-Type: text/plain 21 | 22 | I am a text file with a funky name! 23 | 24 | -----------------------------9849436581144108930470211272-- 25 | -------------------------------------------------------------------------------- /test/fixture/http/special-chars-in-filename/osx-chrome-13.http: -------------------------------------------------------------------------------- 1 | POST /upload HTTP/1.1 2 | Host: localhost:8080 3 | Connection: keep-alive 4 | Referer: http://localhost:8080/ 5 | Content-Length: 383 6 | Cache-Control: max-age=0 7 | Origin: http://localhost:8080 8 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1 9 | Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytyE4wkKlZ5CQJVTG 10 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 11 | Accept-Encoding: gzip,deflate,sdch 12 | Accept-Language: en-US,en;q=0.8 13 | Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3 14 | Cookie: jqCookieJar_tablesorter=%7B%22showListTable%22%3A%5B%5B5%2C1%5D%2C%5B1%2C0%5D%5D%7D 15 | 16 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG 17 | Content-Disposition: form-data; name="title" 18 | 19 | Weird filename 20 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG 21 | Content-Disposition: form-data; name="upload"; filename=": \ ? % * | %22 < > . ? ; ' @ # $ ^ & ( ) - _ = + { } [ ] ` ~.txt" 22 | Content-Type: text/plain 23 | 24 | I am a text file with a funky name! 25 | 26 | ------WebKitFormBoundarytyE4wkKlZ5CQJVTG-- 27 | -------------------------------------------------------------------------------- /test/legacy/simple/test-querystring-parser.js: -------------------------------------------------------------------------------- 1 | var common = require('../common'); 2 | var QuerystringParser = require(common.lib + '/querystring_parser').QuerystringParser, 3 | Buffer = require('buffer').Buffer, 4 | gently, 5 | parser; 6 | 7 | function test(test) { 8 | gently = new Gently(); 9 | parser = new QuerystringParser(); 10 | test(); 11 | gently.verify(test.name); 12 | } 13 | 14 | test(function constructor() { 15 | assert.equal(parser.buffer, ''); 16 | assert.equal(parser.constructor.name, 'QuerystringParser'); 17 | }); 18 | 19 | test(function write() { 20 | var a = new Buffer('a=1'); 21 | assert.equal(parser.write(a), a.length); 22 | 23 | var b = new Buffer('&b=2'); 24 | parser.write(b); 25 | assert.equal(parser.buffer, a + b); 26 | }); 27 | 28 | test(function end() { 29 | var FIELDS = {a: ['b', {c: 'd'}], e: 'f'}; 30 | 31 | gently.expect(GENTLY.hijacked.querystring, 'parse', function(str) { 32 | assert.equal(str, parser.buffer); 33 | return FIELDS; 34 | }); 35 | 36 | gently.expect(parser, 'onField', Object.keys(FIELDS).length, function(key, val) { 37 | assert.deepEqual(FIELDS[key], val); 38 | }); 39 | 40 | gently.expect(parser, 'onEnd'); 41 | 42 | parser.buffer = 'my buffer'; 43 | parser.end(); 44 | assert.equal(parser.buffer, ''); 45 | }); 46 | -------------------------------------------------------------------------------- /example/post.js: -------------------------------------------------------------------------------- 1 | require('../test/common'); 2 | var http = require('http'), 3 | util = require('util'), 4 | formidable = require('formidable'), 5 | server; 6 | 7 | server = http.createServer(function(req, res) { 8 | if (req.url == '/') { 9 | res.writeHead(200, {'content-type': 'text/html'}); 10 | res.end( 11 | '
' 16 | ); 17 | } else if (req.url == '/post') { 18 | var form = new formidable.IncomingForm(), 19 | fields = []; 20 | 21 | form 22 | .on('error', function(err) { 23 | res.writeHead(200, {'content-type': 'text/plain'}); 24 | res.end('error:\n\n'+util.inspect(err)); 25 | }) 26 | .on('field', function(field, value) { 27 | console.log(field, value); 28 | fields.push([field, value]); 29 | }) 30 | .on('end', function() { 31 | console.log('-> post done'); 32 | res.writeHead(200, {'content-type': 'text/plain'}); 33 | res.end('received fields:\n\n '+util.inspect(fields)); 34 | }); 35 | form.parse(req); 36 | } else { 37 | res.writeHead(404, {'content-type': 'text/plain'}); 38 | res.end('404'); 39 | } 40 | }); 41 | server.listen(TEST_PORT); 42 | 43 | console.log('listening on http://localhost:'+TEST_PORT+'/'); 44 | -------------------------------------------------------------------------------- /tool/record.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var fs = require('fs'); 3 | var connections = 0; 4 | 5 | var server = http.createServer(function(req, res) { 6 | var socket = req.socket; 7 | console.log('Request: %s %s -> %s', req.method, req.url, socket.filename); 8 | 9 | req.on('end', function() { 10 | if (req.url !== '/') { 11 | res.end(JSON.stringify({ 12 | method: req.method, 13 | url: req.url, 14 | filename: socket.filename, 15 | })); 16 | return; 17 | } 18 | 19 | res.writeHead(200, {'content-type': 'text/html'}); 20 | res.end( 21 | '' 26 | ); 27 | }); 28 | }); 29 | 30 | server.on('connection', function(socket) { 31 | connections++; 32 | 33 | socket.id = connections; 34 | socket.filename = 'connection-' + socket.id + '.http'; 35 | socket.file = fs.createWriteStream(socket.filename); 36 | socket.pipe(socket.file); 37 | 38 | console.log('--> %s', socket.filename); 39 | socket.on('close', function() { 40 | console.log('<-- %s', socket.filename); 41 | }); 42 | }); 43 | 44 | var port = process.env.PORT || 8080; 45 | server.listen(port, function() { 46 | console.log('Recording connections on port %s', port); 47 | }); 48 | -------------------------------------------------------------------------------- /lib/file.js: -------------------------------------------------------------------------------- 1 | if (global.GENTLY) require = GENTLY.hijack(require); 2 | 3 | var util = require('./util'), 4 | WriteStream = require('fs').WriteStream, 5 | EventEmitter = require('events').EventEmitter; 6 | 7 | function File(properties) { 8 | EventEmitter.call(this); 9 | 10 | this.size = 0; 11 | this.path = null; 12 | this.name = null; 13 | this.type = null; 14 | this.lastModifiedDate = null; 15 | 16 | this._writeStream = null; 17 | 18 | for (var key in properties) { 19 | this[key] = properties[key]; 20 | } 21 | 22 | this._backwardsCompatibility(); 23 | } 24 | module.exports = File; 25 | util.inherits(File, EventEmitter); 26 | 27 | // @todo Next release: Show error messages when accessing these 28 | File.prototype._backwardsCompatibility = function() { 29 | var self = this; 30 | this.__defineGetter__('length', function() { 31 | return self.size; 32 | }); 33 | this.__defineGetter__('filename', function() { 34 | return self.name; 35 | }); 36 | this.__defineGetter__('mime', function() { 37 | return self.type; 38 | }); 39 | }; 40 | 41 | File.prototype.open = function() { 42 | this._writeStream = new WriteStream(this.path); 43 | }; 44 | 45 | File.prototype.write = function(buffer, cb) { 46 | var self = this; 47 | this._writeStream.write(buffer, function() { 48 | self.lastModifiedDate = new Date(); 49 | self.size += buffer.length; 50 | self.emit('progress', self.size); 51 | cb(); 52 | }); 53 | }; 54 | 55 | File.prototype.end = function(cb) { 56 | var self = this; 57 | this._writeStream.end(function() { 58 | self.emit('end'); 59 | cb(); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /example/upload.js: -------------------------------------------------------------------------------- 1 | require('../test/common'); 2 | var http = require('http'), 3 | util = require('util'), 4 | formidable = require('formidable'), 5 | server; 6 | 7 | server = http.createServer(function(req, res) { 8 | if (req.url == '/') { 9 | res.writeHead(200, {'content-type': 'text/html'}); 10 | res.end( 11 | '' 16 | ); 17 | } else if (req.url == '/upload') { 18 | var form = new formidable.IncomingForm(), 19 | files = [], 20 | fields = []; 21 | 22 | form.uploadDir = TEST_TMP; 23 | 24 | form 25 | .on('field', function(field, value) { 26 | console.log(field, value); 27 | fields.push([field, value]); 28 | }) 29 | .on('file', function(field, file) { 30 | console.log(field, file); 31 | files.push([field, file]); 32 | }) 33 | .on('end', function() { 34 | console.log('-> upload done'); 35 | res.writeHead(200, {'content-type': 'text/plain'}); 36 | res.write('received fields:\n\n '+util.inspect(fields)); 37 | res.write('\n\n'); 38 | res.end('received files:\n\n '+util.inspect(files)); 39 | }); 40 | form.parse(req); 41 | } else { 42 | res.writeHead(404, {'content-type': 'text/plain'}); 43 | res.end('404'); 44 | } 45 | }); 46 | server.listen(TEST_PORT); 47 | 48 | console.log('listening on http://localhost:'+TEST_PORT+'/'); 49 | -------------------------------------------------------------------------------- /test/legacy/simple/test-multipart-parser.js: -------------------------------------------------------------------------------- 1 | var common = require('../common'); 2 | var multipartParser = require(common.lib + '/multipart_parser'), 3 | MultipartParser = multipartParser.MultipartParser, 4 | events = require('events'), 5 | Buffer = require('buffer').Buffer, 6 | parser; 7 | 8 | function test(test) { 9 | parser = new MultipartParser(); 10 | test(); 11 | } 12 | 13 | test(function constructor() { 14 | assert.equal(parser.boundary, null); 15 | assert.equal(parser.state, 0); 16 | assert.equal(parser.flags, 0); 17 | assert.equal(parser.boundaryChars, null); 18 | assert.equal(parser.index, null); 19 | assert.equal(parser.lookbehind, null); 20 | assert.equal(parser.constructor.name, 'MultipartParser'); 21 | }); 22 | 23 | test(function initWithBoundary() { 24 | var boundary = 'abc'; 25 | parser.initWithBoundary(boundary); 26 | assert.deepEqual(Array.prototype.slice.call(parser.boundary), [13, 10, 45, 45, 97, 98, 99]); 27 | assert.equal(parser.state, multipartParser.START); 28 | 29 | assert.deepEqual(parser.boundaryChars, {10: true, 13: true, 45: true, 97: true, 98: true, 99: true}); 30 | }); 31 | 32 | test(function parserError() { 33 | var boundary = 'abc', 34 | buffer = new Buffer(5); 35 | 36 | parser.initWithBoundary(boundary); 37 | buffer.write('--ad', 'ascii', 0); 38 | assert.equal(parser.write(buffer), 3); 39 | }); 40 | 41 | test(function end() { 42 | (function testError() { 43 | assert.equal(parser.end().message, 'MultipartParser.end(): stream ended unexpectedly: ' + parser.explain()); 44 | })(); 45 | 46 | (function testRegular() { 47 | parser.state = multipartParser.END; 48 | assert.strictEqual(parser.end(), undefined); 49 | })(); 50 | }); 51 | -------------------------------------------------------------------------------- /benchmark/bench-multipart-parser.js: -------------------------------------------------------------------------------- 1 | require('../test/common'); 2 | var multipartParser = require('../lib/multipart_parser'), 3 | MultipartParser = multipartParser.MultipartParser, 4 | parser = new MultipartParser(), 5 | Buffer = require('buffer').Buffer, 6 | boundary = '-----------------------------168072824752491622650073', 7 | mb = 100, 8 | buffer = createMultipartBuffer(boundary, mb * 1024 * 1024), 9 | callbacks = 10 | { partBegin: -1, 11 | partEnd: -1, 12 | headerField: -1, 13 | headerValue: -1, 14 | partData: -1, 15 | end: -1, 16 | }; 17 | 18 | 19 | parser.initWithBoundary(boundary); 20 | parser.onHeaderField = function() { 21 | callbacks.headerField++; 22 | }; 23 | 24 | parser.onHeaderValue = function() { 25 | callbacks.headerValue++; 26 | }; 27 | 28 | parser.onPartBegin = function() { 29 | callbacks.partBegin++; 30 | }; 31 | 32 | parser.onPartData = function() { 33 | callbacks.partData++; 34 | }; 35 | 36 | parser.onPartEnd = function() { 37 | callbacks.partEnd++; 38 | }; 39 | 40 | parser.onEnd = function() { 41 | callbacks.end++; 42 | }; 43 | 44 | var start = +new Date(), 45 | nparsed = parser.write(buffer), 46 | duration = +new Date - start, 47 | mbPerSec = (mb / (duration / 1000)).toFixed(2); 48 | 49 | console.log(mbPerSec+' mb/sec'); 50 | 51 | assert.equal(nparsed, buffer.length); 52 | 53 | function createMultipartBuffer(boundary, size) { 54 | var head = 55 | '--'+boundary+'\r\n' 56 | + 'content-disposition: form-data; name="field1"\r\n' 57 | + '\r\n' 58 | , tail = '\r\n--'+boundary+'--\r\n' 59 | , buffer = new Buffer(size); 60 | 61 | buffer.write(head, 'ascii', 0); 62 | buffer.write(tail, 'ascii', buffer.length - tail.length); 63 | return buffer; 64 | } 65 | 66 | process.on('exit', function() { 67 | for (var k in callbacks) { 68 | assert.equal(0, callbacks[k], k+' count off by '+callbacks[k]); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /test/unit/test-incoming-form.js: -------------------------------------------------------------------------------- 1 | var common = require('../common'); 2 | var test = require('utest'); 3 | var assert = common.assert; 4 | var IncomingForm = common.require('incoming_form').IncomingForm; 5 | var path = require('path'); 6 | 7 | var from; 8 | test('IncomingForm', { 9 | before: function() { 10 | form = new IncomingForm(); 11 | }, 12 | 13 | '#_fileName with regular characters': function() { 14 | var filename = 'foo.txt'; 15 | assert.equal(form._fileName(makeHeader(filename)), 'foo.txt'); 16 | }, 17 | 18 | '#_fileName with unescaped quote': function() { 19 | var filename = 'my".txt'; 20 | assert.equal(form._fileName(makeHeader(filename)), 'my".txt'); 21 | }, 22 | 23 | '#_fileName with escaped quote': function() { 24 | var filename = 'my%22.txt'; 25 | assert.equal(form._fileName(makeHeader(filename)), 'my".txt'); 26 | }, 27 | 28 | '#_fileName with bad quote and additional sub-header': function() { 29 | var filename = 'my".txt'; 30 | var header = makeHeader(filename) + '; foo="bar"'; 31 | assert.equal(form._fileName(header), filename); 32 | }, 33 | 34 | '#_fileName with semicolon': function() { 35 | var filename = 'my;.txt'; 36 | assert.equal(form._fileName(makeHeader(filename)), 'my;.txt'); 37 | }, 38 | 39 | '#_fileName with utf8 character': function() { 40 | var filename = 'my☃.txt'; 41 | assert.equal(form._fileName(makeHeader(filename)), 'my☃.txt'); 42 | }, 43 | 44 | '#_uploadPath strips harmful characters from extension when keepExtensions': function() { 45 | form.keepExtensions = true; 46 | 47 | var ext = path.extname(form._uploadPath('fine.jpg?foo=bar')); 48 | assert.equal(ext, '.jpg'); 49 | 50 | var ext = path.extname(form._uploadPath('fine?foo=bar')); 51 | assert.equal(ext, ''); 52 | 53 | var ext = path.extname(form._uploadPath('super.cr2+dsad')); 54 | assert.equal(ext, '.cr2'); 55 | 56 | var ext = path.extname(form._uploadPath('super.bar')); 57 | assert.equal(ext, '.bar'); 58 | }, 59 | }); 60 | 61 | function makeHeader(filename) { 62 | return 'Content-Disposition: form-data; name="upload"; filename="' + filename + '"'; 63 | } 64 | -------------------------------------------------------------------------------- /test/fixture/multipart.js: -------------------------------------------------------------------------------- 1 | exports['rfc1867'] = 2 | { boundary: 'AaB03x', 3 | raw: 4 | '--AaB03x\r\n'+ 5 | 'content-disposition: form-data; name="field1"\r\n'+ 6 | '\r\n'+ 7 | 'Joe Blow\r\nalmost tricked you!\r\n'+ 8 | '--AaB03x\r\n'+ 9 | 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n'+ 10 | 'Content-Type: text/plain\r\n'+ 11 | '\r\n'+ 12 | '... contents of file1.txt ...\r\r\n'+ 13 | '--AaB03x--\r\n', 14 | parts: 15 | [ { headers: { 16 | 'content-disposition': 'form-data; name="field1"', 17 | }, 18 | data: 'Joe Blow\r\nalmost tricked you!', 19 | }, 20 | { headers: { 21 | 'content-disposition': 'form-data; name="pics"; filename="file1.txt"', 22 | 'Content-Type': 'text/plain', 23 | }, 24 | data: '... contents of file1.txt ...\r', 25 | } 26 | ] 27 | }; 28 | 29 | exports['noTrailing\r\n'] = 30 | { boundary: 'AaB03x', 31 | raw: 32 | '--AaB03x\r\n'+ 33 | 'content-disposition: form-data; name="field1"\r\n'+ 34 | '\r\n'+ 35 | 'Joe Blow\r\nalmost tricked you!\r\n'+ 36 | '--AaB03x\r\n'+ 37 | 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n'+ 38 | 'Content-Type: text/plain\r\n'+ 39 | '\r\n'+ 40 | '... contents of file1.txt ...\r\r\n'+ 41 | '--AaB03x--', 42 | parts: 43 | [ { headers: { 44 | 'content-disposition': 'form-data; name="field1"', 45 | }, 46 | data: 'Joe Blow\r\nalmost tricked you!', 47 | }, 48 | { headers: { 49 | 'content-disposition': 'form-data; name="pics"; filename="file1.txt"', 50 | 'Content-Type': 'text/plain', 51 | }, 52 | data: '... contents of file1.txt ...\r', 53 | } 54 | ] 55 | }; 56 | 57 | exports['emptyHeader'] = 58 | { boundary: 'AaB03x', 59 | raw: 60 | '--AaB03x\r\n'+ 61 | 'content-disposition: form-data; name="field1"\r\n'+ 62 | ': foo\r\n'+ 63 | '\r\n'+ 64 | 'Joe Blow\r\nalmost tricked you!\r\n'+ 65 | '--AaB03x\r\n'+ 66 | 'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n'+ 67 | 'Content-Type: text/plain\r\n'+ 68 | '\r\n'+ 69 | '... contents of file1.txt ...\r\r\n'+ 70 | '--AaB03x--\r\n', 71 | expectError: true, 72 | }; 73 | -------------------------------------------------------------------------------- /test/legacy/integration/test-multipart-parser.js: -------------------------------------------------------------------------------- 1 | var common = require('../common'); 2 | var CHUNK_LENGTH = 10, 3 | multipartParser = require(common.lib + '/multipart_parser'), 4 | MultipartParser = multipartParser.MultipartParser, 5 | parser = new MultipartParser(), 6 | fixtures = require(TEST_FIXTURES + '/multipart'), 7 | Buffer = require('buffer').Buffer; 8 | 9 | Object.keys(fixtures).forEach(function(name) { 10 | var fixture = fixtures[name], 11 | buffer = new Buffer(Buffer.byteLength(fixture.raw, 'binary')), 12 | offset = 0, 13 | chunk, 14 | nparsed, 15 | 16 | parts = [], 17 | part = null, 18 | headerField, 19 | headerValue, 20 | endCalled = ''; 21 | 22 | parser.initWithBoundary(fixture.boundary); 23 | parser.onPartBegin = function() { 24 | part = {headers: {}, data: ''}; 25 | parts.push(part); 26 | headerField = ''; 27 | headerValue = ''; 28 | }; 29 | 30 | parser.onHeaderField = function(b, start, end) { 31 | headerField += b.toString('ascii', start, end); 32 | }; 33 | 34 | parser.onHeaderValue = function(b, start, end) { 35 | headerValue += b.toString('ascii', start, end); 36 | } 37 | 38 | parser.onHeaderEnd = function() { 39 | part.headers[headerField] = headerValue; 40 | headerField = ''; 41 | headerValue = ''; 42 | }; 43 | 44 | parser.onPartData = function(b, start, end) { 45 | var str = b.toString('ascii', start, end); 46 | part.data += b.slice(start, end); 47 | } 48 | 49 | parser.onEnd = function() { 50 | endCalled = true; 51 | } 52 | 53 | buffer.write(fixture.raw, 'binary', 0); 54 | 55 | while (offset < buffer.length) { 56 | if (offset + CHUNK_LENGTH < buffer.length) { 57 | chunk = buffer.slice(offset, offset+CHUNK_LENGTH); 58 | } else { 59 | chunk = buffer.slice(offset, buffer.length); 60 | } 61 | offset = offset + CHUNK_LENGTH; 62 | 63 | nparsed = parser.write(chunk); 64 | if (nparsed != chunk.length) { 65 | if (fixture.expectError) { 66 | return; 67 | } 68 | puts('-- ERROR --'); 69 | p(chunk.toString('ascii')); 70 | throw new Error(chunk.length+' bytes written, but only '+nparsed+' bytes parsed!'); 71 | } 72 | } 73 | 74 | if (fixture.expectError) { 75 | throw new Error('expected parse error did not happen'); 76 | } 77 | 78 | assert.ok(endCalled); 79 | assert.deepEqual(parts, fixture.parts); 80 | }); 81 | -------------------------------------------------------------------------------- /test/legacy/system/test-multi-video-upload.js: -------------------------------------------------------------------------------- 1 | var common = require('../common'); 2 | var BOUNDARY = '---------------------------10102754414578508781458777923', 3 | FIXTURE = TEST_FIXTURES+'/multi_video.upload', 4 | fs = require('fs'), 5 | util = require(common.lib + '/util'), 6 | http = require('http'), 7 | formidable = require(common.lib + '/index'), 8 | server = http.createServer(); 9 | 10 | server.on('request', function(req, res) { 11 | var form = new formidable.IncomingForm(), 12 | uploads = {}; 13 | 14 | form.uploadDir = TEST_TMP; 15 | form.parse(req); 16 | 17 | form 18 | .on('fileBegin', function(field, file) { 19 | assert.equal(field, 'upload'); 20 | 21 | var tracker = {file: file, progress: [], ended: false}; 22 | uploads[file.filename] = tracker; 23 | file 24 | .on('progress', function(bytesReceived) { 25 | tracker.progress.push(bytesReceived); 26 | assert.equal(bytesReceived, file.length); 27 | }) 28 | .on('end', function() { 29 | tracker.ended = true; 30 | }); 31 | }) 32 | .on('field', function(field, value) { 33 | assert.equal(field, 'title'); 34 | assert.equal(value, ''); 35 | }) 36 | .on('file', function(field, file) { 37 | assert.equal(field, 'upload'); 38 | assert.strictEqual(uploads[file.filename].file, file); 39 | }) 40 | .on('end', function() { 41 | assert.ok(uploads['shortest_video.flv']); 42 | assert.ok(uploads['shortest_video.flv'].ended); 43 | assert.ok(uploads['shortest_video.flv'].progress.length > 3); 44 | assert.equal(uploads['shortest_video.flv'].progress.slice(-1), uploads['shortest_video.flv'].file.length); 45 | assert.ok(uploads['shortest_video.mp4']); 46 | assert.ok(uploads['shortest_video.mp4'].ended); 47 | assert.ok(uploads['shortest_video.mp4'].progress.length > 3); 48 | 49 | server.close(); 50 | res.writeHead(200); 51 | res.end('good'); 52 | }); 53 | }); 54 | 55 | server.listen(TEST_PORT, function() { 56 | var client = http.createClient(TEST_PORT), 57 | stat = fs.statSync(FIXTURE), 58 | headers = { 59 | 'content-type': 'multipart/form-data; boundary='+BOUNDARY, 60 | 'content-length': stat.size, 61 | } 62 | request = client.request('POST', '/', headers), 63 | fixture = new fs.ReadStream(FIXTURE); 64 | 65 | fixture 66 | .on('data', function(b) { 67 | request.write(b); 68 | }) 69 | .on('end', function() { 70 | request.end(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/integration/test-fixtures.js: -------------------------------------------------------------------------------- 1 | var hashish = require('hashish'); 2 | var fs = require('fs'); 3 | var findit = require('findit'); 4 | var path = require('path'); 5 | var http = require('http'); 6 | var net = require('net'); 7 | var assert = require('assert'); 8 | 9 | var common = require('../common'); 10 | var formidable = common.formidable; 11 | 12 | var server = http.createServer(); 13 | server.listen(common.port, findFixtures); 14 | 15 | function findFixtures() { 16 | var fixtures = []; 17 | findit 18 | .sync(common.dir.fixture + '/js') 19 | .forEach(function(jsPath) { 20 | if (!/\.js$/.test(jsPath)) return; 21 | 22 | var group = path.basename(jsPath, '.js'); 23 | hashish.forEach(require(jsPath), function(fixture, name) { 24 | fixtures.push({ 25 | name : group + '/' + name, 26 | fixture : fixture, 27 | }); 28 | }); 29 | }); 30 | 31 | testNext(fixtures); 32 | } 33 | 34 | function testNext(fixtures) { 35 | var fixture = fixtures.shift(); 36 | if (!fixture) return server.close(); 37 | 38 | var name = fixture.name; 39 | var fixture = fixture.fixture; 40 | 41 | uploadFixture(name, function(err, parts) { 42 | if (err) throw err; 43 | 44 | fixture.forEach(function(expectedPart, i) { 45 | var parsedPart = parts[i]; 46 | assert.equal(parsedPart.type, expectedPart.type); 47 | assert.equal(parsedPart.name, expectedPart.name); 48 | 49 | if (parsedPart.type === 'file') { 50 | var filename = parsedPart.value.name; 51 | assert.equal(filename, expectedPart.filename); 52 | } 53 | }); 54 | 55 | testNext(fixtures); 56 | }); 57 | }; 58 | 59 | function uploadFixture(name, cb) { 60 | server.once('request', function(req, res) { 61 | var form = new formidable.IncomingForm(); 62 | form.uploadDir = common.dir.tmp; 63 | form.parse(req); 64 | 65 | function callback() { 66 | var realCallback = cb; 67 | cb = function() {}; 68 | realCallback.apply(null, arguments); 69 | } 70 | 71 | var parts = []; 72 | form 73 | .on('error', callback) 74 | .on('fileBegin', function(name, value) { 75 | parts.push({type: 'file', name: name, value: value}); 76 | }) 77 | .on('field', function(name, value) { 78 | parts.push({type: 'field', name: name, value: value}); 79 | }) 80 | .on('end', function() { 81 | callback(null, parts); 82 | }); 83 | }); 84 | 85 | var socket = net.createConnection(common.port); 86 | var file = fs.createReadStream(common.dir.fixture + '/http/' + name); 87 | 88 | file.pipe(socket); 89 | } 90 | -------------------------------------------------------------------------------- /test/legacy/simple/test-file.js: -------------------------------------------------------------------------------- 1 | var common = require('../common'); 2 | var WriteStreamStub = GENTLY.stub('fs', 'WriteStream'); 3 | 4 | var File = require(common.lib + '/file'), 5 | EventEmitter = require('events').EventEmitter, 6 | file, 7 | gently; 8 | 9 | function test(test) { 10 | gently = new Gently(); 11 | file = new File(); 12 | test(); 13 | gently.verify(test.name); 14 | } 15 | 16 | test(function constructor() { 17 | assert.ok(file instanceof EventEmitter); 18 | assert.strictEqual(file.size, 0); 19 | assert.strictEqual(file.path, null); 20 | assert.strictEqual(file.name, null); 21 | assert.strictEqual(file.type, null); 22 | assert.strictEqual(file.lastModifiedDate, null); 23 | 24 | assert.strictEqual(file._writeStream, null); 25 | 26 | (function testSetProperties() { 27 | var file2 = new File({foo: 'bar'}); 28 | assert.equal(file2.foo, 'bar'); 29 | })(); 30 | }); 31 | 32 | test(function open() { 33 | var WRITE_STREAM; 34 | file.path = '/foo'; 35 | 36 | gently.expect(WriteStreamStub, 'new', function (path) { 37 | WRITE_STREAM = this; 38 | assert.strictEqual(path, file.path); 39 | }); 40 | 41 | file.open(); 42 | assert.strictEqual(file._writeStream, WRITE_STREAM); 43 | }); 44 | 45 | test(function write() { 46 | var BUFFER = {length: 10}, 47 | CB_STUB, 48 | CB = function() { 49 | CB_STUB.apply(this, arguments); 50 | }; 51 | 52 | file._writeStream = {}; 53 | 54 | gently.expect(file._writeStream, 'write', function (buffer, cb) { 55 | assert.strictEqual(buffer, BUFFER); 56 | 57 | gently.expect(file, 'emit', function (event, bytesWritten) { 58 | assert.ok(file.lastModifiedDate instanceof Date); 59 | assert.equal(event, 'progress'); 60 | assert.equal(bytesWritten, file.size); 61 | }); 62 | 63 | CB_STUB = gently.expect(function writeCb() { 64 | assert.equal(file.size, 10); 65 | }); 66 | 67 | cb(); 68 | 69 | gently.expect(file, 'emit', function (event, bytesWritten) { 70 | assert.equal(event, 'progress'); 71 | assert.equal(bytesWritten, file.size); 72 | }); 73 | 74 | CB_STUB = gently.expect(function writeCb() { 75 | assert.equal(file.size, 20); 76 | }); 77 | 78 | cb(); 79 | }); 80 | 81 | file.write(BUFFER, CB); 82 | }); 83 | 84 | test(function end() { 85 | var CB_STUB, 86 | CB = function() { 87 | CB_STUB.apply(this, arguments); 88 | }; 89 | 90 | file._writeStream = {}; 91 | 92 | gently.expect(file._writeStream, 'end', function (cb) { 93 | gently.expect(file, 'emit', function (event) { 94 | assert.equal(event, 'end'); 95 | }); 96 | 97 | CB_STUB = gently.expect(function endCb() { 98 | }); 99 | 100 | cb(); 101 | }); 102 | 103 | file.end(CB); 104 | }); 105 | -------------------------------------------------------------------------------- /lib/multipart_parser.js: -------------------------------------------------------------------------------- 1 | var Buffer = require('buffer').Buffer, 2 | s = 0, 3 | S = 4 | { PARSER_UNINITIALIZED: s++, 5 | START: s++, 6 | START_BOUNDARY: s++, 7 | HEADER_FIELD_START: s++, 8 | HEADER_FIELD: s++, 9 | HEADER_VALUE_START: s++, 10 | HEADER_VALUE: s++, 11 | HEADER_VALUE_ALMOST_DONE: s++, 12 | HEADERS_ALMOST_DONE: s++, 13 | PART_DATA_START: s++, 14 | PART_DATA: s++, 15 | PART_END: s++, 16 | END: s++, 17 | }, 18 | 19 | f = 1, 20 | F = 21 | { PART_BOUNDARY: f, 22 | LAST_BOUNDARY: f *= 2, 23 | }, 24 | 25 | LF = 10, 26 | CR = 13, 27 | SPACE = 32, 28 | HYPHEN = 45, 29 | COLON = 58, 30 | A = 97, 31 | Z = 122, 32 | 33 | lower = function(c) { 34 | return c | 0x20; 35 | }; 36 | 37 | for (var s in S) { 38 | exports[s] = S[s]; 39 | } 40 | 41 | function MultipartParser() { 42 | this.boundary = null; 43 | this.boundaryChars = null; 44 | this.lookbehind = null; 45 | this.state = S.PARSER_UNINITIALIZED; 46 | 47 | this.index = null; 48 | this.flags = 0; 49 | }; 50 | exports.MultipartParser = MultipartParser; 51 | 52 | MultipartParser.stateToString = function(stateNumber) { 53 | for (var state in S) { 54 | var number = S[state]; 55 | if (number === stateNumber) return state; 56 | } 57 | }; 58 | 59 | MultipartParser.prototype.initWithBoundary = function(str) { 60 | this.boundary = new Buffer(str.length+4); 61 | this.boundary.write('\r\n--', 'ascii', 0); 62 | this.boundary.write(str, 'ascii', 4); 63 | this.lookbehind = new Buffer(this.boundary.length+8); 64 | this.state = S.START; 65 | 66 | this.boundaryChars = {}; 67 | for (var i = 0; i < this.boundary.length; i++) { 68 | this.boundaryChars[this.boundary[i]] = true; 69 | } 70 | }; 71 | 72 | MultipartParser.prototype.write = function(buffer) { 73 | var self = this, 74 | i = 0, 75 | len = buffer.length, 76 | prevIndex = this.index, 77 | index = this.index, 78 | state = this.state, 79 | flags = this.flags, 80 | lookbehind = this.lookbehind, 81 | boundary = this.boundary, 82 | boundaryChars = this.boundaryChars, 83 | boundaryLength = this.boundary.length, 84 | boundaryEnd = boundaryLength - 1, 85 | bufferLength = buffer.length, 86 | c, 87 | cl, 88 | 89 | mark = function(name) { 90 | self[name+'Mark'] = i; 91 | }, 92 | clear = function(name) { 93 | delete self[name+'Mark']; 94 | }, 95 | callback = function(name, buffer, start, end) { 96 | if (start !== undefined && start === end) { 97 | return; 98 | } 99 | 100 | var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1); 101 | if (callbackSymbol in self) { 102 | self[callbackSymbol](buffer, start, end); 103 | } 104 | }, 105 | dataCallback = function(name, clear) { 106 | var markSymbol = name+'Mark'; 107 | if (!(markSymbol in self)) { 108 | return; 109 | } 110 | 111 | if (!clear) { 112 | callback(name, buffer, self[markSymbol], buffer.length); 113 | self[markSymbol] = 0; 114 | } else { 115 | callback(name, buffer, self[markSymbol], i); 116 | delete self[markSymbol]; 117 | } 118 | }; 119 | 120 | for (i = 0; i < len; i++) { 121 | c = buffer[i]; 122 | switch (state) { 123 | case S.PARSER_UNINITIALIZED: 124 | return i; 125 | case S.START: 126 | index = 0; 127 | state = S.START_BOUNDARY; 128 | case S.START_BOUNDARY: 129 | if (index == boundary.length - 2) { 130 | if (c != CR) { 131 | return i; 132 | } 133 | index++; 134 | break; 135 | } else if (index - 1 == boundary.length - 2) { 136 | if (c != LF) { 137 | return i; 138 | } 139 | index = 0; 140 | callback('partBegin'); 141 | state = S.HEADER_FIELD_START; 142 | break; 143 | } 144 | 145 | if (c != boundary[index+2]) { 146 | return i; 147 | } 148 | index++; 149 | break; 150 | case S.HEADER_FIELD_START: 151 | state = S.HEADER_FIELD; 152 | mark('headerField'); 153 | index = 0; 154 | case S.HEADER_FIELD: 155 | if (c == CR) { 156 | clear('headerField'); 157 | state = S.HEADERS_ALMOST_DONE; 158 | break; 159 | } 160 | 161 | index++; 162 | if (c == HYPHEN) { 163 | break; 164 | } 165 | 166 | if (c == COLON) { 167 | if (index == 1) { 168 | // empty header field 169 | return i; 170 | } 171 | dataCallback('headerField', true); 172 | state = S.HEADER_VALUE_START; 173 | break; 174 | } 175 | 176 | cl = lower(c); 177 | if (cl < A || cl > Z) { 178 | return i; 179 | } 180 | break; 181 | case S.HEADER_VALUE_START: 182 | if (c == SPACE) { 183 | break; 184 | } 185 | 186 | mark('headerValue'); 187 | state = S.HEADER_VALUE; 188 | case S.HEADER_VALUE: 189 | if (c == CR) { 190 | dataCallback('headerValue', true); 191 | callback('headerEnd'); 192 | state = S.HEADER_VALUE_ALMOST_DONE; 193 | } 194 | break; 195 | case S.HEADER_VALUE_ALMOST_DONE: 196 | if (c != LF) { 197 | return i; 198 | } 199 | state = S.HEADER_FIELD_START; 200 | break; 201 | case S.HEADERS_ALMOST_DONE: 202 | if (c != LF) { 203 | return i; 204 | } 205 | 206 | callback('headersEnd'); 207 | state = S.PART_DATA_START; 208 | break; 209 | case S.PART_DATA_START: 210 | state = S.PART_DATA 211 | mark('partData'); 212 | case S.PART_DATA: 213 | prevIndex = index; 214 | 215 | if (index == 0) { 216 | // boyer-moore derrived algorithm to safely skip non-boundary data 217 | i += boundaryEnd; 218 | while (i < bufferLength && !(buffer[i] in boundaryChars)) { 219 | i += boundaryLength; 220 | } 221 | i -= boundaryEnd; 222 | c = buffer[i]; 223 | } 224 | 225 | if (index < boundary.length) { 226 | if (boundary[index] == c) { 227 | if (index == 0) { 228 | dataCallback('partData', true); 229 | } 230 | index++; 231 | } else { 232 | index = 0; 233 | } 234 | } else if (index == boundary.length) { 235 | index++; 236 | if (c == CR) { 237 | // CR = part boundary 238 | flags |= F.PART_BOUNDARY; 239 | } else if (c == HYPHEN) { 240 | // HYPHEN = end boundary 241 | flags |= F.LAST_BOUNDARY; 242 | } else { 243 | index = 0; 244 | } 245 | } else if (index - 1 == boundary.length) { 246 | if (flags & F.PART_BOUNDARY) { 247 | index = 0; 248 | if (c == LF) { 249 | // unset the PART_BOUNDARY flag 250 | flags &= ~F.PART_BOUNDARY; 251 | callback('partEnd'); 252 | callback('partBegin'); 253 | state = S.HEADER_FIELD_START; 254 | break; 255 | } 256 | } else if (flags & F.LAST_BOUNDARY) { 257 | if (c == HYPHEN) { 258 | callback('partEnd'); 259 | callback('end'); 260 | state = S.END; 261 | } else { 262 | index = 0; 263 | } 264 | } else { 265 | index = 0; 266 | } 267 | } 268 | 269 | if (index > 0) { 270 | // when matching a possible boundary, keep a lookbehind reference 271 | // in case it turns out to be a false lead 272 | lookbehind[index-1] = c; 273 | } else if (prevIndex > 0) { 274 | // if our boundary turned out to be rubbish, the captured lookbehind 275 | // belongs to partData 276 | callback('partData', lookbehind, 0, prevIndex); 277 | prevIndex = 0; 278 | mark('partData'); 279 | 280 | // reconsider the current character even so it interrupted the sequence 281 | // it could be the beginning of a new sequence 282 | i--; 283 | } 284 | 285 | break; 286 | case S.END: 287 | break; 288 | default: 289 | return i; 290 | } 291 | } 292 | 293 | dataCallback('headerField'); 294 | dataCallback('headerValue'); 295 | dataCallback('partData'); 296 | 297 | this.index = index; 298 | this.state = state; 299 | this.flags = flags; 300 | 301 | return len; 302 | }; 303 | 304 | MultipartParser.prototype.end = function() { 305 | if (this.state != S.END) { 306 | return new Error('MultipartParser.end(): stream ended unexpectedly: ' + this.explain()); 307 | } 308 | }; 309 | 310 | MultipartParser.prototype.explain = function() { 311 | return 'state = ' + MultipartParser.stateToString(this.state); 312 | }; 313 | -------------------------------------------------------------------------------- /lib/incoming_form.js: -------------------------------------------------------------------------------- 1 | if (global.GENTLY) require = GENTLY.hijack(require); 2 | 3 | var fs = require('fs'); 4 | var util = require('./util'), 5 | path = require('path'), 6 | File = require('./file'), 7 | MultipartParser = require('./multipart_parser').MultipartParser, 8 | QuerystringParser = require('./querystring_parser').QuerystringParser, 9 | StringDecoder = require('string_decoder').StringDecoder, 10 | EventEmitter = require('events').EventEmitter; 11 | 12 | function IncomingForm() { 13 | if (!(this instanceof IncomingForm)) return new IncomingForm; 14 | EventEmitter.call(this); 15 | 16 | this.error = null; 17 | this.ended = false; 18 | 19 | this.maxFieldsSize = 2 * 1024 * 1024; 20 | this.keepExtensions = false; 21 | this.uploadDir = IncomingForm.UPLOAD_DIR; 22 | this.encoding = 'utf-8'; 23 | this.headers = null; 24 | this.type = null; 25 | 26 | this.bytesReceived = null; 27 | this.bytesExpected = null; 28 | 29 | this._parser = null; 30 | this._flushing = 0; 31 | this._fieldsSize = 0; 32 | }; 33 | util.inherits(IncomingForm, EventEmitter); 34 | exports.IncomingForm = IncomingForm; 35 | 36 | IncomingForm.UPLOAD_DIR = (function() { 37 | var dirs = [process.env.TMP, '/tmp', process.cwd()]; 38 | for (var i = 0; i < dirs.length; i++) { 39 | var dir = dirs[i]; 40 | var isDirectory = false; 41 | 42 | try { 43 | isDirectory = fs.statSync(dir).isDirectory(); 44 | } catch (e) {} 45 | 46 | if (isDirectory) return dir; 47 | } 48 | })(); 49 | 50 | IncomingForm.prototype.parse = function(req, cb) { 51 | this.pause = function() { 52 | try { 53 | req.pause(); 54 | } catch (err) { 55 | // the stream was destroyed 56 | if (!this.ended) { 57 | // before it was completed, crash & burn 58 | this._error(err); 59 | } 60 | return false; 61 | } 62 | return true; 63 | }; 64 | 65 | this.resume = function() { 66 | try { 67 | req.resume(); 68 | } catch (err) { 69 | // the stream was destroyed 70 | if (!this.ended) { 71 | // before it was completed, crash & burn 72 | this._error(err); 73 | } 74 | return false; 75 | } 76 | 77 | return true; 78 | }; 79 | 80 | this.writeHeaders(req.headers); 81 | 82 | var self = this; 83 | req 84 | .on('error', function(err) { 85 | self._error(err); 86 | }) 87 | .on('aborted', function() { 88 | self.emit('aborted'); 89 | }) 90 | .on('data', function(buffer) { 91 | self.write(buffer); 92 | }) 93 | .on('end', function() { 94 | if (self.error) { 95 | return; 96 | } 97 | 98 | var err = self._parser.end(); 99 | if (err) { 100 | self._error(err); 101 | } 102 | }); 103 | 104 | if (cb) { 105 | var fields = {}, files = {}; 106 | this 107 | .on('field', function(name, value) { 108 | fields[name] = value; 109 | }) 110 | .on('file', function(name, file) { 111 | files[name] = file; 112 | }) 113 | .on('error', function(err) { 114 | cb(err, fields, files); 115 | }) 116 | .on('end', function() { 117 | cb(null, fields, files); 118 | }); 119 | } 120 | 121 | return this; 122 | }; 123 | 124 | IncomingForm.prototype.writeHeaders = function(headers) { 125 | this.headers = headers; 126 | this._parseContentLength(); 127 | this._parseContentType(); 128 | }; 129 | 130 | IncomingForm.prototype.write = function(buffer) { 131 | if (!this._parser) { 132 | this._error(new Error('unintialized parser')); 133 | return; 134 | } 135 | 136 | this.bytesReceived += buffer.length; 137 | this.emit('progress', this.bytesReceived, this.bytesExpected); 138 | 139 | var bytesParsed = this._parser.write(buffer); 140 | if (bytesParsed !== buffer.length) { 141 | this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed')); 142 | } 143 | 144 | return bytesParsed; 145 | }; 146 | 147 | IncomingForm.prototype.pause = function() { 148 | // this does nothing, unless overwritten in IncomingForm.parse 149 | return false; 150 | }; 151 | 152 | IncomingForm.prototype.resume = function() { 153 | // this does nothing, unless overwritten in IncomingForm.parse 154 | return false; 155 | }; 156 | 157 | IncomingForm.prototype.onPart = function(part) { 158 | // this method can be overwritten by the user 159 | this.handlePart(part); 160 | }; 161 | 162 | IncomingForm.prototype.handlePart = function(part) { 163 | var self = this; 164 | 165 | if (part.filename === undefined) { 166 | var value = '' 167 | , decoder = new StringDecoder(this.encoding); 168 | 169 | part.on('data', function(buffer) { 170 | self._fieldsSize += buffer.length; 171 | if (self._fieldsSize > self.maxFieldsSize) { 172 | self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data')); 173 | return; 174 | } 175 | value += decoder.write(buffer); 176 | }); 177 | 178 | part.on('end', function() { 179 | self.emit('field', part.name, value); 180 | }); 181 | return; 182 | } 183 | 184 | this._flushing++; 185 | 186 | var file = new File({ 187 | path: this._uploadPath(part.filename), 188 | name: part.filename, 189 | type: part.mime, 190 | }); 191 | 192 | this.emit('fileBegin', part.name, file); 193 | 194 | file.open(); 195 | 196 | part.on('data', function(buffer) { 197 | self.pause(); 198 | file.write(buffer, function() { 199 | self.resume(); 200 | }); 201 | }); 202 | 203 | part.on('end', function() { 204 | file.end(function() { 205 | self._flushing--; 206 | self.emit('file', part.name, file); 207 | self._maybeEnd(); 208 | }); 209 | }); 210 | }; 211 | 212 | IncomingForm.prototype._parseContentType = function() { 213 | if (!this.headers['content-type']) { 214 | this._error(new Error('bad content-type header, no content-type')); 215 | return; 216 | } 217 | 218 | if (this.headers['content-type'].match(/urlencoded/i)) { 219 | this._initUrlencoded(); 220 | return; 221 | } 222 | 223 | if (this.headers['content-type'].match(/multipart/i)) { 224 | var m; 225 | if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) { 226 | this._initMultipart(m[1] || m[2]); 227 | } else { 228 | this._error(new Error('bad content-type header, no multipart boundary')); 229 | } 230 | return; 231 | } 232 | 233 | this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type'])); 234 | }; 235 | 236 | IncomingForm.prototype._error = function(err) { 237 | if (this.error) { 238 | return; 239 | } 240 | 241 | this.error = err; 242 | this.pause(); 243 | this.emit('error', err); 244 | }; 245 | 246 | IncomingForm.prototype._parseContentLength = function() { 247 | if (this.headers['content-length']) { 248 | this.bytesReceived = 0; 249 | this.bytesExpected = parseInt(this.headers['content-length'], 10); 250 | } 251 | }; 252 | 253 | IncomingForm.prototype._newParser = function() { 254 | return new MultipartParser(); 255 | }; 256 | 257 | IncomingForm.prototype._initMultipart = function(boundary) { 258 | this.type = 'multipart'; 259 | 260 | var parser = new MultipartParser(), 261 | self = this, 262 | headerField, 263 | headerValue, 264 | part; 265 | 266 | parser.initWithBoundary(boundary); 267 | 268 | parser.onPartBegin = function() { 269 | part = new EventEmitter(); 270 | part.headers = {}; 271 | part.name = null; 272 | part.filename = null; 273 | part.mime = null; 274 | headerField = ''; 275 | headerValue = ''; 276 | }; 277 | 278 | parser.onHeaderField = function(b, start, end) { 279 | headerField += b.toString(self.encoding, start, end); 280 | }; 281 | 282 | parser.onHeaderValue = function(b, start, end) { 283 | headerValue += b.toString(self.encoding, start, end); 284 | }; 285 | 286 | parser.onHeaderEnd = function() { 287 | headerField = headerField.toLowerCase(); 288 | part.headers[headerField] = headerValue; 289 | 290 | var m; 291 | if (headerField == 'content-disposition') { 292 | if (m = headerValue.match(/name="([^"]+)"/i)) { 293 | part.name = m[1]; 294 | } 295 | 296 | part.filename = self._fileName(headerValue); 297 | } else if (headerField == 'content-type') { 298 | part.mime = headerValue; 299 | } 300 | 301 | headerField = ''; 302 | headerValue = ''; 303 | }; 304 | 305 | parser.onHeadersEnd = function() { 306 | self.onPart(part); 307 | }; 308 | 309 | parser.onPartData = function(b, start, end) { 310 | part.emit('data', b.slice(start, end)); 311 | }; 312 | 313 | parser.onPartEnd = function() { 314 | part.emit('end'); 315 | }; 316 | 317 | parser.onEnd = function() { 318 | self.ended = true; 319 | self._maybeEnd(); 320 | }; 321 | 322 | this._parser = parser; 323 | }; 324 | 325 | IncomingForm.prototype._fileName = function(headerValue) { 326 | var m = headerValue.match(/filename="(.*?)"($|; )/i) 327 | if (!m) return; 328 | 329 | var filename = m[1].substr(m[1].lastIndexOf('\\') + 1); 330 | filename = filename.replace(/%22/g, '"'); 331 | filename = filename.replace(/([\d]{4});/g, function(m, code) { 332 | return String.fromCharCode(code); 333 | }); 334 | return filename; 335 | }; 336 | 337 | IncomingForm.prototype._initUrlencoded = function() { 338 | this.type = 'urlencoded'; 339 | 340 | var parser = new QuerystringParser() 341 | , self = this; 342 | 343 | parser.onField = function(key, val) { 344 | self.emit('field', key, val); 345 | }; 346 | 347 | parser.onEnd = function() { 348 | self.ended = true; 349 | self._maybeEnd(); 350 | }; 351 | 352 | this._parser = parser; 353 | }; 354 | 355 | IncomingForm.prototype._uploadPath = function(filename) { 356 | var name = ''; 357 | for (var i = 0; i < 32; i++) { 358 | name += Math.floor(Math.random() * 16).toString(16); 359 | } 360 | 361 | if (this.keepExtensions) { 362 | var ext = path.extname(filename); 363 | ext = ext.replace(/(\.[a-z0-9]+).*/, '$1') 364 | 365 | name += ext; 366 | } 367 | 368 | return path.join(this.uploadDir, name); 369 | }; 370 | 371 | IncomingForm.prototype._maybeEnd = function() { 372 | if (!this.ended || this._flushing) { 373 | return; 374 | } 375 | 376 | this.emit('end'); 377 | }; 378 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Formidable 2 | 3 | [](http://travis-ci.org/felixge/node-formidable) 4 | 5 | ## Purpose 6 | 7 | A node.js module for parsing form data, especially file uploads. 8 | 9 | ## Current status 10 | 11 | This module was developed for [Transloadit](http://transloadit.com/), a service focused on uploading 12 | and encoding images and videos. It has been battle-tested against hundreds of GB of file uploads from 13 | a large variety of clients and is considered production-ready. 14 | 15 | ## Features 16 | 17 | * Fast (~500mb/sec), non-buffering multipart parser 18 | * Automatically writing file uploads to disk 19 | * Low memory footprint 20 | * Graceful error handling 21 | * Very high test coverage 22 | 23 | ## Changelog 24 | 25 | ### v1.0.8 26 | 27 | * Strip potentially unsafe characters when using `keepExtensions: true`. 28 | * Switch to utest / urun for testing 29 | * Add travis build 30 | 31 | ### v1.0.7 32 | 33 | * Remove file from package that was causing problems when installing on windows. (#102) 34 | * Fix typos in Readme (Jason Davies). 35 | 36 | ### v1.0.6 37 | 38 | * Do not default to the default to the field name for file uploads where 39 | filename="". 40 | 41 | ### v1.0.5 42 | 43 | * Support filename="" in multipart parts 44 | * Explain unexpected end() errors in parser better 45 | 46 | **Note:** Starting with this version, formidable emits 'file' events for empty 47 | file input fields. Previously those were incorrectly emitted as regular file 48 | input fields with value = "". 49 | 50 | ### v1.0.4 51 | 52 | * Detect a good default tmp directory regardless of platform. (#88) 53 | 54 | ### v1.0.3 55 | 56 | * Fix problems with utf8 characters (#84) / semicolons in filenames (#58) 57 | * Small performance improvements 58 | * New test suite and fixture system 59 | 60 | ### v1.0.2 61 | 62 | * Exclude node\_modules folder from git 63 | * Implement new `'aborted'` event 64 | * Fix files in example folder to work with recent node versions 65 | * Make gently a devDependency 66 | 67 | [See Commits](https://github.com/felixge/node-formidable/compare/v1.0.1...v1.0.2) 68 | 69 | ### v1.0.1 70 | 71 | * Fix package.json to refer to proper main directory. (#68, Dean Landolt) 72 | 73 | [See Commits](https://github.com/felixge/node-formidable/compare/v1.0.0...v1.0.1) 74 | 75 | ### v1.0.0 76 | 77 | * Add support for multipart boundaries that are quoted strings. (Jeff Craig) 78 | 79 | This marks the beginning of development on version 2.0 which will include 80 | several architectural improvements. 81 | 82 | [See Commits](https://github.com/felixge/node-formidable/compare/v0.9.11...v1.0.0) 83 | 84 | ### v0.9.11 85 | 86 | * Emit `'progress'` event when receiving data, regardless of parsing it. (Tim Koschützki) 87 | * Use [W3C FileAPI Draft](http://dev.w3.org/2006/webapi/FileAPI/) properties for File class 88 | 89 | **Important:** The old property names of the File class will be removed in a 90 | future release. 91 | 92 | [See Commits](https://github.com/felixge/node-formidable/compare/v0.9.10...v0.9.11) 93 | 94 | ### Older releases 95 | 96 | These releases were done before starting to maintain the above Changelog: 97 | 98 | * [v0.9.10](https://github.com/felixge/node-formidable/compare/v0.9.9...v0.9.10) 99 | * [v0.9.9](https://github.com/felixge/node-formidable/compare/v0.9.8...v0.9.9) 100 | * [v0.9.8](https://github.com/felixge/node-formidable/compare/v0.9.7...v0.9.8) 101 | * [v0.9.7](https://github.com/felixge/node-formidable/compare/v0.9.6...v0.9.7) 102 | * [v0.9.6](https://github.com/felixge/node-formidable/compare/v0.9.5...v0.9.6) 103 | * [v0.9.5](https://github.com/felixge/node-formidable/compare/v0.9.4...v0.9.5) 104 | * [v0.9.4](https://github.com/felixge/node-formidable/compare/v0.9.3...v0.9.4) 105 | * [v0.9.3](https://github.com/felixge/node-formidable/compare/v0.9.2...v0.9.3) 106 | * [v0.9.2](https://github.com/felixge/node-formidable/compare/v0.9.1...v0.9.2) 107 | * [v0.9.1](https://github.com/felixge/node-formidable/compare/v0.9.0...v0.9.1) 108 | * [v0.9.0](https://github.com/felixge/node-formidable/compare/v0.8.0...v0.9.0) 109 | * [v0.9.0](https://github.com/felixge/node-formidable/compare/v0.8.0...v0.9.0) 110 | * [v0.9.0](https://github.com/felixge/node-formidable/compare/v0.8.0...v0.9.0) 111 | * [v0.9.0](https://github.com/felixge/node-formidable/compare/v0.8.0...v0.9.0) 112 | * [v0.9.0](https://github.com/felixge/node-formidable/compare/v0.8.0...v0.9.0) 113 | * [v0.9.0](https://github.com/felixge/node-formidable/compare/v0.8.0...v0.9.0) 114 | * [v0.9.0](https://github.com/felixge/node-formidable/compare/v0.8.0...v0.9.0) 115 | * [v0.9.0](https://github.com/felixge/node-formidable/compare/v0.8.0...v0.9.0) 116 | * [v0.1.0](https://github.com/felixge/node-formidable/commits/v0.1.0) 117 | 118 | ## Installation 119 | 120 | Via [npm](http://github.com/isaacs/npm): 121 | 122 | npm install formidable@latest 123 | 124 | Manually: 125 | 126 | git clone git://github.com/felixge/node-formidable.git formidable 127 | vim my.js 128 | # var formidable = require('./formidable'); 129 | 130 | Note: Formidable requires [gently](http://github.com/felixge/node-gently) to run the unit tests, but you won't need it for just using the library. 131 | 132 | ## Example 133 | 134 | Parse an incoming file upload. 135 | 136 | var formidable = require('formidable'), 137 | http = require('http'), 138 | 139 | util = require('util'); 140 | 141 | http.createServer(function(req, res) { 142 | if (req.url == '/upload' && req.method.toLowerCase() == 'post') { 143 | // parse a file upload 144 | var form = new formidable.IncomingForm(); 145 | form.parse(req, function(err, fields, files) { 146 | res.writeHead(200, {'content-type': 'text/plain'}); 147 | res.write('received upload:\n\n'); 148 | res.end(util.inspect({fields: fields, files: files})); 149 | }); 150 | return; 151 | } 152 | 153 | // show a file upload form 154 | res.writeHead(200, {'content-type': 'text/html'}); 155 | res.end( 156 | '' 161 | ); 162 | }).listen(80); 163 | 164 | ## API 165 | 166 | ### formidable.IncomingForm 167 | 168 | #### new formidable.IncomingForm() 169 | 170 | Creates a new incoming form. 171 | 172 | #### incomingForm.encoding = 'utf-8' 173 | 174 | The encoding to use for incoming form fields. 175 | 176 | #### incomingForm.uploadDir = process.env.TMP || '/tmp' || process.cwd() 177 | 178 | The directory for placing file uploads in. You can move them later on using 179 | `fs.rename()`. The default directory is picked at module load time depending on 180 | the first existing directory from those listed above. 181 | 182 | #### incomingForm.keepExtensions = false 183 | 184 | If you want the files written to `incomingForm.uploadDir` to include the extensions of the original files, set this property to `true`. 185 | 186 | #### incomingForm.type 187 | 188 | Either 'multipart' or 'urlencoded' depending on the incoming request. 189 | 190 | #### incomingForm.maxFieldsSize = 2 * 1024 * 1024 191 | 192 | Limits the amount of memory a field (not file) can allocate in bytes. 193 | If this value is exceeded, an `'error'` event is emitted. The default 194 | size is 2MB. 195 | 196 | #### incomingForm.bytesReceived 197 | 198 | The amount of bytes received for this form so far. 199 | 200 | #### incomingForm.bytesExpected 201 | 202 | The expected number of bytes in this form. 203 | 204 | #### incomingForm.parse(request, [cb]) 205 | 206 | Parses an incoming node.js `request` containing form data. If `cb` is provided, all fields an files are collected and passed to the callback: 207 | 208 | incomingForm.parse(req, function(err, fields, files) { 209 | // ... 210 | }); 211 | 212 | #### incomingForm.onPart(part) 213 | 214 | You may overwrite this method if you are interested in directly accessing the multipart stream. Doing so will disable any `'field'` / `'file'` events processing which would occur otherwise, making you fully responsible for handling the processing. 215 | 216 | incomingForm.onPart = function(part) { 217 | part.addListener('data', function() { 218 | // ... 219 | }); 220 | } 221 | 222 | If you want to use formidable to only handle certain parts for you, you can do so: 223 | 224 | incomingForm.onPart = function(part) { 225 | if (!part.filename) { 226 | // let formidable handle all non-file parts 227 | incomingForm.handlePart(part); 228 | } 229 | } 230 | 231 | Check the code in this method for further inspiration. 232 | 233 | #### Event: 'progress' (bytesReceived, bytesExpected) 234 | 235 | Emitted after each incoming chunk of data that has been parsed. Can be used to roll your own progress bar. 236 | 237 | #### Event: 'field' (name, value) 238 | 239 | Emitted whenever a field / value pair has been received. 240 | 241 | #### Event: 'fileBegin' (name, file) 242 | 243 | Emitted whenever a new file is detected in the upload stream. Use this even if 244 | you want to stream the file to somewhere else while buffering the upload on 245 | the file system. 246 | 247 | #### Event: 'file' (name, file) 248 | 249 | Emitted whenever a field / file pair has been received. `file` is an instance of `File`. 250 | 251 | #### Event: 'error' (err) 252 | 253 | Emitted when there is an error processing the incoming form. A request that experiences an error is automatically paused, you will have to manually call `request.resume()` if you want the request to continue firing `'data'` events. 254 | 255 | #### Event: 'aborted' 256 | 257 | Emitted when the request was aborted by the user. Right now this can be due to a 'timeout' or 'close' event on the socket. In the future there will be a separate 'timeout' event (needs a change in the node core). 258 | 259 | #### Event: 'end' () 260 | 261 | Emitted when the entire request has been received, and all contained files have finished flushing to disk. This is a great place for you to send your response. 262 | 263 | ### formidable.File 264 | 265 | #### file.size = 0 266 | 267 | The size of the uploaded file in bytes. If the file is still being uploaded (see `'fileBegin'` event), this property says how many bytes of the file have been written to disk yet. 268 | 269 | #### file.path = null 270 | 271 | The path this file is being written to. You can modify this in the `'fileBegin'` event in 272 | case you are unhappy with the way formidable generates a temporary path for your files. 273 | 274 | #### file.name = null 275 | 276 | The name this file had according to the uploading client. 277 | 278 | #### file.type = null 279 | 280 | The mime type of this file, according to the uploading client. 281 | 282 | #### file.lastModifiedDate = null 283 | 284 | A date object (or `null`) containing the time this file was last written to. Mostly 285 | here for compatibility with the [W3C File API Draft](http://dev.w3.org/2006/webapi/FileAPI/). 286 | 287 | ## License 288 | 289 | Formidable is licensed under the MIT license. 290 | 291 | ## Ports 292 | 293 | * [multipart-parser](http://github.com/FooBarWidget/multipart-parser): a C++ parser based on formidable 294 | 295 | ## Credits 296 | 297 | * [Ryan Dahl](http://twitter.com/ryah) for his work on [http-parser](http://github.com/ry/http-parser) which heavily inspired multipart_parser.js 298 | -------------------------------------------------------------------------------- /test/legacy/simple/test-incoming-form.js: -------------------------------------------------------------------------------- 1 | var common = require('../common'); 2 | var MultipartParserStub = GENTLY.stub('./multipart_parser', 'MultipartParser'), 3 | QuerystringParserStub = GENTLY.stub('./querystring_parser', 'QuerystringParser'), 4 | EventEmitterStub = GENTLY.stub('events', 'EventEmitter'), 5 | FileStub = GENTLY.stub('./file'); 6 | 7 | var formidable = require(common.lib + '/index'), 8 | IncomingForm = formidable.IncomingForm, 9 | events = require('events'), 10 | fs = require('fs'), 11 | path = require('path'), 12 | Buffer = require('buffer').Buffer, 13 | fixtures = require(TEST_FIXTURES + '/multipart'), 14 | form, 15 | gently; 16 | 17 | function test(test) { 18 | gently = new Gently(); 19 | gently.expect(EventEmitterStub, 'call'); 20 | form = new IncomingForm(); 21 | test(); 22 | gently.verify(test.name); 23 | } 24 | 25 | test(function constructor() { 26 | assert.strictEqual(form.error, null); 27 | assert.strictEqual(form.ended, false); 28 | assert.strictEqual(form.type, null); 29 | assert.strictEqual(form.headers, null); 30 | assert.strictEqual(form.keepExtensions, false); 31 | assert.strictEqual(form.uploadDir, '/tmp'); 32 | assert.strictEqual(form.encoding, 'utf-8'); 33 | assert.strictEqual(form.bytesReceived, null); 34 | assert.strictEqual(form.bytesExpected, null); 35 | assert.strictEqual(form.maxFieldsSize, 2 * 1024 * 1024); 36 | assert.strictEqual(form._parser, null); 37 | assert.strictEqual(form._flushing, 0); 38 | assert.strictEqual(form._fieldsSize, 0); 39 | assert.ok(form instanceof EventEmitterStub); 40 | assert.equal(form.constructor.name, 'IncomingForm'); 41 | 42 | (function testSimpleConstructor() { 43 | gently.expect(EventEmitterStub, 'call'); 44 | var form = IncomingForm(); 45 | assert.ok(form instanceof IncomingForm); 46 | })(); 47 | 48 | (function testSimpleConstructorShortcut() { 49 | gently.expect(EventEmitterStub, 'call'); 50 | var form = formidable(); 51 | assert.ok(form instanceof IncomingForm); 52 | })(); 53 | }); 54 | 55 | test(function parse() { 56 | var REQ = {headers: {}} 57 | , emit = {}; 58 | 59 | gently.expect(form, 'writeHeaders', function(headers) { 60 | assert.strictEqual(headers, REQ.headers); 61 | }); 62 | 63 | var events = ['error', 'aborted', 'data', 'end']; 64 | gently.expect(REQ, 'on', events.length, function(event, fn) { 65 | assert.equal(event, events.shift()); 66 | emit[event] = fn; 67 | return this; 68 | }); 69 | 70 | form.parse(REQ); 71 | 72 | (function testPause() { 73 | gently.expect(REQ, 'pause'); 74 | assert.strictEqual(form.pause(), true); 75 | })(); 76 | 77 | (function testPauseCriticalException() { 78 | form.ended = false; 79 | 80 | var ERR = new Error('dasdsa'); 81 | gently.expect(REQ, 'pause', function() { 82 | throw ERR; 83 | }); 84 | 85 | gently.expect(form, '_error', function(err) { 86 | assert.strictEqual(err, ERR); 87 | }); 88 | 89 | assert.strictEqual(form.pause(), false); 90 | })(); 91 | 92 | (function testPauseHarmlessException() { 93 | form.ended = true; 94 | 95 | var ERR = new Error('dasdsa'); 96 | gently.expect(REQ, 'pause', function() { 97 | throw ERR; 98 | }); 99 | 100 | assert.strictEqual(form.pause(), false); 101 | })(); 102 | 103 | (function testResume() { 104 | gently.expect(REQ, 'resume'); 105 | assert.strictEqual(form.resume(), true); 106 | })(); 107 | 108 | (function testResumeCriticalException() { 109 | form.ended = false; 110 | 111 | var ERR = new Error('dasdsa'); 112 | gently.expect(REQ, 'resume', function() { 113 | throw ERR; 114 | }); 115 | 116 | gently.expect(form, '_error', function(err) { 117 | assert.strictEqual(err, ERR); 118 | }); 119 | 120 | assert.strictEqual(form.resume(), false); 121 | })(); 122 | 123 | (function testResumeHarmlessException() { 124 | form.ended = true; 125 | 126 | var ERR = new Error('dasdsa'); 127 | gently.expect(REQ, 'resume', function() { 128 | throw ERR; 129 | }); 130 | 131 | assert.strictEqual(form.resume(), false); 132 | })(); 133 | 134 | (function testEmitError() { 135 | var ERR = new Error('something bad happened'); 136 | gently.expect(form, '_error',function(err) { 137 | assert.strictEqual(err, ERR); 138 | }); 139 | emit.error(ERR); 140 | })(); 141 | 142 | (function testEmitAborted() { 143 | gently.expect(form, 'emit',function(event) { 144 | assert.equal(event, 'aborted'); 145 | }); 146 | 147 | emit.aborted(); 148 | })(); 149 | 150 | 151 | (function testEmitData() { 152 | var BUFFER = [1, 2, 3]; 153 | gently.expect(form, 'write', function(buffer) { 154 | assert.strictEqual(buffer, BUFFER); 155 | }); 156 | emit.data(BUFFER); 157 | })(); 158 | 159 | (function testEmitEnd() { 160 | form._parser = {}; 161 | 162 | (function testWithError() { 163 | var ERR = new Error('haha'); 164 | gently.expect(form._parser, 'end', function() { 165 | return ERR; 166 | }); 167 | 168 | gently.expect(form, '_error', function(err) { 169 | assert.strictEqual(err, ERR); 170 | }); 171 | 172 | emit.end(); 173 | })(); 174 | 175 | (function testWithoutError() { 176 | gently.expect(form._parser, 'end'); 177 | emit.end(); 178 | })(); 179 | 180 | (function testAfterError() { 181 | form.error = true; 182 | emit.end(); 183 | })(); 184 | })(); 185 | 186 | (function testWithCallback() { 187 | gently.expect(EventEmitterStub, 'call'); 188 | var form = new IncomingForm(), 189 | REQ = {headers: {}}, 190 | parseCalled = 0; 191 | 192 | gently.expect(form, 'writeHeaders'); 193 | gently.expect(REQ, 'on', 4, function() { 194 | return this; 195 | }); 196 | 197 | gently.expect(form, 'on', 4, function(event, fn) { 198 | if (event == 'field') { 199 | fn('field1', 'foo'); 200 | fn('field1', 'bar'); 201 | fn('field2', 'nice'); 202 | } 203 | 204 | if (event == 'file') { 205 | fn('file1', '1'); 206 | fn('file1', '2'); 207 | fn('file2', '3'); 208 | } 209 | 210 | if (event == 'end') { 211 | fn(); 212 | } 213 | return this; 214 | }); 215 | 216 | form.parse(REQ, gently.expect(function parseCbOk(err, fields, files) { 217 | assert.deepEqual(fields, {field1: 'bar', field2: 'nice'}); 218 | assert.deepEqual(files, {file1: '2', file2: '3'}); 219 | })); 220 | 221 | gently.expect(form, 'writeHeaders'); 222 | gently.expect(REQ, 'on', 4, function() { 223 | return this; 224 | }); 225 | 226 | var ERR = new Error('test'); 227 | gently.expect(form, 'on', 3, function(event, fn) { 228 | if (event == 'field') { 229 | fn('foo', 'bar'); 230 | } 231 | 232 | if (event == 'error') { 233 | fn(ERR); 234 | gently.expect(form, 'on'); 235 | } 236 | return this; 237 | }); 238 | 239 | form.parse(REQ, gently.expect(function parseCbErr(err, fields, files) { 240 | assert.strictEqual(err, ERR); 241 | assert.deepEqual(fields, {foo: 'bar'}); 242 | })); 243 | })(); 244 | }); 245 | 246 | test(function pause() { 247 | assert.strictEqual(form.pause(), false); 248 | }); 249 | 250 | test(function resume() { 251 | assert.strictEqual(form.resume(), false); 252 | }); 253 | 254 | 255 | test(function writeHeaders() { 256 | var HEADERS = {}; 257 | gently.expect(form, '_parseContentLength'); 258 | gently.expect(form, '_parseContentType'); 259 | 260 | form.writeHeaders(HEADERS); 261 | assert.strictEqual(form.headers, HEADERS); 262 | }); 263 | 264 | test(function write() { 265 | var parser = {}, 266 | BUFFER = [1, 2, 3]; 267 | 268 | form._parser = parser; 269 | form.bytesExpected = 523423; 270 | 271 | (function testBasic() { 272 | gently.expect(form, 'emit', function(event, bytesReceived, bytesExpected) { 273 | assert.equal(event, 'progress'); 274 | assert.equal(bytesReceived, BUFFER.length); 275 | assert.equal(bytesExpected, form.bytesExpected); 276 | }); 277 | 278 | gently.expect(parser, 'write', function(buffer) { 279 | assert.strictEqual(buffer, BUFFER); 280 | return buffer.length; 281 | }); 282 | 283 | assert.equal(form.write(BUFFER), BUFFER.length); 284 | assert.equal(form.bytesReceived, BUFFER.length); 285 | })(); 286 | 287 | (function testParserError() { 288 | gently.expect(form, 'emit'); 289 | 290 | gently.expect(parser, 'write', function(buffer) { 291 | assert.strictEqual(buffer, BUFFER); 292 | return buffer.length - 1; 293 | }); 294 | 295 | gently.expect(form, '_error', function(err) { 296 | assert.ok(err.message.match(/parser error/i)); 297 | }); 298 | 299 | assert.equal(form.write(BUFFER), BUFFER.length - 1); 300 | assert.equal(form.bytesReceived, BUFFER.length + BUFFER.length); 301 | })(); 302 | 303 | (function testUninitialized() { 304 | delete form._parser; 305 | 306 | gently.expect(form, '_error', function(err) { 307 | assert.ok(err.message.match(/unintialized parser/i)); 308 | }); 309 | form.write(BUFFER); 310 | })(); 311 | }); 312 | 313 | test(function parseContentType() { 314 | var HEADERS = {}; 315 | 316 | form.headers = {'content-type': 'application/x-www-form-urlencoded'}; 317 | gently.expect(form, '_initUrlencoded'); 318 | form._parseContentType(); 319 | 320 | // accept anything that has 'urlencoded' in it 321 | form.headers = {'content-type': 'broken-client/urlencoded-stupid'}; 322 | gently.expect(form, '_initUrlencoded'); 323 | form._parseContentType(); 324 | 325 | var BOUNDARY = '---------------------------57814261102167618332366269'; 326 | form.headers = {'content-type': 'multipart/form-data; boundary='+BOUNDARY}; 327 | 328 | gently.expect(form, '_initMultipart', function(boundary) { 329 | assert.equal(boundary, BOUNDARY); 330 | }); 331 | form._parseContentType(); 332 | 333 | (function testQuotedBoundary() { 334 | form.headers = {'content-type': 'multipart/form-data; boundary="' + BOUNDARY + '"'}; 335 | 336 | gently.expect(form, '_initMultipart', function(boundary) { 337 | assert.equal(boundary, BOUNDARY); 338 | }); 339 | form._parseContentType(); 340 | })(); 341 | 342 | (function testNoBoundary() { 343 | form.headers = {'content-type': 'multipart/form-data'}; 344 | 345 | gently.expect(form, '_error', function(err) { 346 | assert.ok(err.message.match(/no multipart boundary/i)); 347 | }); 348 | form._parseContentType(); 349 | })(); 350 | 351 | (function testNoContentType() { 352 | form.headers = {}; 353 | 354 | gently.expect(form, '_error', function(err) { 355 | assert.ok(err.message.match(/no content-type/i)); 356 | }); 357 | form._parseContentType(); 358 | })(); 359 | 360 | (function testUnknownContentType() { 361 | form.headers = {'content-type': 'invalid'}; 362 | 363 | gently.expect(form, '_error', function(err) { 364 | assert.ok(err.message.match(/unknown content-type/i)); 365 | }); 366 | form._parseContentType(); 367 | })(); 368 | }); 369 | 370 | test(function parseContentLength() { 371 | var HEADERS = {}; 372 | 373 | form.headers = {}; 374 | form._parseContentLength(); 375 | assert.strictEqual(form.bytesExpected, null); 376 | 377 | form.headers['content-length'] = '8'; 378 | form._parseContentLength(); 379 | assert.strictEqual(form.bytesReceived, 0); 380 | assert.strictEqual(form.bytesExpected, 8); 381 | 382 | // JS can be evil, lets make sure we are not 383 | form.headers['content-length'] = '08'; 384 | form._parseContentLength(); 385 | assert.strictEqual(form.bytesExpected, 8); 386 | }); 387 | 388 | test(function _initMultipart() { 389 | var BOUNDARY = '123', 390 | PARSER; 391 | 392 | gently.expect(MultipartParserStub, 'new', function() { 393 | PARSER = this; 394 | }); 395 | 396 | gently.expect(MultipartParserStub.prototype, 'initWithBoundary', function(boundary) { 397 | assert.equal(boundary, BOUNDARY); 398 | }); 399 | 400 | form._initMultipart(BOUNDARY); 401 | assert.equal(form.type, 'multipart'); 402 | assert.strictEqual(form._parser, PARSER); 403 | 404 | (function testRegularField() { 405 | var PART; 406 | gently.expect(EventEmitterStub, 'new', function() { 407 | PART = this; 408 | }); 409 | 410 | gently.expect(form, 'onPart', function(part) { 411 | assert.strictEqual(part, PART); 412 | assert.deepEqual 413 | ( part.headers 414 | , { 'content-disposition': 'form-data; name="field1"' 415 | , 'foo': 'bar' 416 | } 417 | ); 418 | assert.equal(part.name, 'field1'); 419 | 420 | var strings = ['hello', ' world']; 421 | gently.expect(part, 'emit', 2, function(event, b) { 422 | assert.equal(event, 'data'); 423 | assert.equal(b.toString(), strings.shift()); 424 | }); 425 | 426 | gently.expect(part, 'emit', function(event, b) { 427 | assert.equal(event, 'end'); 428 | }); 429 | }); 430 | 431 | PARSER.onPartBegin(); 432 | PARSER.onHeaderField(new Buffer('content-disposition'), 0, 10); 433 | PARSER.onHeaderField(new Buffer('content-disposition'), 10, 19); 434 | PARSER.onHeaderValue(new Buffer('form-data; name="field1"'), 0, 14); 435 | PARSER.onHeaderValue(new Buffer('form-data; name="field1"'), 14, 24); 436 | PARSER.onHeaderEnd(); 437 | PARSER.onHeaderField(new Buffer('foo'), 0, 3); 438 | PARSER.onHeaderValue(new Buffer('bar'), 0, 3); 439 | PARSER.onHeaderEnd(); 440 | PARSER.onHeadersEnd(); 441 | PARSER.onPartData(new Buffer('hello world'), 0, 5); 442 | PARSER.onPartData(new Buffer('hello world'), 5, 11); 443 | PARSER.onPartEnd(); 444 | })(); 445 | 446 | (function testFileField() { 447 | var PART; 448 | gently.expect(EventEmitterStub, 'new', function() { 449 | PART = this; 450 | }); 451 | 452 | gently.expect(form, 'onPart', function(part) { 453 | assert.deepEqual 454 | ( part.headers 455 | , { 'content-disposition': 'form-data; name="field2"; filename="C:\\Documents and Settings\\IE\\Must\\Die\\Sun"et.jpg"' 456 | , 'content-type': 'text/plain' 457 | } 458 | ); 459 | assert.equal(part.name, 'field2'); 460 | assert.equal(part.filename, 'Sun"et.jpg'); 461 | assert.equal(part.mime, 'text/plain'); 462 | 463 | gently.expect(part, 'emit', function(event, b) { 464 | assert.equal(event, 'data'); 465 | assert.equal(b.toString(), '... contents of file1.txt ...'); 466 | }); 467 | 468 | gently.expect(part, 'emit', function(event, b) { 469 | assert.equal(event, 'end'); 470 | }); 471 | }); 472 | 473 | PARSER.onPartBegin(); 474 | PARSER.onHeaderField(new Buffer('content-disposition'), 0, 19); 475 | PARSER.onHeaderValue(new Buffer('form-data; name="field2"; filename="C:\\Documents and Settings\\IE\\Must\\Die\\Sun"et.jpg"'), 0, 85); 476 | PARSER.onHeaderEnd(); 477 | PARSER.onHeaderField(new Buffer('Content-Type'), 0, 12); 478 | PARSER.onHeaderValue(new Buffer('text/plain'), 0, 10); 479 | PARSER.onHeaderEnd(); 480 | PARSER.onHeadersEnd(); 481 | PARSER.onPartData(new Buffer('... contents of file1.txt ...'), 0, 29); 482 | PARSER.onPartEnd(); 483 | })(); 484 | 485 | (function testEnd() { 486 | gently.expect(form, '_maybeEnd'); 487 | PARSER.onEnd(); 488 | assert.ok(form.ended); 489 | })(); 490 | }); 491 | 492 | test(function _fileName() { 493 | // TODO 494 | return; 495 | }); 496 | 497 | test(function _initUrlencoded() { 498 | var PARSER; 499 | 500 | gently.expect(QuerystringParserStub, 'new', function() { 501 | PARSER = this; 502 | }); 503 | 504 | form._initUrlencoded(); 505 | assert.equal(form.type, 'urlencoded'); 506 | assert.strictEqual(form._parser, PARSER); 507 | 508 | (function testOnField() { 509 | var KEY = 'KEY', VAL = 'VAL'; 510 | gently.expect(form, 'emit', function(field, key, val) { 511 | assert.equal(field, 'field'); 512 | assert.equal(key, KEY); 513 | assert.equal(val, VAL); 514 | }); 515 | 516 | PARSER.onField(KEY, VAL); 517 | })(); 518 | 519 | (function testOnEnd() { 520 | gently.expect(form, '_maybeEnd'); 521 | 522 | PARSER.onEnd(); 523 | assert.equal(form.ended, true); 524 | })(); 525 | }); 526 | 527 | test(function _error() { 528 | var ERR = new Error('bla'); 529 | 530 | gently.expect(form, 'pause'); 531 | gently.expect(form, 'emit', function(event, err) { 532 | assert.equal(event, 'error'); 533 | assert.strictEqual(err, ERR); 534 | }); 535 | 536 | form._error(ERR); 537 | assert.strictEqual(form.error, ERR); 538 | 539 | // make sure _error only does its thing once 540 | form._error(ERR); 541 | }); 542 | 543 | test(function onPart() { 544 | var PART = {}; 545 | gently.expect(form, 'handlePart', function(part) { 546 | assert.strictEqual(part, PART); 547 | }); 548 | 549 | form.onPart(PART); 550 | }); 551 | 552 | test(function handlePart() { 553 | (function testUtf8Field() { 554 | var PART = new events.EventEmitter(); 555 | PART.name = 'my_field'; 556 | 557 | gently.expect(form, 'emit', function(event, field, value) { 558 | assert.equal(event, 'field'); 559 | assert.equal(field, 'my_field'); 560 | assert.equal(value, 'hello world: €'); 561 | }); 562 | 563 | form.handlePart(PART); 564 | PART.emit('data', new Buffer('hello')); 565 | PART.emit('data', new Buffer(' world: ')); 566 | PART.emit('data', new Buffer([0xE2])); 567 | PART.emit('data', new Buffer([0x82, 0xAC])); 568 | PART.emit('end'); 569 | })(); 570 | 571 | (function testBinaryField() { 572 | var PART = new events.EventEmitter(); 573 | PART.name = 'my_field2'; 574 | 575 | gently.expect(form, 'emit', function(event, field, value) { 576 | assert.equal(event, 'field'); 577 | assert.equal(field, 'my_field2'); 578 | assert.equal(value, 'hello world: '+new Buffer([0xE2, 0x82, 0xAC]).toString('binary')); 579 | }); 580 | 581 | form.encoding = 'binary'; 582 | form.handlePart(PART); 583 | PART.emit('data', new Buffer('hello')); 584 | PART.emit('data', new Buffer(' world: ')); 585 | PART.emit('data', new Buffer([0xE2])); 586 | PART.emit('data', new Buffer([0x82, 0xAC])); 587 | PART.emit('end'); 588 | })(); 589 | 590 | (function testFieldSize() { 591 | form.maxFieldsSize = 8; 592 | var PART = new events.EventEmitter(); 593 | PART.name = 'my_field'; 594 | 595 | gently.expect(form, '_error', function(err) { 596 | assert.equal(err.message, 'maxFieldsSize exceeded, received 9 bytes of field data'); 597 | }); 598 | 599 | form.handlePart(PART); 600 | form._fieldsSize = 1; 601 | PART.emit('data', new Buffer(7)); 602 | PART.emit('data', new Buffer(1)); 603 | })(); 604 | 605 | (function testFilePart() { 606 | var PART = new events.EventEmitter(), 607 | FILE = new events.EventEmitter(), 608 | PATH = '/foo/bar'; 609 | 610 | PART.name = 'my_file'; 611 | PART.filename = 'sweet.txt'; 612 | PART.mime = 'sweet.txt'; 613 | 614 | gently.expect(form, '_uploadPath', function(filename) { 615 | assert.equal(filename, PART.filename); 616 | return PATH; 617 | }); 618 | 619 | gently.expect(FileStub, 'new', function(properties) { 620 | assert.equal(properties.path, PATH); 621 | assert.equal(properties.name, PART.filename); 622 | assert.equal(properties.type, PART.mime); 623 | FILE = this; 624 | 625 | gently.expect(form, 'emit', function (event, field, file) { 626 | assert.equal(event, 'fileBegin'); 627 | assert.strictEqual(field, PART.name); 628 | assert.strictEqual(file, FILE); 629 | }); 630 | 631 | gently.expect(FILE, 'open'); 632 | }); 633 | 634 | form.handlePart(PART); 635 | assert.equal(form._flushing, 1); 636 | 637 | var BUFFER; 638 | gently.expect(form, 'pause'); 639 | gently.expect(FILE, 'write', function(buffer, cb) { 640 | assert.strictEqual(buffer, BUFFER); 641 | gently.expect(form, 'resume'); 642 | // @todo handle cb(new Err) 643 | cb(); 644 | }); 645 | 646 | PART.emit('data', BUFFER = new Buffer('test')); 647 | 648 | gently.expect(FILE, 'end', function(cb) { 649 | gently.expect(form, 'emit', function(event, field, file) { 650 | assert.equal(event, 'file'); 651 | assert.strictEqual(file, FILE); 652 | }); 653 | 654 | gently.expect(form, '_maybeEnd'); 655 | 656 | cb(); 657 | assert.equal(form._flushing, 0); 658 | }); 659 | 660 | PART.emit('end'); 661 | })(); 662 | }); 663 | 664 | test(function _uploadPath() { 665 | (function testUniqueId() { 666 | var UUID_A, UUID_B; 667 | gently.expect(GENTLY.hijacked.path, 'join', function(uploadDir, uuid) { 668 | assert.equal(uploadDir, form.uploadDir); 669 | UUID_A = uuid; 670 | }); 671 | form._uploadPath(); 672 | 673 | gently.expect(GENTLY.hijacked.path, 'join', function(uploadDir, uuid) { 674 | UUID_B = uuid; 675 | }); 676 | form._uploadPath(); 677 | 678 | assert.notEqual(UUID_A, UUID_B); 679 | })(); 680 | 681 | (function testFileExtension() { 682 | form.keepExtensions = true; 683 | var FILENAME = 'foo.jpg', 684 | EXT = '.bar'; 685 | 686 | gently.expect(GENTLY.hijacked.path, 'extname', function(filename) { 687 | assert.equal(filename, FILENAME); 688 | gently.restore(path, 'extname'); 689 | 690 | return EXT; 691 | }); 692 | 693 | gently.expect(GENTLY.hijacked.path, 'join', function(uploadDir, name) { 694 | assert.equal(path.extname(name), EXT); 695 | }); 696 | form._uploadPath(FILENAME); 697 | })(); 698 | }); 699 | 700 | test(function _maybeEnd() { 701 | gently.expect(form, 'emit', 0); 702 | form._maybeEnd(); 703 | 704 | form.ended = true; 705 | form._flushing = 1; 706 | form._maybeEnd(); 707 | 708 | gently.expect(form, 'emit', function(event) { 709 | assert.equal(event, 'end'); 710 | }); 711 | 712 | form.ended = true; 713 | form._flushing = 0; 714 | form._maybeEnd(); 715 | }); 716 | --------------------------------------------------------------------------------