From a2df9a69dcc14cb70118cda2ded499055e7ee358 Mon Sep 17 00:00:00 2001 From: Minteck Date: Sun, 21 Aug 2022 17:31:56 +0200 Subject: m. update --- together/node_modules/formidable/src/Formidable.js | 617 +++++++++++++++++++++ .../node_modules/formidable/src/FormidableError.js | 45 ++ .../node_modules/formidable/src/PersistentFile.js | 87 +++ .../node_modules/formidable/src/VolatileFile.js | 82 +++ together/node_modules/formidable/src/index.js | 38 ++ .../node_modules/formidable/src/parsers/Dummy.js | 21 + .../node_modules/formidable/src/parsers/JSON.js | 35 ++ .../formidable/src/parsers/Multipart.js | 347 ++++++++++++ .../formidable/src/parsers/OctetStream.js | 12 + .../formidable/src/parsers/Querystring.js | 38 ++ .../formidable/src/parsers/StreamingQuerystring.js | 121 ++++ .../node_modules/formidable/src/parsers/index.js | 17 + .../node_modules/formidable/src/plugins/index.js | 13 + .../node_modules/formidable/src/plugins/json.js | 38 ++ .../formidable/src/plugins/multipart.js | 173 ++++++ .../formidable/src/plugins/octetstream.js | 86 +++ .../formidable/src/plugins/querystring.js | 42 ++ 17 files changed, 1812 insertions(+) create mode 100644 together/node_modules/formidable/src/Formidable.js create mode 100644 together/node_modules/formidable/src/FormidableError.js create mode 100644 together/node_modules/formidable/src/PersistentFile.js create mode 100644 together/node_modules/formidable/src/VolatileFile.js create mode 100644 together/node_modules/formidable/src/index.js create mode 100644 together/node_modules/formidable/src/parsers/Dummy.js create mode 100644 together/node_modules/formidable/src/parsers/JSON.js create mode 100644 together/node_modules/formidable/src/parsers/Multipart.js create mode 100644 together/node_modules/formidable/src/parsers/OctetStream.js create mode 100644 together/node_modules/formidable/src/parsers/Querystring.js create mode 100644 together/node_modules/formidable/src/parsers/StreamingQuerystring.js create mode 100644 together/node_modules/formidable/src/parsers/index.js create mode 100644 together/node_modules/formidable/src/plugins/index.js create mode 100644 together/node_modules/formidable/src/plugins/json.js create mode 100644 together/node_modules/formidable/src/plugins/multipart.js create mode 100644 together/node_modules/formidable/src/plugins/octetstream.js create mode 100644 together/node_modules/formidable/src/plugins/querystring.js (limited to 'together/node_modules/formidable/src') diff --git a/together/node_modules/formidable/src/Formidable.js b/together/node_modules/formidable/src/Formidable.js new file mode 100644 index 0000000..0542700 --- /dev/null +++ b/together/node_modules/formidable/src/Formidable.js @@ -0,0 +1,617 @@ +/* eslint-disable class-methods-use-this */ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const os = require('os'); +const path = require('path'); +const hexoid = require('hexoid'); +const once = require('once'); +const dezalgo = require('dezalgo'); +const { EventEmitter } = require('events'); +const { StringDecoder } = require('string_decoder'); +const qs = require('qs'); + +const toHexoId = hexoid(25); +const DEFAULT_OPTIONS = { + maxFields: 1000, + maxFieldsSize: 20 * 1024 * 1024, + maxFileSize: 200 * 1024 * 1024, + minFileSize: 1, + allowEmptyFiles: true, + keepExtensions: false, + encoding: 'utf-8', + hashAlgorithm: false, + uploadDir: os.tmpdir(), + multiples: false, + enabledPlugins: ['octetstream', 'querystring', 'multipart', 'json'], + fileWriteStreamHandler: null, + defaultInvalidName: 'invalid-name', + filter: function () { + return true; + }, +}; + +const PersistentFile = require('./PersistentFile'); +const VolatileFile = require('./VolatileFile'); +const DummyParser = require('./parsers/Dummy'); +const MultipartParser = require('./parsers/Multipart'); +const errors = require('./FormidableError.js'); + +const { FormidableError } = errors; + +function hasOwnProp(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + +class IncomingForm extends EventEmitter { + constructor(options = {}) { + super(); + + this.options = { ...DEFAULT_OPTIONS, ...options }; + + const dir = path.resolve( + this.options.uploadDir || this.options.uploaddir || os.tmpdir(), + ); + + this.uploaddir = dir; + this.uploadDir = dir; + + // initialize with null + [ + 'error', + 'headers', + 'type', + 'bytesExpected', + 'bytesReceived', + '_parser', + ].forEach((key) => { + this[key] = null; + }); + + this._setUpRename(); + + this._flushing = 0; + this._fieldsSize = 0; + this._fileSize = 0; + this._plugins = []; + this.openedFiles = []; + + this.options.enabledPlugins = [] + .concat(this.options.enabledPlugins) + .filter(Boolean); + + if (this.options.enabledPlugins.length === 0) { + throw new FormidableError( + 'expect at least 1 enabled builtin plugin, see options.enabledPlugins', + errors.missingPlugin, + ); + } + + this.options.enabledPlugins.forEach((pluginName) => { + const plgName = pluginName.toLowerCase(); + // eslint-disable-next-line import/no-dynamic-require, global-require + this.use(require(path.join(__dirname, 'plugins', `${plgName}.js`))); + }); + + this._setUpMaxFields(); + } + + use(plugin) { + if (typeof plugin !== 'function') { + throw new FormidableError( + '.use: expect `plugin` to be a function', + errors.pluginFunction, + ); + } + this._plugins.push(plugin.bind(this)); + return this; + } + + parse(req, cb) { + this.pause = () => { + try { + req.pause(); + } catch (err) { + // the stream was destroyed + if (!this.ended) { + // before it was completed, crash & burn + this._error(err); + } + return false; + } + return true; + }; + + this.resume = () => { + try { + req.resume(); + } catch (err) { + // the stream was destroyed + if (!this.ended) { + // before it was completed, crash & burn + this._error(err); + } + return false; + } + + return true; + }; + + // Setup callback first, so we don't miss anything from data events emitted immediately. + if (cb) { + const callback = once(dezalgo(cb)); + const fields = {}; + let mockFields = ''; + const files = {}; + + this.on('field', (name, value) => { + if ( + this.options.multiples && + (this.type === 'multipart' || this.type === 'urlencoded') + ) { + const mObj = { [name]: value }; + mockFields = mockFields + ? `${mockFields}&${qs.stringify(mObj)}` + : `${qs.stringify(mObj)}`; + } else { + fields[name] = value; + } + }); + this.on('file', (name, file) => { + // TODO: too much nesting + if (this.options.multiples) { + if (hasOwnProp(files, name)) { + if (!Array.isArray(files[name])) { + files[name] = [files[name]]; + } + files[name].push(file); + } else { + files[name] = file; + } + } else { + files[name] = file; + } + }); + this.on('error', (err) => { + callback(err, fields, files); + }); + this.on('end', () => { + if (this.options.multiples) { + Object.assign(fields, qs.parse(mockFields)); + } + callback(null, fields, files); + }); + } + + // Parse headers and setup the parser, ready to start listening for data. + this.writeHeaders(req.headers); + + // Start listening for data. + req + .on('error', (err) => { + this._error(err); + }) + .on('aborted', () => { + this.emit('aborted'); + this._error(new FormidableError('Request aborted', errors.aborted)); + }) + .on('data', (buffer) => { + try { + this.write(buffer); + } catch (err) { + this._error(err); + } + }) + .on('end', () => { + if (this.error) { + return; + } + if (this._parser) { + this._parser.end(); + } + this._maybeEnd(); + }); + + return this; + } + + writeHeaders(headers) { + this.headers = headers; + this._parseContentLength(); + this._parseContentType(); + + if (!this._parser) { + this._error( + new FormidableError( + 'no parser found', + errors.noParser, + 415, // Unsupported Media Type + ), + ); + return; + } + + this._parser.once('error', (error) => { + this._error(error); + }); + } + + write(buffer) { + if (this.error) { + return null; + } + if (!this._parser) { + this._error( + new FormidableError('uninitialized parser', errors.uninitializedParser), + ); + return null; + } + + this.bytesReceived += buffer.length; + this.emit('progress', this.bytesReceived, this.bytesExpected); + + this._parser.write(buffer); + + return this.bytesReceived; + } + + pause() { + // this does nothing, unless overwritten in IncomingForm.parse + return false; + } + + resume() { + // this does nothing, unless overwritten in IncomingForm.parse + return false; + } + + onPart(part) { + // this method can be overwritten by the user + this._handlePart(part); + } + + _handlePart(part) { + if (part.originalFilename && typeof part.originalFilename !== 'string') { + this._error( + new FormidableError( + `the part.originalFilename should be string when it exists`, + errors.filenameNotString, + ), + ); + return; + } + + // This MUST check exactly for undefined. You can not change it to !part.originalFilename. + + // todo: uncomment when switch tests to Jest + // console.log(part); + + // ? NOTE(@tunnckocore): no it can be any falsey value, it most probably depends on what's returned + // from somewhere else. Where recently I changed the return statements + // and such thing because code style + // ? NOTE(@tunnckocore): or even better, if there is no mimetype, then it's for sure a field + // ? NOTE(@tunnckocore): originalFilename is an empty string when a field? + if (!part.mimetype) { + let value = ''; + const decoder = new StringDecoder( + part.transferEncoding || this.options.encoding, + ); + + part.on('data', (buffer) => { + this._fieldsSize += buffer.length; + if (this._fieldsSize > this.options.maxFieldsSize) { + this._error( + new FormidableError( + `options.maxFieldsSize (${this.options.maxFieldsSize} bytes) exceeded, received ${this._fieldsSize} bytes of field data`, + errors.maxFieldsSizeExceeded, + 413, // Payload Too Large + ), + ); + return; + } + value += decoder.write(buffer); + }); + + part.on('end', () => { + this.emit('field', part.name, value); + }); + return; + } + + if (!this.options.filter(part)) { + return; + } + + this._flushing += 1; + + const newFilename = this._getNewName(part); + const filepath = this._joinDirectoryName(newFilename); + const file = this._newFile({ + newFilename, + filepath, + originalFilename: part.originalFilename, + mimetype: part.mimetype, + }); + file.on('error', (err) => { + this._error(err); + }); + this.emit('fileBegin', part.name, file); + + file.open(); + this.openedFiles.push(file); + + part.on('data', (buffer) => { + this._fileSize += buffer.length; + if (this._fileSize < this.options.minFileSize) { + this._error( + new FormidableError( + `options.minFileSize (${this.options.minFileSize} bytes) inferior, received ${this._fileSize} bytes of file data`, + errors.smallerThanMinFileSize, + 400, + ), + ); + return; + } + if (this._fileSize > this.options.maxFileSize) { + this._error( + new FormidableError( + `options.maxFileSize (${this.options.maxFileSize} bytes) exceeded, received ${this._fileSize} bytes of file data`, + errors.biggerThanMaxFileSize, + 413, + ), + ); + return; + } + if (buffer.length === 0) { + return; + } + this.pause(); + file.write(buffer, () => { + this.resume(); + }); + }); + + part.on('end', () => { + if (!this.options.allowEmptyFiles && this._fileSize === 0) { + this._error( + new FormidableError( + `options.allowEmptyFiles is false, file size should be greather than 0`, + errors.noEmptyFiles, + 400, + ), + ); + return; + } + + file.end(() => { + this._flushing -= 1; + this.emit('file', part.name, file); + this._maybeEnd(); + }); + }); + } + + // eslint-disable-next-line max-statements + _parseContentType() { + if (this.bytesExpected === 0) { + this._parser = new DummyParser(this, this.options); + return; + } + + if (!this.headers['content-type']) { + this._error( + new FormidableError( + 'bad content-type header, no content-type', + errors.missingContentType, + 400, + ), + ); + return; + } + + const results = []; + const _dummyParser = new DummyParser(this, this.options); + + // eslint-disable-next-line no-plusplus + for (let idx = 0; idx < this._plugins.length; idx++) { + const plugin = this._plugins[idx]; + + let pluginReturn = null; + + try { + pluginReturn = plugin(this, this.options) || this; + } catch (err) { + // directly throw from the `form.parse` method; + // there is no other better way, except a handle through options + const error = new FormidableError( + `plugin on index ${idx} failed with: ${err.message}`, + errors.pluginFailed, + 500, + ); + error.idx = idx; + throw error; + } + + Object.assign(this, pluginReturn); + + // todo: use Set/Map and pass plugin name instead of the `idx` index + this.emit('plugin', idx, pluginReturn); + results.push(pluginReturn); + } + + this.emit('pluginsResults', results); + + // NOTE: probably not needed, because we check options.enabledPlugins in the constructor + // if (results.length === 0 /* && results.length !== this._plugins.length */) { + // this._error( + // new Error( + // `bad content-type header, unknown content-type: ${this.headers['content-type']}`, + // ), + // ); + // } + } + + _error(err, eventName = 'error') { + // if (!err && this.error) { + // this.emit('error', this.error); + // return; + // } + if (this.error || this.ended) { + return; + } + + this.error = err; + this.emit(eventName, err); + + if (Array.isArray(this.openedFiles)) { + this.openedFiles.forEach((file) => { + file.destroy(); + }); + } + } + + _parseContentLength() { + this.bytesReceived = 0; + if (this.headers['content-length']) { + this.bytesExpected = parseInt(this.headers['content-length'], 10); + } else if (this.headers['transfer-encoding'] === undefined) { + this.bytesExpected = 0; + } + + if (this.bytesExpected !== null) { + this.emit('progress', this.bytesReceived, this.bytesExpected); + } + } + + _newParser() { + return new MultipartParser(this.options); + } + + _newFile({ filepath, originalFilename, mimetype, newFilename }) { + return this.options.fileWriteStreamHandler + ? new VolatileFile({ + newFilename, + filepath, + originalFilename, + mimetype, + createFileWriteStream: this.options.fileWriteStreamHandler, + hashAlgorithm: this.options.hashAlgorithm, + }) + : new PersistentFile({ + newFilename, + filepath, + originalFilename, + mimetype, + hashAlgorithm: this.options.hashAlgorithm, + }); + } + + _getFileName(headerValue) { + // matches either a quoted-string or a token (RFC 2616 section 19.5.1) + const m = headerValue.match( + /\bfilename=("(.*?)"|([^()<>{}[\]@,;:"?=\s/\t]+))($|;\s)/i, + ); + if (!m) return null; + + const match = m[2] || m[3] || ''; + let originalFilename = match.substr(match.lastIndexOf('\\') + 1); + originalFilename = originalFilename.replace(/%22/g, '"'); + originalFilename = originalFilename.replace(/&#([\d]{4});/g, (_, code) => + String.fromCharCode(code), + ); + + return originalFilename; + } + + _getExtension(str) { + if (!str) { + return ''; + } + + const basename = path.basename(str); + const firstDot = basename.indexOf('.'); + const lastDot = basename.lastIndexOf('.'); + const extname = path.extname(basename).replace(/(\.[a-z0-9]+).*/i, '$1'); + + if (firstDot === lastDot) { + return extname; + } + + return basename.slice(firstDot, lastDot) + extname; + } + + + + _joinDirectoryName(name) { + const newPath = path.join(this.uploadDir, name); + + // prevent directory traversal attacks + if (!newPath.startsWith(this.uploadDir)) { + return path.join(this.uploadDir, this.options.defaultInvalidName); + } + + return newPath; + } + + _setUpRename() { + const hasRename = typeof this.options.filename === 'function'; + if (hasRename) { + this._getNewName = (part) => { + let ext = ''; + let name = this.options.defaultInvalidName; + if (part.originalFilename) { + // can be null + ({ ext, name } = path.parse(part.originalFilename)); + if (this.options.keepExtensions !== true) { + ext = ''; + } + } + return this.options.filename.call(this, name, ext, part, this); + }; + } else { + this._getNewName = (part) => { + const name = toHexoId(); + + if (part && this.options.keepExtensions) { + const originalFilename = typeof part === 'string' ? part : part.originalFilename; + return `${name}${this._getExtension(originalFilename)}`; + } + + return name; + } + } + } + + _setUpMaxFields() { + if (this.options.maxFields !== 0) { + let fieldsCount = 0; + this.on('field', () => { + fieldsCount += 1; + if (fieldsCount > this.options.maxFields) { + this._error( + new FormidableError( + `options.maxFields (${this.options.maxFields}) exceeded`, + errors.maxFieldsExceeded, + 413, + ), + ); + } + }); + } + } + + _maybeEnd() { + // console.log('ended', this.ended); + // console.log('_flushing', this._flushing); + // console.log('error', this.error); + if (!this.ended || this._flushing || this.error) { + return; + } + + this.emit('end'); + } +} + +IncomingForm.DEFAULT_OPTIONS = DEFAULT_OPTIONS; +module.exports = IncomingForm; diff --git a/together/node_modules/formidable/src/FormidableError.js b/together/node_modules/formidable/src/FormidableError.js new file mode 100644 index 0000000..da5c25f --- /dev/null +++ b/together/node_modules/formidable/src/FormidableError.js @@ -0,0 +1,45 @@ +/* eslint-disable no-plusplus */ + +const missingPlugin = 1000; +const pluginFunction = 1001; +const aborted = 1002; +const noParser = 1003; +const uninitializedParser = 1004; +const filenameNotString = 1005; +const maxFieldsSizeExceeded = 1006; +const maxFieldsExceeded = 1007; +const smallerThanMinFileSize = 1008; +const biggerThanMaxFileSize = 1009; +const noEmptyFiles = 1010; +const missingContentType = 1011; +const malformedMultipart = 1012; +const missingMultipartBoundary = 1013; +const unknownTransferEncoding = 1014; + +const FormidableError = class extends Error { + constructor(message, internalCode, httpCode = 500) { + super(message); + this.code = internalCode; + this.httpCode = httpCode; + } +}; + +module.exports = { + missingPlugin, + pluginFunction, + aborted, + noParser, + uninitializedParser, + filenameNotString, + maxFieldsSizeExceeded, + maxFieldsExceeded, + smallerThanMinFileSize, + biggerThanMaxFileSize, + noEmptyFiles, + missingContentType, + malformedMultipart, + missingMultipartBoundary, + unknownTransferEncoding, + + FormidableError, +}; diff --git a/together/node_modules/formidable/src/PersistentFile.js b/together/node_modules/formidable/src/PersistentFile.js new file mode 100644 index 0000000..3a28aa7 --- /dev/null +++ b/together/node_modules/formidable/src/PersistentFile.js @@ -0,0 +1,87 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const fs = require('fs'); +const crypto = require('crypto'); +const { EventEmitter } = require('events'); + +class PersistentFile extends EventEmitter { + constructor({ filepath, newFilename, originalFilename, mimetype, hashAlgorithm }) { + super(); + + this.lastModifiedDate = null; + Object.assign(this, { filepath, newFilename, originalFilename, mimetype, hashAlgorithm }); + + this.size = 0; + this._writeStream = null; + + if (typeof this.hashAlgorithm === 'string') { + this.hash = crypto.createHash(this.hashAlgorithm); + } else { + this.hash = null; + } + } + + open() { + this._writeStream = new fs.WriteStream(this.filepath); + this._writeStream.on('error', (err) => { + this.emit('error', err); + }); + } + + toJSON() { + const json = { + size: this.size, + filepath: this.filepath, + newFilename: this.newFilename, + mimetype: this.mimetype, + mtime: this.lastModifiedDate, + length: this.length, + originalFilename: this.originalFilename, + }; + if (this.hash && this.hash !== '') { + json.hash = this.hash; + } + return json; + } + + toString() { + return `PersistentFile: ${this._file.newFilename}, Original: ${this._file.originalFilename}, Path: ${this._file.filepath}`; + } + + write(buffer, cb) { + if (this.hash) { + this.hash.update(buffer); + } + + if (this._writeStream.closed) { + cb(); + return; + } + + this._writeStream.write(buffer, () => { + this.lastModifiedDate = new Date(); + this.size += buffer.length; + this.emit('progress', this.size); + cb(); + }); + } + + end(cb) { + if (this.hash) { + this.hash = this.hash.digest('hex'); + } + this._writeStream.end(() => { + this.emit('end'); + cb(); + }); + } + + destroy() { + this._writeStream.destroy(); + fs.unlink(this.filepath, () => {}); + } +} + +module.exports = PersistentFile; diff --git a/together/node_modules/formidable/src/VolatileFile.js b/together/node_modules/formidable/src/VolatileFile.js new file mode 100644 index 0000000..01af428 --- /dev/null +++ b/together/node_modules/formidable/src/VolatileFile.js @@ -0,0 +1,82 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const crypto = require('crypto'); +const { EventEmitter } = require('events'); + +class VolatileFile extends EventEmitter { + constructor({ filepath, newFilename, originalFilename, mimetype, hashAlgorithm, createFileWriteStream }) { + super(); + + this.lastModifiedDate = null; + Object.assign(this, { filepath, newFilename, originalFilename, mimetype, hashAlgorithm, createFileWriteStream }); + + this.size = 0; + this._writeStream = null; + + if (typeof this.hashAlgorithm === 'string') { + this.hash = crypto.createHash(this.hashAlgorithm); + } else { + this.hash = null; + } + } + + open() { + this._writeStream = this.createFileWriteStream(this); + this._writeStream.on('error', (err) => { + this.emit('error', err); + }); + } + + destroy() { + this._writeStream.destroy(); + } + + toJSON() { + const json = { + size: this.size, + newFilename: this.newFilename, + length: this.length, + originalFilename: this.originalFilename, + mimetype: this.mimetype, + }; + if (this.hash && this.hash !== '') { + json.hash = this.hash; + } + return json; + } + + toString() { + return `VolatileFile: ${this.originalFilename}`; + } + + write(buffer, cb) { + if (this.hash) { + this.hash.update(buffer); + } + + if (this._writeStream.closed || this._writeStream.destroyed) { + cb(); + return; + } + + this._writeStream.write(buffer, () => { + this.size += buffer.length; + this.emit('progress', this.size); + cb(); + }); + } + + end(cb) { + if (this.hash) { + this.hash = this.hash.digest('hex'); + } + this._writeStream.end(() => { + this.emit('end'); + cb(); + }); + } +} + +module.exports = VolatileFile; diff --git a/together/node_modules/formidable/src/index.js b/together/node_modules/formidable/src/index.js new file mode 100644 index 0000000..bf555a9 --- /dev/null +++ b/together/node_modules/formidable/src/index.js @@ -0,0 +1,38 @@ +'use strict'; + +const PersistentFile = require('./PersistentFile'); +const VolatileFile = require('./VolatileFile'); +const Formidable = require('./Formidable'); +const FormidableError = require('./FormidableError'); + +const plugins = require('./plugins/index'); +const parsers = require('./parsers/index'); + +// make it available without requiring the `new` keyword +// if you want it access `const formidable.IncomingForm` as v1 +const formidable = (...args) => new Formidable(...args); + +module.exports = Object.assign(formidable, { + errors: FormidableError, + File: PersistentFile, + PersistentFile, + VolatileFile, + Formidable, + formidable, + + // alias + IncomingForm: Formidable, + + // parsers + ...parsers, + parsers, + + // misc + defaultOptions: Formidable.DEFAULT_OPTIONS, + enabledPlugins: Formidable.DEFAULT_OPTIONS.enabledPlugins, + + // plugins + plugins: { + ...plugins, + }, +}); diff --git a/together/node_modules/formidable/src/parsers/Dummy.js b/together/node_modules/formidable/src/parsers/Dummy.js new file mode 100644 index 0000000..6340959 --- /dev/null +++ b/together/node_modules/formidable/src/parsers/Dummy.js @@ -0,0 +1,21 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const { Transform } = require('stream'); + +class DummyParser extends Transform { + constructor(incomingForm, options = {}) { + super(); + this.globalOptions = { ...options }; + this.incomingForm = incomingForm; + } + + _flush(callback) { + this.incomingForm.ended = true; + this.incomingForm._maybeEnd(); + callback(); + } +} + +module.exports = DummyParser; diff --git a/together/node_modules/formidable/src/parsers/JSON.js b/together/node_modules/formidable/src/parsers/JSON.js new file mode 100644 index 0000000..9a096c2 --- /dev/null +++ b/together/node_modules/formidable/src/parsers/JSON.js @@ -0,0 +1,35 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const { Transform } = require('stream'); + +class JSONParser extends Transform { + constructor(options = {}) { + super({ readableObjectMode: true }); + this.chunks = []; + this.globalOptions = { ...options }; + } + + _transform(chunk, encoding, callback) { + this.chunks.push(String(chunk)); // todo consider using a string decoder + callback(); + } + + _flush(callback) { + try { + const fields = JSON.parse(this.chunks.join('')); + Object.keys(fields).forEach((key) => { + const value = fields[key]; + this.push({ key, value }); + }); + } catch (e) { + callback(e); + return; + } + this.chunks = null; + callback(); + } +} + +module.exports = JSONParser; diff --git a/together/node_modules/formidable/src/parsers/Multipart.js b/together/node_modules/formidable/src/parsers/Multipart.js new file mode 100644 index 0000000..23a298a --- /dev/null +++ b/together/node_modules/formidable/src/parsers/Multipart.js @@ -0,0 +1,347 @@ +/* eslint-disable no-fallthrough */ +/* eslint-disable no-bitwise */ +/* eslint-disable no-plusplus */ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const { Transform } = require('stream'); +const errors = require('../FormidableError.js'); + +const { FormidableError } = errors; + +let s = 0; +const STATE = { + PARSER_UNINITIALIZED: s++, + START: s++, + START_BOUNDARY: s++, + HEADER_FIELD_START: s++, + HEADER_FIELD: s++, + HEADER_VALUE_START: s++, + HEADER_VALUE: s++, + HEADER_VALUE_ALMOST_DONE: s++, + HEADERS_ALMOST_DONE: s++, + PART_DATA_START: s++, + PART_DATA: s++, + PART_END: s++, + END: s++, +}; + +let f = 1; +const FBOUNDARY = { PART_BOUNDARY: f, LAST_BOUNDARY: (f *= 2) }; + +const LF = 10; +const CR = 13; +const SPACE = 32; +const HYPHEN = 45; +const COLON = 58; +const A = 97; +const Z = 122; + +function lower(c) { + return c | 0x20; +} + +exports.STATES = {}; + +Object.keys(STATE).forEach((stateName) => { + exports.STATES[stateName] = STATE[stateName]; +}); + +class MultipartParser extends Transform { + constructor(options = {}) { + super({ readableObjectMode: true }); + this.boundary = null; + this.boundaryChars = null; + this.lookbehind = null; + this.bufferLength = 0; + this.state = STATE.PARSER_UNINITIALIZED; + + this.globalOptions = { ...options }; + this.index = null; + this.flags = 0; + } + + _flush(done) { + if ( + (this.state === STATE.HEADER_FIELD_START && this.index === 0) || + (this.state === STATE.PART_DATA && this.index === this.boundary.length) + ) { + this._handleCallback('partEnd'); + this._handleCallback('end'); + done(); + } else if (this.state !== STATE.END) { + done( + new FormidableError( + `MultipartParser.end(): stream ended unexpectedly: ${this.explain()}`, + errors.malformedMultipart, + 400, + ), + ); + } + } + + initWithBoundary(str) { + this.boundary = Buffer.from(`\r\n--${str}`); + this.lookbehind = Buffer.alloc(this.boundary.length + 8); + this.state = STATE.START; + this.boundaryChars = {}; + + for (let i = 0; i < this.boundary.length; i++) { + this.boundaryChars[this.boundary[i]] = true; + } + } + + // eslint-disable-next-line max-params + _handleCallback(name, buf, start, end) { + if (start !== undefined && start === end) { + return; + } + this.push({ name, buffer: buf, start, end }); + } + + // eslint-disable-next-line max-statements + _transform(buffer, _, done) { + let i = 0; + let prevIndex = this.index; + let { index, state, flags } = this; + const { lookbehind, boundary, boundaryChars } = this; + const boundaryLength = boundary.length; + const boundaryEnd = boundaryLength - 1; + this.bufferLength = buffer.length; + let c = null; + let cl = null; + + const setMark = (name, idx) => { + this[`${name}Mark`] = typeof idx === 'number' ? idx : i; + }; + + const clearMarkSymbol = (name) => { + delete this[`${name}Mark`]; + }; + + const dataCallback = (name, shouldClear) => { + const markSymbol = `${name}Mark`; + if (!(markSymbol in this)) { + return; + } + + if (!shouldClear) { + this._handleCallback(name, buffer, this[markSymbol], buffer.length); + setMark(name, 0); + } else { + this._handleCallback(name, buffer, this[markSymbol], i); + clearMarkSymbol(name); + } + }; + + for (i = 0; i < this.bufferLength; i++) { + c = buffer[i]; + switch (state) { + case STATE.PARSER_UNINITIALIZED: + return i; + case STATE.START: + index = 0; + state = STATE.START_BOUNDARY; + case STATE.START_BOUNDARY: + if (index === boundary.length - 2) { + if (c === HYPHEN) { + flags |= FBOUNDARY.LAST_BOUNDARY; + } else if (c !== CR) { + return i; + } + index++; + break; + } else if (index - 1 === boundary.length - 2) { + if (flags & FBOUNDARY.LAST_BOUNDARY && c === HYPHEN) { + this._handleCallback('end'); + state = STATE.END; + flags = 0; + } else if (!(flags & FBOUNDARY.LAST_BOUNDARY) && c === LF) { + index = 0; + this._handleCallback('partBegin'); + state = STATE.HEADER_FIELD_START; + } else { + return i; + } + break; + } + + if (c !== boundary[index + 2]) { + index = -2; + } + if (c === boundary[index + 2]) { + index++; + } + break; + case STATE.HEADER_FIELD_START: + state = STATE.HEADER_FIELD; + setMark('headerField'); + index = 0; + case STATE.HEADER_FIELD: + if (c === CR) { + clearMarkSymbol('headerField'); + state = STATE.HEADERS_ALMOST_DONE; + break; + } + + index++; + if (c === HYPHEN) { + break; + } + + if (c === COLON) { + if (index === 1) { + // empty header field + return i; + } + dataCallback('headerField', true); + state = STATE.HEADER_VALUE_START; + break; + } + + cl = lower(c); + if (cl < A || cl > Z) { + return i; + } + break; + case STATE.HEADER_VALUE_START: + if (c === SPACE) { + break; + } + + setMark('headerValue'); + state = STATE.HEADER_VALUE; + case STATE.HEADER_VALUE: + if (c === CR) { + dataCallback('headerValue', true); + this._handleCallback('headerEnd'); + state = STATE.HEADER_VALUE_ALMOST_DONE; + } + break; + case STATE.HEADER_VALUE_ALMOST_DONE: + if (c !== LF) { + return i; + } + state = STATE.HEADER_FIELD_START; + break; + case STATE.HEADERS_ALMOST_DONE: + if (c !== LF) { + return i; + } + + this._handleCallback('headersEnd'); + state = STATE.PART_DATA_START; + break; + case STATE.PART_DATA_START: + state = STATE.PART_DATA; + setMark('partData'); + case STATE.PART_DATA: + prevIndex = index; + + if (index === 0) { + // boyer-moore derrived algorithm to safely skip non-boundary data + i += boundaryEnd; + while (i < this.bufferLength && !(buffer[i] in boundaryChars)) { + i += boundaryLength; + } + i -= boundaryEnd; + c = buffer[i]; + } + + if (index < boundary.length) { + if (boundary[index] === c) { + if (index === 0) { + dataCallback('partData', true); + } + index++; + } else { + index = 0; + } + } else if (index === boundary.length) { + index++; + if (c === CR) { + // CR = part boundary + flags |= FBOUNDARY.PART_BOUNDARY; + } else if (c === HYPHEN) { + // HYPHEN = end boundary + flags |= FBOUNDARY.LAST_BOUNDARY; + } else { + index = 0; + } + } else if (index - 1 === boundary.length) { + if (flags & FBOUNDARY.PART_BOUNDARY) { + index = 0; + if (c === LF) { + // unset the PART_BOUNDARY flag + flags &= ~FBOUNDARY.PART_BOUNDARY; + this._handleCallback('partEnd'); + this._handleCallback('partBegin'); + state = STATE.HEADER_FIELD_START; + break; + } + } else if (flags & FBOUNDARY.LAST_BOUNDARY) { + if (c === HYPHEN) { + this._handleCallback('partEnd'); + this._handleCallback('end'); + state = STATE.END; + flags = 0; + } else { + index = 0; + } + } else { + index = 0; + } + } + + if (index > 0) { + // when matching a possible boundary, keep a lookbehind reference + // in case it turns out to be a false lead + lookbehind[index - 1] = c; + } else if (prevIndex > 0) { + // if our boundary turned out to be rubbish, the captured lookbehind + // belongs to partData + this._handleCallback('partData', lookbehind, 0, prevIndex); + prevIndex = 0; + setMark('partData'); + + // reconsider the current character even so it interrupted the sequence + // it could be the beginning of a new sequence + i--; + } + + break; + case STATE.END: + break; + default: + return i; + } + } + + dataCallback('headerField'); + dataCallback('headerValue'); + dataCallback('partData'); + + this.index = index; + this.state = state; + this.flags = flags; + + done(); + return this.bufferLength; + } + + explain() { + return `state = ${MultipartParser.stateToString(this.state)}`; + } +} + +// eslint-disable-next-line consistent-return +MultipartParser.stateToString = (stateNumber) => { + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const stateName in STATE) { + const number = STATE[stateName]; + if (number === stateNumber) return stateName; + } +}; + +module.exports = Object.assign(MultipartParser, { STATES: exports.STATES }); diff --git a/together/node_modules/formidable/src/parsers/OctetStream.js b/together/node_modules/formidable/src/parsers/OctetStream.js new file mode 100644 index 0000000..cdf55f2 --- /dev/null +++ b/together/node_modules/formidable/src/parsers/OctetStream.js @@ -0,0 +1,12 @@ +'use strict'; + +const { PassThrough } = require('stream'); + +class OctetStreamParser extends PassThrough { + constructor(options = {}) { + super(); + this.globalOptions = { ...options }; + } +} + +module.exports = OctetStreamParser; diff --git a/together/node_modules/formidable/src/parsers/Querystring.js b/together/node_modules/formidable/src/parsers/Querystring.js new file mode 100644 index 0000000..a0d4243 --- /dev/null +++ b/together/node_modules/formidable/src/parsers/Querystring.js @@ -0,0 +1,38 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const { Transform } = require('stream'); +const querystring = require('querystring'); + +// This is a buffering parser, not quite as nice as the multipart one. +// If I find time I'll rewrite this to be fully streaming as well +class QuerystringParser extends Transform { + constructor(options = {}) { + super({ readableObjectMode: true }); + this.globalOptions = { ...options }; + this.buffer = ''; + this.bufferLength = 0; + } + + _transform(buffer, encoding, callback) { + this.buffer += buffer.toString('ascii'); + this.bufferLength = this.buffer.length; + callback(); + } + + _flush(callback) { + const fields = querystring.parse(this.buffer, '&', '='); + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const key in fields) { + this.push({ + key, + value: fields[key], + }); + } + this.buffer = ''; + callback(); + } +} + +module.exports = QuerystringParser; diff --git a/together/node_modules/formidable/src/parsers/StreamingQuerystring.js b/together/node_modules/formidable/src/parsers/StreamingQuerystring.js new file mode 100644 index 0000000..06d7577 --- /dev/null +++ b/together/node_modules/formidable/src/parsers/StreamingQuerystring.js @@ -0,0 +1,121 @@ +// not used +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const { Transform } = require('stream'); +const errors = require('../FormidableError.js'); + +const { FormidableError } = errors; + +const AMPERSAND = 38; +const EQUALS = 61; + +class QuerystringParser extends Transform { + constructor(options = {}) { + super({ readableObjectMode: true }); + + const { maxFieldSize } = options; + this.maxFieldLength = maxFieldSize; + this.buffer = Buffer.from(''); + this.fieldCount = 0; + this.sectionStart = 0; + this.key = ''; + this.readingKey = true; + } + + _transform(buffer, encoding, callback) { + let len = buffer.length; + if (this.buffer && this.buffer.length) { + // we have some data left over from the last write which we are in the middle of processing + len += this.buffer.length; + buffer = Buffer.concat([this.buffer, buffer], len); + } + + for (let i = this.buffer.length || 0; i < len; i += 1) { + const c = buffer[i]; + if (this.readingKey) { + // KEY, check for = + if (c === EQUALS) { + this.key = this.getSection(buffer, i); + this.readingKey = false; + this.sectionStart = i + 1; + } else if (c === AMPERSAND) { + // just key, no value. Prepare to read another key + this.emitField(this.getSection(buffer, i)); + this.sectionStart = i + 1; + } + // VALUE, check for & + } else if (c === AMPERSAND) { + this.emitField(this.key, this.getSection(buffer, i)); + this.sectionStart = i + 1; + } + + if ( + this.maxFieldLength && + i - this.sectionStart === this.maxFieldLength + ) { + callback( + new FormidableError( + `${ + this.readingKey ? 'Key' : `Value for ${this.key}` + } longer than maxFieldLength`, + ), + errors.maxFieldsSizeExceeded, + 413, + ); + } + } + + // Prepare the remaining key or value (from sectionStart to the end) for the next write() or for end() + len -= this.sectionStart; + if (len) { + // i.e. Unless the last character was a & or = + this.buffer = Buffer.from(this.buffer, 0, this.sectionStart); + } else this.buffer = null; + + this.sectionStart = 0; + callback(); + } + + _flush(callback) { + // Emit the last field + if (this.readingKey) { + // we only have a key if there's something in the buffer. We definitely have no value + if (this.buffer && this.buffer.length){ + this.emitField(this.buffer.toString('ascii')); + } + } else { + // We have a key, we may or may not have a value + this.emitField( + this.key, + this.buffer && this.buffer.length && this.buffer.toString('ascii'), + ); + } + this.buffer = ''; + callback(); + } + + getSection(buffer, i) { + if (i === this.sectionStart) return ''; + + return buffer.toString('ascii', this.sectionStart, i); + } + + emitField(key, val) { + this.key = ''; + this.readingKey = true; + this.push({ key, value: val || '' }); + } +} + +module.exports = QuerystringParser; + +// const q = new QuerystringParser({maxFieldSize: 100}); +// (async function() { +// for await (const chunk of q) { +// console.log(chunk); +// } +// })(); +// q.write("a=b&c=d") +// q.end() diff --git a/together/node_modules/formidable/src/parsers/index.js b/together/node_modules/formidable/src/parsers/index.js new file mode 100644 index 0000000..bbf9ef6 --- /dev/null +++ b/together/node_modules/formidable/src/parsers/index.js @@ -0,0 +1,17 @@ +'use strict'; + +const JSONParser = require('./JSON'); +const DummyParser = require('./Dummy'); +const MultipartParser = require('./Multipart'); +const OctetStreamParser = require('./OctetStream'); +const QueryStringParser = require('./Querystring'); + +Object.assign(exports, { + JSONParser, + DummyParser, + MultipartParser, + OctetStreamParser, + OctetstreamParser: OctetStreamParser, + QueryStringParser, + QuerystringParser: QueryStringParser, +}); diff --git a/together/node_modules/formidable/src/plugins/index.js b/together/node_modules/formidable/src/plugins/index.js new file mode 100644 index 0000000..cbd491a --- /dev/null +++ b/together/node_modules/formidable/src/plugins/index.js @@ -0,0 +1,13 @@ +'use strict'; + +const octetstream = require('./octetstream'); +const querystring = require('./querystring'); +const multipart = require('./multipart'); +const json = require('./json'); + +Object.assign(exports, { + octetstream, + querystring, + multipart, + json, +}); diff --git a/together/node_modules/formidable/src/plugins/json.js b/together/node_modules/formidable/src/plugins/json.js new file mode 100644 index 0000000..20d3b26 --- /dev/null +++ b/together/node_modules/formidable/src/plugins/json.js @@ -0,0 +1,38 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const JSONParser = require('../parsers/JSON'); + +// the `options` is also available through the `this.options` / `formidable.options` +module.exports = function plugin(formidable, options) { + // the `this` context is always formidable, as the first argument of a plugin + // but this allows us to customize/test each plugin + + /* istanbul ignore next */ + const self = this || formidable; + + if (/json/i.test(self.headers['content-type'])) { + init.call(self, self, options); + } +}; + +// Note that it's a good practice (but it's up to you) to use the `this.options` instead +// of the passed `options` (second) param, because when you decide +// to test the plugin you can pass custom `this` context to it (and so `this.options`) +function init(_self, _opts) { + this.type = 'json'; + + const parser = new JSONParser(this.options); + + parser.on('data', ({ key, value }) => { + this.emit('field', key, value); + }); + + parser.once('end', () => { + this.ended = true; + this._maybeEnd(); + }); + + this._parser = parser; +} diff --git a/together/node_modules/formidable/src/plugins/multipart.js b/together/node_modules/formidable/src/plugins/multipart.js new file mode 100644 index 0000000..ba07373 --- /dev/null +++ b/together/node_modules/formidable/src/plugins/multipart.js @@ -0,0 +1,173 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const { Stream } = require('stream'); +const MultipartParser = require('../parsers/Multipart'); +const errors = require('../FormidableError.js'); + +const { FormidableError } = errors; + +// the `options` is also available through the `options` / `formidable.options` +module.exports = function plugin(formidable, options) { + // the `this` context is always formidable, as the first argument of a plugin + // but this allows us to customize/test each plugin + + /* istanbul ignore next */ + const self = this || formidable; + + // NOTE: we (currently) support both multipart/form-data and multipart/related + const multipart = /multipart/i.test(self.headers['content-type']); + + if (multipart) { + const m = self.headers['content-type'].match( + /boundary=(?:"([^"]+)"|([^;]+))/i, + ); + if (m) { + const initMultipart = createInitMultipart(m[1] || m[2]); + initMultipart.call(self, self, options); // lgtm [js/superfluous-trailing-arguments] + } else { + const err = new FormidableError( + 'bad content-type header, no multipart boundary', + errors.missingMultipartBoundary, + 400, + ); + self._error(err); + } + } +}; + +// Note that it's a good practice (but it's up to you) to use the `this.options` instead +// of the passed `options` (second) param, because when you decide +// to test the plugin you can pass custom `this` context to it (and so `this.options`) +function createInitMultipart(boundary) { + return function initMultipart() { + this.type = 'multipart'; + + const parser = new MultipartParser(this.options); + let headerField; + let headerValue; + let part; + + parser.initWithBoundary(boundary); + + // eslint-disable-next-line max-statements, consistent-return + parser.on('data', ({ name, buffer, start, end }) => { + if (name === 'partBegin') { + part = new Stream(); + part.readable = true; + part.headers = {}; + part.name = null; + part.originalFilename = null; + part.mimetype = null; + + part.transferEncoding = this.options.encoding; + part.transferBuffer = ''; + + headerField = ''; + headerValue = ''; + } else if (name === 'headerField') { + headerField += buffer.toString(this.options.encoding, start, end); + } else if (name === 'headerValue') { + headerValue += buffer.toString(this.options.encoding, start, end); + } else if (name === 'headerEnd') { + headerField = headerField.toLowerCase(); + part.headers[headerField] = headerValue; + + // matches either a quoted-string or a token (RFC 2616 section 19.5.1) + const m = headerValue.match( + // eslint-disable-next-line no-useless-escape + /\bname=("([^"]*)"|([^\(\)<>@,;:\\"\/\[\]\?=\{\}\s\t/]+))/i, + ); + if (headerField === 'content-disposition') { + if (m) { + part.name = m[2] || m[3] || ''; + } + + part.originalFilename = this._getFileName(headerValue); + } else if (headerField === 'content-type') { + part.mimetype = headerValue; + } else if (headerField === 'content-transfer-encoding') { + part.transferEncoding = headerValue.toLowerCase(); + } + + headerField = ''; + headerValue = ''; + } else if (name === 'headersEnd') { + switch (part.transferEncoding) { + case 'binary': + case '7bit': + case '8bit': + case 'utf-8': { + const dataPropagation = (ctx) => { + if (ctx.name === 'partData') { + part.emit('data', ctx.buffer.slice(ctx.start, ctx.end)); + } + }; + const dataStopPropagation = (ctx) => { + if (ctx.name === 'partEnd') { + part.emit('end'); + parser.off('data', dataPropagation); + parser.off('data', dataStopPropagation); + } + }; + parser.on('data', dataPropagation); + parser.on('data', dataStopPropagation); + break; + } + case 'base64': { + const dataPropagation = (ctx) => { + if (ctx.name === 'partData') { + part.transferBuffer += ctx.buffer + .slice(ctx.start, ctx.end) + .toString('ascii'); + + /* + four bytes (chars) in base64 converts to three bytes in binary + encoding. So we should always work with a number of bytes that + can be divided by 4, it will result in a number of buytes that + can be divided vy 3. + */ + const offset = parseInt(part.transferBuffer.length / 4, 10) * 4; + part.emit( + 'data', + Buffer.from( + part.transferBuffer.substring(0, offset), + 'base64', + ), + ); + part.transferBuffer = part.transferBuffer.substring(offset); + } + }; + const dataStopPropagation = (ctx) => { + if (ctx.name === 'partEnd') { + part.emit('data', Buffer.from(part.transferBuffer, 'base64')); + part.emit('end'); + parser.off('data', dataPropagation); + parser.off('data', dataStopPropagation); + } + }; + parser.on('data', dataPropagation); + parser.on('data', dataStopPropagation); + break; + } + default: + return this._error( + new FormidableError( + 'unknown transfer-encoding', + errors.unknownTransferEncoding, + 501, + ), + ); + } + + this.onPart(part); + } else if (name === 'end') { + this.ended = true; + this._maybeEnd(); + } + }); + + this._parser = parser; + }; +} diff --git a/together/node_modules/formidable/src/plugins/octetstream.js b/together/node_modules/formidable/src/plugins/octetstream.js new file mode 100644 index 0000000..96fee40 --- /dev/null +++ b/together/node_modules/formidable/src/plugins/octetstream.js @@ -0,0 +1,86 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const OctetStreamParser = require('../parsers/OctetStream'); + +// the `options` is also available through the `options` / `formidable.options` +module.exports = function plugin(formidable, options) { + // the `this` context is always formidable, as the first argument of a plugin + // but this allows us to customize/test each plugin + + /* istanbul ignore next */ + const self = this || formidable; + + if (/octet-stream/i.test(self.headers['content-type'])) { + init.call(self, self, options); + } + + return self; +}; + +// Note that it's a good practice (but it's up to you) to use the `this.options` instead +// of the passed `options` (second) param, because when you decide +// to test the plugin you can pass custom `this` context to it (and so `this.options`) +function init(_self, _opts) { + this.type = 'octet-stream'; + const originalFilename = this.headers['x-file-name']; + const mimetype = this.headers['content-type']; + + const thisPart = { + originalFilename, + mimetype, + }; + const newFilename = this._getNewName(thisPart); + const filepath = this._joinDirectoryName(newFilename); + const file = this._newFile({ + newFilename, + filepath, + originalFilename, + mimetype, + }); + + this.emit('fileBegin', originalFilename, file); + file.open(); + this.openedFiles.push(file); + this._flushing += 1; + + this._parser = new OctetStreamParser(this.options); + + // Keep track of writes that haven't finished so we don't emit the file before it's done being written + let outstandingWrites = 0; + + this._parser.on('data', (buffer) => { + this.pause(); + outstandingWrites += 1; + + file.write(buffer, () => { + outstandingWrites -= 1; + this.resume(); + + if (this.ended) { + this._parser.emit('doneWritingFile'); + } + }); + }); + + this._parser.on('end', () => { + this._flushing -= 1; + this.ended = true; + + const done = () => { + file.end(() => { + this.emit('file', 'file', file); + this._maybeEnd(); + }); + }; + + if (outstandingWrites === 0) { + done(); + } else { + this._parser.once('doneWritingFile', done); + } + }); + + return this; +} diff --git a/together/node_modules/formidable/src/plugins/querystring.js b/together/node_modules/formidable/src/plugins/querystring.js new file mode 100644 index 0000000..c9dcf1e --- /dev/null +++ b/together/node_modules/formidable/src/plugins/querystring.js @@ -0,0 +1,42 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const QuerystringParser = require('../parsers/Querystring'); + +// the `options` is also available through the `this.options` / `formidable.options` +module.exports = function plugin(formidable, options) { + // the `this` context is always formidable, as the first argument of a plugin + // but this allows us to customize/test each plugin + + /* istanbul ignore next */ + const self = this || formidable; + + if (/urlencoded/i.test(self.headers['content-type'])) { + init.call(self, self, options); + } + + return self; +}; + +// Note that it's a good practice (but it's up to you) to use the `this.options` instead +// of the passed `options` (second) param, because when you decide +// to test the plugin you can pass custom `this` context to it (and so `this.options`) +function init(_self, _opts) { + this.type = 'urlencoded'; + + const parser = new QuerystringParser(this.options); + + parser.on('data', ({ key, value }) => { + this.emit('field', key, value); + }); + + parser.once('end', () => { + this.ended = true; + this._maybeEnd(); + }); + + this._parser = parser; + + return this; +} -- cgit