diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a3fd634..b6774b00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ * fix(tests): include multipart and qs parser unit tests, part of [#415](https://github.com/node-formidable/node-formidable/issues/415) * fix: reorganize exports + move parsers to `src/parsers/` * fix: update docs and examples [#544](https://github.com/node-formidable/node-formidable/pull/544) ([#248](https://github.com/node-formidable/node-formidable/issues/248), [#335](https://github.com/node-formidable/node-formidable/issues/335), [#371](https://github.com/node-formidable/node-formidable/issues/371), [#372](https://github.com/node-formidable/node-formidable/issues/372), [#387](https://github.com/node-formidable/node-formidable/issues/387), partly [#471](https://github.com/node-formidable/node-formidable/issues/471), [#535](https://github.com/node-formidable/node-formidable/issues/535)) + * feat: introduce Plugins API, fix silent failing tests ([#545](https://github.com/node-formidable/node-formidable/pull/545), [#391](https://github.com/node-formidable/node-formidable/pull/391), [#407](https://github.com/node-formidable/node-formidable/pull/407), [#386](https://github.com/node-formidable/node-formidable/pull/386), [#374](https://github.com/node-formidable/node-formidable/pull/374), [#521](https://github.com/node-formidable/node-formidable/pull/521), [#267](https://github.com/node-formidable/node-formidable/pull/267)) + * respect form hash option on incoming octect/stream requests ([#407](https://github.com/node-formidable/node-formidable/pull/407)) * fix: exposing file writable stream errors ([#520](https://github.com/node-formidable/node-formidable/pull/520), [#316](https://github.com/node-formidable/node-formidable/pull/316), [#469](https://github.com/node-formidable/node-formidable/pull/469), [#470](https://github.com/node-formidable/node-formidable/pull/470)) ### v1.2.1 (2018-03-20) diff --git a/README.md b/README.md index d1e09ab9..73a84023 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,68 @@ form.on('data', ({ name, key, value, buffer, start, end, ...more }) => { }); ``` +### .use(plugin: Plugin) + +A method that allows you to extend the Formidable library. By default we include +4 plugins, which esentially are adapters to plug the different built-in parsers. + +**The plugins added by this method are always enabled.** + +_See [src/plugins/](./src/plugins/) for more detailed look on default plugins._ + +The `plugin` param has such signature: + +```typescript +function(formidable: Formidable, options: Options): void; +``` + +The architecture is simple. The `plugin` is a function that is passed with the +Formidable instance (the `form` across the README examples) and the options. + +**Note:** the plugin function's `this` context is also the same instance. + +```js +const formidable = require('formidable'); + +const form = formidable({ keepExtensions: true }); + +form.use((self, options) => { + // self === this === form + console.log('woohoo, custom plugin'); + // do your stuff; check `src/plugins` for inspiration +}); + +form.parse(req, (error, fields, files) => { + console.log('done!'); +}); +``` + +**Important to note**, is that inside plugin `this.options`, `self.options` and +`options` MAY or MAY NOT be the same. General best practice is to always use the +`this`, so you can later test your plugin independently and more easily. + +If you want to disable some parsing capabilities of Formidable, you can disable +the plugin which corresponds to the parser. For example, if you want to disable +multipart parsing (so the [src/parsers/Multipart.js](./src/parsers/Multipart.js) +which is used in [src/plugins/multipart.js](./src/plugins/multipart.js)), then +you can remove it from the `options.enabledPlugins`, like so + +```js +const { Formidable } = require('formidable'); + +const form = new Formidable({ + hash: 'sha1', + enabledPlugins: ['octetstream', 'querystring', 'json'], +}); +``` + +**Be aware** that the order _MAY_ be important too. The names corresponds 1:1 to +files in [src/plugins/](./src/plugins) folder. + +Pull requests for new built-in plugins MAY be accepted - for example, more +advanced querystring parser. Add your plugin as a new file in `src/plugins/` +folder (lowercased) and follow how the other plugins are made. + ### form.onPart If you want to use Formidable to only handle certain parts for you, you can do @@ -509,4 +571,5 @@ Formidable is licensed under the [MIT License][license-url]. [npm-monthly-img]: https://badgen.net/npm/dm/formidable?icon=npm&cache=300 [npm-yearly-img]: https://badgen.net/npm/dy/formidable?icon=npm&cache=300 [npm-alltime-img]: https://badgen.net/npm/dt/formidable?icon=npm&cache=300 + diff --git a/package.json b/package.json index 72466535..b5789a9a 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,12 @@ "license": "MIT", "description": "A node.js module for parsing form data, especially file uploads.", "homepage": "https://github.com/node-formidable/node-formidable", - "funding": "https://ko-fi.com/tunnckoCore/commissions", + "funding": "https://tidelift.com/funding/github/npm/formidable", "repository": "node-formidable/node-formidable", "main": "./src/index.js", "files": [ - "src" + "src", + "test" ], "publishConfig": { "access": "public", @@ -26,6 +27,10 @@ "pretest:ci": "yarn pretest", "test:ci": "nyc node test/run.js" }, + "dependencies": { + "dezalgo": "^1.0.3", + "once": "^1.4.0" + }, "devDependencies": { "@commitlint/cli": "^8.3.5", "@commitlint/config-conventional": "^8.3.4", @@ -37,11 +42,13 @@ "eslint-plugin-prettier": "^3.1.2", "husky": "^4.2.2", "jest": "^25.1.0", + "koa": "^2.11.0", "lint-staged": "^10.0.7", "nyc": "^15.0.0", "prettier": "^1.19.1", "prettier-plugin-pkgjson": "^0.2.3", "request": "^2.88.2", + "supertest": "^4.0.2", "urun": "^0.0.8", "utest": "^0.0.8" }, diff --git a/src/Formidable.js b/src/Formidable.js index 84aa7b69..15548caa 100644 --- a/src/Formidable.js +++ b/src/Formidable.js @@ -7,7 +7,8 @@ const os = require('os'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); -const { Stream } = require('stream'); +const once = require('once'); +const dezalgo = require('dezalgo'); const { EventEmitter } = require('events'); const { StringDecoder } = require('string_decoder'); @@ -19,16 +20,12 @@ const DEFAULT_OPTIONS = { encoding: 'utf-8', hash: false, multiples: false, + enabledPlugins: ['octetstream', 'querystring', 'multipart', 'json'], }; const File = require('./File'); - -/** Parsers */ -const JSONParser = require('./parsers/JSON'); const DummyParser = require('./parsers/Dummy'); -const OctetParser = require('./parsers/OctetStream'); const MultipartParser = require('./parsers/Multipart'); -const QuerystringParser = require('./parsers/Querystring'); function hasOwnProp(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); @@ -40,7 +37,7 @@ class IncomingForm extends EventEmitter { this.error = null; this.ended = false; - Object.assign(this, DEFAULT_OPTIONS, options); + this.options = { ...DEFAULT_OPTIONS, ...options }; this.uploadDir = this.uploadDir || os.tmpdir(); this.headers = null; @@ -53,7 +50,32 @@ class IncomingForm extends EventEmitter { this._flushing = 0; this._fieldsSize = 0; this._fileSize = 0; + this._plugins = []; this.openedFiles = []; + + const enabledPlugins = [] + .concat(this.options.enabledPlugins) + .filter(Boolean); + + if (enabledPlugins.length === 0) { + throw new Error( + 'expect at least 1 enabled builtin plugin, see options.enabledPlugins', + ); + } + + this.options.enabledPlugins.forEach((pluginName) => { + const plgName = pluginName.toLowerCase(); + // eslint-disable-next-line import/no-dynamic-require, global-require + this.use(require(path.join(__dirname, 'plugins', `${plgName}.js`))); + }); + } + + use(plugin) { + if (typeof plugin !== 'function') { + throw new Error('.use: expect `plugin` to be a function'); + } + this._plugins.push(plugin.bind(this)); + return this; } parse(req, cb) { @@ -88,12 +110,13 @@ class IncomingForm extends EventEmitter { // Setup callback first, so we don't miss anything from data events emitted immediately. if (cb) { + const callback = once(dezalgo(cb)); const fields = {}; const files = {}; this.on('field', (name, value) => { // TODO: too much nesting - if (this.multiples && name.slice(-2) === '[]') { + if (this.options.multiples && name.slice(-2) === '[]') { const realName = name.slice(0, name.length - 2); if (hasOwnProp(fields, realName)) { if (!Array.isArray(fields[realName])) { @@ -113,7 +136,7 @@ class IncomingForm extends EventEmitter { }); this.on('file', (name, file) => { // TODO: too much nesting - if (this.multiples) { + if (this.options.multiples) { if (hasOwnProp(files, name)) { if (!Array.isArray(files[name])) { files[name] = [files[name]]; @@ -132,10 +155,10 @@ class IncomingForm extends EventEmitter { // } }); this.on('error', (err) => { - cb(err, fields, files); + callback(err, fields, files); }); this.on('end', () => { - cb(null, fields, files); + callback(null, fields, files); }); } @@ -162,8 +185,10 @@ class IncomingForm extends EventEmitter { if (this.error) { return; } - - this._parser.end(); + if (this._parser) { + this._parser.end(); + } + this._maybeEnd(); }); return this; @@ -173,6 +198,12 @@ class IncomingForm extends EventEmitter { this.headers = headers; this._parseContentLength(); this._parseContentType(); + + if (!this._parser) { + this._error(new Error('not parser found')); + return; + } + this._parser.once('error', (error) => { this._error(error); }); @@ -207,10 +238,10 @@ class IncomingForm extends EventEmitter { onPart(part) { // this method can be overwritten by the user - this.handlePart(part); + this._handlePart(part); } - handlePart(part) { + _handlePart(part) { if (part.filename && typeof part.filename !== 'string') { this._error(new Error(`the part.filename should be string when exists`)); return; @@ -228,14 +259,16 @@ class IncomingForm extends EventEmitter { // ? NOTE(@tunnckocore): filename is an empty string when a field? if (!part.mime) { let value = ''; - const decoder = new StringDecoder(part.transferEncoding || this.encoding); + const decoder = new StringDecoder( + part.transferEncoding || this.options.encoding, + ); part.on('data', (buffer) => { this._fieldsSize += buffer.length; - if (this._fieldsSize > this.maxFieldsSize) { + if (this._fieldsSize > this.options.maxFieldsSize) { this._error( new Error( - `maxFieldsSize exceeded, received ${this._fieldsSize} bytes of field data`, + `options.maxFieldsSize exceeded, received ${this._fieldsSize} bytes of field data`, ), ); return; @@ -255,7 +288,7 @@ class IncomingForm extends EventEmitter { path: this._uploadPath(part.filename), name: part.filename, type: part.mime, - hash: this.hash, + hash: this.options.hash, }); file.on('error', (err) => { this._error(err); @@ -267,10 +300,10 @@ class IncomingForm extends EventEmitter { part.on('data', (buffer) => { this._fileSize += buffer.length; - if (this._fileSize > this.maxFileSize) { + if (this._fileSize > this.options.maxFileSize) { this._error( new Error( - `maxFileSize exceeded, received ${this._fileSize} bytes of file data`, + `options.maxFileSize exceeded, received ${this._fileSize} bytes of file data`, ), ); return; @@ -296,7 +329,7 @@ class IncomingForm extends EventEmitter { // eslint-disable-next-line max-statements _parseContentType() { if (this.bytesExpected === 0) { - this._parser = new DummyParser(this); + this._parser = new DummyParser(this, this.options); return; } @@ -305,49 +338,57 @@ class IncomingForm extends EventEmitter { return; } - if (this.headers['content-type'].match(/octet-stream/i)) { - this._initOctetStream(); - return; - } + const results = []; + const _dummyParser = new DummyParser(this, this.options); - if (this.headers['content-type'].match(/urlencoded/i)) { - this._initUrlencoded(); - return; - } + // eslint-disable-next-line no-plusplus + for (let idx = 0; idx < this._plugins.length; idx++) { + const plugin = this._plugins[idx]; - if (this.headers['content-type'].match(/multipart/i)) { - const m = this.headers['content-type'].match( - /boundary=(?:"([^"]+)"|([^;]+))/i, - ); - if (m) { - this._initMultipart(m[1] || m[2]); - } else { - this._error( - new Error('bad content-type header, no multipart boundary'), + let pluginReturn = null; + + try { + pluginReturn = plugin(this, this.options) || this; + } catch (err) { + // directly throw from the `form.parse` method; + // there is no other better way, except a handle through options + const error = new Error( + `plugin on index ${idx} failed with: ${err.message}`, ); + error.idx = idx; + throw error; } - return; - } - if (this.headers['content-type'].match(/json/i)) { - this._initJSONencoded(); - return; + Object.assign(this, pluginReturn); + + // todo: use Set/Map and pass plugin name instead of the `idx` index + this.emit('plugin', idx, pluginReturn); + results.push(pluginReturn); } - this._error( - new Error( - `bad content-type header, unknown content-type: ${this.headers['content-type']}`, - ), - ); + this.emit('pluginsResults', results); + + // NOTE: probably not needed, because we check options.enabledPlugins in the constructor + // if (results.length === 0 /* && results.length !== this._plugins.length */) { + // this._error( + // new Error( + // `bad content-type header, unknown content-type: ${this.headers['content-type']}`, + // ), + // ); + // } } - _error(err) { + _error(err, eventName = 'error') { + // if (!err && this.error) { + // this.emit('error', this.error); + // return; + // } if (this.error || this.ended) { return; } this.error = err; - this.emit('error', err); + this.emit(eventName, err); if (Array.isArray(this.openedFiles)) { this.openedFiles.forEach((file) => { @@ -371,133 +412,10 @@ class IncomingForm extends EventEmitter { } _newParser() { - return new MultipartParser(); + return new MultipartParser(this.options); } - _initMultipart(boundary) { - this.type = 'multipart'; - - const parser = new MultipartParser(); - let headerField; - let headerValue; - let part; - - parser.initWithBoundary(boundary); - - // eslint-disable-next-line max-statements, consistent-return - parser.on('data', ({ name, buffer, start, end }) => { - if (name === 'partBegin') { - part = new Stream(); - part.readable = true; - part.headers = {}; - part.name = null; - part.filename = null; - part.mime = null; - - part.transferEncoding = 'binary'; - part.transferBuffer = ''; - - headerField = ''; - headerValue = ''; - } else if (name === 'headerField') { - headerField += buffer.toString(this.encoding, start, end); - } else if (name === 'headerValue') { - headerValue += buffer.toString(this.encoding, start, end); - } else if (name === 'headerEnd') { - headerField = headerField.toLowerCase(); - part.headers[headerField] = headerValue; - - // matches either a quoted-string or a token (RFC 2616 section 19.5.1) - const m = headerValue.match( - // eslint-disable-next-line no-useless-escape - /\bname=("([^"]*)"|([^\(\)<>@,;:\\"\/\[\]\?=\{\}\s\t/]+))/i, - ); - if (headerField === 'content-disposition') { - if (m) { - part.name = m[2] || m[3] || ''; - } - - part.filename = this._fileName(headerValue); - } else if (headerField === 'content-type') { - part.mime = headerValue; - } else if (headerField === 'content-transfer-encoding') { - part.transferEncoding = headerValue.toLowerCase(); - } - - headerField = ''; - headerValue = ''; - } else if (name === 'headersEnd') { - switch (part.transferEncoding) { - case 'binary': - case '7bit': - case '8bit': { - const dataPropagation = (ctx) => { - if (ctx.name === 'partData') { - part.emit('data', ctx.buffer.slice(ctx.start, ctx.end)); - } - }; - const dataStopPropagation = (ctx) => { - if (ctx.name === 'partEnd') { - part.emit('end'); - parser.off('data', dataPropagation); - parser.off('data', dataStopPropagation); - } - }; - parser.on('data', dataPropagation); - parser.on('data', dataStopPropagation); - break; - } - case 'base64': { - const dataPropagation = (ctx) => { - if (ctx.name === 'partData') { - part.transferBuffer += ctx.buffer - .slice(ctx.start, ctx.end) - .toString('ascii'); - - /* - four bytes (chars) in base64 converts to three bytes in binary - encoding. So we should always work with a number of bytes that - can be divided by 4, it will result in a number of buytes that - can be divided vy 3. - */ - const offset = parseInt(part.transferBuffer.length / 4, 10) * 4; - part.emit( - 'data', - Buffer.from( - part.transferBuffer.substring(0, offset), - 'base64', - ), - ); - part.transferBuffer = part.transferBuffer.substring(offset); - } - }; - const dataStopPropagation = (ctx) => { - if (ctx.name === 'partEnd') { - part.emit('data', Buffer.from(part.transferBuffer, 'base64')); - part.emit('end'); - parser.off('data', dataPropagation); - parser.off('data', dataStopPropagation); - } - }; - parser.on('data', dataPropagation); - parser.on('data', dataStopPropagation); - break; - } - default: - return this._error(new Error('unknown transfer-encoding')); - } - - this.onPart(part); - } else if (name === 'end') { - this.ended = true; - this._maybeEnd(); - } - }); - - this._parser = parser; - } - - _fileName(headerValue) { + _getFileName(headerValue) { // matches either a quoted-string or a token (RFC 2616 section 19.5.1) const m = headerValue.match( // eslint-disable-next-line no-useless-escape @@ -514,103 +432,11 @@ class IncomingForm extends EventEmitter { return filename; } - _initUrlencoded() { - this.type = 'urlencoded'; - - const parser = new QuerystringParser(this.maxFields); - - parser.on('data', ({ key, value }) => { - this.emit('field', key, value); - }); - - parser.once('end', () => { - this.ended = true; - this._maybeEnd(); - }); - - this._parser = parser; - } - - _initOctetStream() { - this.type = 'octet-stream'; - const filename = this.headers['x-file-name']; - const mime = this.headers['content-type']; - - const file = new File({ - path: this._uploadPath(filename), - name: filename, - type: mime, - }); - - file.on('error', (err) => { - this._error(err); - }); - - this.emit('fileBegin', filename, file); - file.open(); - this.openedFiles.push(file); - this._flushing += 1; - - this._parser = new OctetParser(); - - // Keep track of writes that haven't finished so we don't emit the file before it's done being written - let outstandingWrites = 0; - - this._parser.on('data', (buffer) => { - this.pause(); - outstandingWrites += 1; - - file.write(buffer, () => { - outstandingWrites -= 1; - this.resume(); - - if (this.ended) { - this._parser.emit('doneWritingFile'); - } - }); - }); - - this._parser.on('end', () => { - this._flushing -= 1; - this.ended = true; - - const done = () => { - file.end(() => { - this.emit('file', 'file', file); - this._maybeEnd(); - }); - }; - - if (outstandingWrites === 0) { - done(); - } else { - this._parser.once('doneWritingFile', done); - } - }); - } - - _initJSONencoded() { - this.type = 'json'; - - const parser = new JSONParser(); - - parser.on('data', ({ key, value }) => { - this.emit('field', key, value); - }); - - parser.once('end', () => { - this.ended = true; - this._maybeEnd(); - }); - - this._parser = parser; - } - _uploadPath(filename) { const buf = crypto.randomBytes(16); let name = `upload_${buf.toString('hex')}`; - if (this.keepExtensions) { + if (this.options.keepExtensions) { let ext = path.extname(filename); ext = ext.replace(/(\.[a-z0-9]+).*/i, '$1'); @@ -632,4 +458,5 @@ class IncomingForm extends EventEmitter { } } +IncomingForm.DEFAULT_OPTIONS = DEFAULT_OPTIONS; module.exports = IncomingForm; diff --git a/src/index.js b/src/index.js index 011434de..f08819c1 100644 --- a/src/index.js +++ b/src/index.js @@ -3,11 +3,8 @@ const File = require('./File'); const Formidable = require('./Formidable'); -const JSONParser = require('./parsers/JSON'); -const DummyParser = require('./parsers/Dummy'); -const MultipartParser = require('./parsers/Multipart'); -const OctetStreamParser = require('./parsers/OctetStream'); -const QuerystringParser = require('./parsers/Querystring'); +const plugins = require('./plugins/index'); +const parsers = require('./parsers/index'); // make it available without requiring the `new` keyword // if you want it access `const formidable.IncomingForm` as v1 @@ -22,13 +19,15 @@ module.exports = Object.assign(formidable, { IncomingForm: Formidable, // parsers - JSONParser, - DummyParser, - MultipartParser, - OctetStreamParser, - QuerystringParser, - - // typo aliases - OctetstreamParser: OctetStreamParser, - QueryStringParser: QuerystringParser, + ...parsers, + parsers, + + // misc + defaultOptions: Formidable.DEFAULT_OPTIONS, + enabledPlugins: Formidable.DEFAULT_OPTIONS.enabledPlugins, + + // plugins + plugins: { + ...plugins, + }, }); diff --git a/src/parsers/Dummy.js b/src/parsers/Dummy.js index f9240f21..63409597 100644 --- a/src/parsers/Dummy.js +++ b/src/parsers/Dummy.js @@ -5,8 +5,9 @@ const { Transform } = require('stream'); class DummyParser extends Transform { - constructor(incomingForm) { + constructor(incomingForm, options = {}) { super(); + this.globalOptions = { ...options }; this.incomingForm = incomingForm; } diff --git a/src/parsers/JSON.js b/src/parsers/JSON.js index 6b2a15ab..9a096c25 100644 --- a/src/parsers/JSON.js +++ b/src/parsers/JSON.js @@ -5,9 +5,10 @@ const { Transform } = require('stream'); class JSONParser extends Transform { - constructor() { + constructor(options = {}) { super({ readableObjectMode: true }); this.chunks = []; + this.globalOptions = { ...options }; } _transform(chunk, encoding, callback) { diff --git a/src/parsers/Multipart.js b/src/parsers/Multipart.js index 9d41396e..7ade8abb 100644 --- a/src/parsers/Multipart.js +++ b/src/parsers/Multipart.js @@ -46,7 +46,7 @@ Object.keys(STATE).forEach((stateName) => { }); class MultipartParser extends Transform { - constructor() { + constructor(options = {}) { super({ readableObjectMode: true }); this.boundary = null; this.boundaryChars = null; @@ -54,6 +54,7 @@ class MultipartParser extends Transform { this.bufferLength = 0; this.state = STATE.PARSER_UNINITIALIZED; + this.globalOptions = { ...options }; this.index = null; this.flags = 0; } diff --git a/src/parsers/OctetStream.js b/src/parsers/OctetStream.js index 2e402405..cdf55f23 100644 --- a/src/parsers/OctetStream.js +++ b/src/parsers/OctetStream.js @@ -2,6 +2,11 @@ const { PassThrough } = require('stream'); -class OctetStreamParser extends PassThrough {} +class OctetStreamParser extends PassThrough { + constructor(options = {}) { + super(); + this.globalOptions = { ...options }; + } +} module.exports = OctetStreamParser; diff --git a/src/parsers/Querystring.js b/src/parsers/Querystring.js index b18c5c32..bfa91889 100644 --- a/src/parsers/Querystring.js +++ b/src/parsers/Querystring.js @@ -8,9 +8,10 @@ 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(maxKeys) { + constructor(options = {}) { super({ readableObjectMode: true }); - this.maxKeys = maxKeys; + this.globalOptions = { ...options }; + this.maxKeys = this.globalOptions.maxFields; this.buffer = ''; this.bufferLength = 0; } diff --git a/src/parsers/index.js b/src/parsers/index.js new file mode 100644 index 00000000..bbf9ef63 --- /dev/null +++ b/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/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 00000000..cbd491af --- /dev/null +++ b/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/src/plugins/json.js b/src/plugins/json.js new file mode 100644 index 00000000..20d3b261 --- /dev/null +++ b/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/src/plugins/multipart.js b/src/plugins/multipart.js new file mode 100644 index 00000000..b5c1c088 --- /dev/null +++ b/src/plugins/multipart.js @@ -0,0 +1,159 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const { Stream } = require('stream'); +const MultipartParser = require('../parsers/Multipart'); + +// 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); + } else { + const err = new Error('bad content-type header, no multipart boundary'); + 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.filename = null; + part.mime = null; + + part.transferEncoding = 'binary'; + 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.filename = this._getFileName(headerValue); + } else if (headerField === 'content-type') { + part.mime = headerValue; + } else if (headerField === 'content-transfer-encoding') { + part.transferEncoding = headerValue.toLowerCase(); + } + + headerField = ''; + headerValue = ''; + } else if (name === 'headersEnd') { + switch (part.transferEncoding) { + case 'binary': + case '7bit': + case '8bit': { + const dataPropagation = (ctx) => { + if (ctx.name === 'partData') { + part.emit('data', ctx.buffer.slice(ctx.start, ctx.end)); + } + }; + const dataStopPropagation = (ctx) => { + if (ctx.name === 'partEnd') { + part.emit('end'); + parser.off('data', dataPropagation); + parser.off('data', dataStopPropagation); + } + }; + parser.on('data', dataPropagation); + parser.on('data', dataStopPropagation); + break; + } + case 'base64': { + const dataPropagation = (ctx) => { + if (ctx.name === 'partData') { + part.transferBuffer += ctx.buffer + .slice(ctx.start, ctx.end) + .toString('ascii'); + + /* + four bytes (chars) in base64 converts to three bytes in binary + encoding. So we should always work with a number of bytes that + can be divided by 4, it will result in a number of buytes that + can be divided vy 3. + */ + const offset = parseInt(part.transferBuffer.length / 4, 10) * 4; + part.emit( + 'data', + Buffer.from( + part.transferBuffer.substring(0, offset), + 'base64', + ), + ); + part.transferBuffer = part.transferBuffer.substring(offset); + } + }; + const dataStopPropagation = (ctx) => { + if (ctx.name === 'partEnd') { + part.emit('data', Buffer.from(part.transferBuffer, 'base64')); + part.emit('end'); + parser.off('data', dataPropagation); + parser.off('data', dataStopPropagation); + } + }; + parser.on('data', dataPropagation); + parser.on('data', dataStopPropagation); + break; + } + default: + return this._error(new Error('unknown transfer-encoding')); + } + + this.onPart(part); + } else if (name === 'end') { + this.ended = true; + this._maybeEnd(); + } + }); + + this._parser = parser; + }; +} diff --git a/src/plugins/octetstream.js b/src/plugins/octetstream.js new file mode 100644 index 00000000..da17ab88 --- /dev/null +++ b/src/plugins/octetstream.js @@ -0,0 +1,81 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const File = require('../File'); +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 filename = this.headers['x-file-name']; + const mime = this.headers['content-type']; + + const file = new File({ + path: this._uploadPath(filename), + name: filename, + type: mime, + hash: this.options.hash, + }); + + this.emit('fileBegin', filename, 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/src/plugins/querystring.js b/src/plugins/querystring.js new file mode 100644 index 00000000..c9dcf1ec --- /dev/null +++ b/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; +} diff --git a/test/integration/test-fixtures.js b/test/integration/test-fixtures.js index 64d98f06..fe4298f9 100644 --- a/test/integration/test-fixtures.js +++ b/test/integration/test-fixtures.js @@ -77,7 +77,7 @@ function testNext(results) { assert.strictEqual( file.hash, expectedPart.sha1, - `error ${file.name} on ${file.path}`, + `SHA1 error ${file.name} on ${file.path}`, ); } } diff --git a/test/standalone/test-connection-aborted.js b/test/standalone/test-connection-aborted.js index 616c3b3f..a052f667 100644 --- a/test/standalone/test-connection-aborted.js +++ b/test/standalone/test-connection-aborted.js @@ -20,7 +20,12 @@ const server = http.createServer((req) => { form.on('end', () => { throw new Error('Unexpected "end" event'); }); - form.parse(req); + form.parse(req, () => { + assert( + abortedReceived, + 'from .parse() callback: Error event should follow aborted', + ); + }); }); server.listen(PORT, 'localhost', () => { diff --git a/test/standalone/test-issue-46.js b/test/standalone/test-issue-46.js index e95c13ce..a2189dcc 100644 --- a/test/standalone/test-issue-46.js +++ b/test/standalone/test-issue-46.js @@ -48,8 +48,9 @@ server.listen(PORT, () => { request({ method: 'POST', url, multipart: parts }, (e, res, body) => { const obj = JSON.parse(body); - assert.ok(obj.fields.foo, 'should havce fields.foo === barry'); + assert.ok(obj.fields.foo, 'should have fields.foo === barry'); assert.strictEqual(obj.fields.foo, 'barry'); + server.close(); }); }); diff --git a/test/standalone/test-keep-alive-error.js b/test/standalone/test-keep-alive-error.js index 7a6c997d..4954239b 100644 --- a/test/standalone/test-keep-alive-error.js +++ b/test/standalone/test-keep-alive-error.js @@ -64,7 +64,8 @@ server.listen(PORT, () => { clientTwo.end(); setTimeout(() => { - assert.strictEqual(ok, 1, `should "ok" count === 1, has: ${ok}`); + // ? yup, quite true, it makes sense to be 2 + assert.strictEqual(ok, 2, `should "ok" count === 2, has: ${ok}`); server.close(); }, 300); diff --git a/test/unit/custom-plugins.test.js b/test/unit/custom-plugins.test.js new file mode 100644 index 00000000..4a456050 --- /dev/null +++ b/test/unit/custom-plugins.test.js @@ -0,0 +1,213 @@ +/* eslint-disable no-underscore-dangle */ + +'use strict'; + +const path = require('path'); + +const Koa = require('koa'); +const request = require('supertest'); + +const { formidable } = require('../../src/index'); + +function createServer(options, handler) { + const app = new Koa(); + + app.use(async (ctx, next) => { + const form = formidable(options); + await handler(ctx, form); + await next(); + }); + + return app; +} + +function fromFixtures(...args) { + return path.join(process.cwd(), 'test', 'fixture', ...args); +} + +// function makeRequest(server, options) { +// server.listen(0, () => { +// const choosenPort = server.address().port; +// const url = `http://localhost:${choosenPort}`; +// console.log('Server up and running at:', url); + +// const method = 'POST'; + +// const opts = { +// ...options, +// port: choosenPort, +// url, +// method, +// }; + +// return http.request(opts); +// }); +// } + +// function onDone({ server, form, req, res }) { +// form.parse(req, (err, fields) => { +// assert.strictEqual(fields.qux, 'zaz'); + +// setTimeout(() => { +// res.end(); +// server.close(); +// }, 200); +// }); +// } + +// ! tests + +test('should call 3 custom and 1 builtin plugins, when .parse() is called', async () => { + const server = createServer({ enabledPlugins: ['json'] }, (ctx, form) => { + form.on('plugin', () => { + ctx.__pluginsCount = ctx.__pluginsCount || 0; + ctx.__pluginsCount += 1; + }); + form.on('pluginsResults', (results) => { + expect(results.length).toBe(4); + ctx.__pluginsResults = true; + }); + form.on('end', () => { + ctx.__ends = 1; + expect(ctx.__customPlugin1).toBe(111); + expect(ctx.__customPlugin2).toBe(222); + expect(ctx.__customPlugin3).toBe(333); + ctx.__ends = 2; + + const len = form._plugins.length; + expect(len).toBe(4); + }); + + form.use(() => { + ctx.__customPlugin1 = 111; + }); + form.use(() => { + ctx.__customPlugin2 = 222; + }); + form.use(() => { + ctx.__customPlugin3 = 333; + }); + + form.parse(ctx.req, (err, fields) => { + expect(fields.qux).toBe('zaz'); + expect(fields.a).toBe('bbb'); + expect(ctx.__pluginsCount).toBe(4); + expect(ctx.__pluginsResults).toBe(true); + }); + }); + + await new Promise((resolve, reject) => { + request(server.callback()) + .post('/') + .type('application/json') + .send({ qux: 'zaz', a: 'bbb' }) + .end((err) => (err ? reject(err) : resolve())); + }); +}); + +test('.parse throw error when some plugin fail', async () => { + const server = createServer( + { enabledPlugins: ['octetstream', 'json'] }, + (ctx, form) => { + // const failedIsOkay = false; + // ! not emitted? + // form.on('file', () => { + // ctx.__onFileCalled = ctx.__onFileCalled || 0; + // ctx.__onFileCalled += 1; + // }); + + form.on('plugin', () => { + ctx.__pluginsCount = ctx.__pluginsCount || 0; + ctx.__pluginsCount += 1; + }); + form.on('pluginsResults', () => { + throw new Error('should not be called'); + }); + + form.once('error', () => { + throw new Error('error event should not be fired when plugin throw'); + }); + + form.use(() => { + throw new Error('custom plugin err'); + }); + + let res = null; + try { + form.parse(ctx.req); + } catch (err) { + expect(err.message).toMatch(/custom plugin err/); + expect(err.message).toMatch(/plugin on index 2 failed/); + + expect(form._plugins.length).toBe(3); + expect(ctx.__pluginsCount).toBe(2); + expect(ctx.__pluginsResults).toBe(undefined); + + res = err; + } + + if (!res) { + throw new Error( + '^ .parse should throw & be caught with the try/catch ^', + ); + } + }, + ); + + await new Promise((resolve, reject) => { + request(server.callback()) + .post('/') + .type('application/octet-stream') + .attach('bin', fromFixtures('file', 'binaryfile.tar.gz')) + .end((err) => (err ? reject(err) : resolve())); + }); +}); + +test('multipart plugin fire `error` event when malformed boundary', async () => { + const server = createServer( + { enabledPlugins: ['json', 'multipart'] }, + (ctx, form) => { + let failedIsOkay = false; + + form.once('error', (err) => { + expect(form._plugins.length).toBe(2); + expect(err).toBeTruthy(); + expect(err.message).toMatch(/bad content-type header/); + expect(err.message).toMatch(/no multipart boundary/); + failedIsOkay = true; + }); + + // Should never be called when `error` + form.on('end', () => { + throw new Error('should not fire `end` event when error'); + }); + + form.parse(ctx.req, (err) => { + expect(err).toBeTruthy(); + expect(failedIsOkay).toBe(true); + }); + }, + ); + + // 'Content-Length': 1111111, + // 'content-Disposition': 'form-data; bouZndary=', + // 'Content-Type': 'multipart/form-data; bouZndary=', + await new Promise((resolve, reject) => { + request(server.callback()) + .post('/') + .type('multipart/form-data') + .set('Content-Length', 11111111) + .set('Content-Disposition', 'form-data; bouZndary=') + .set('Content-Type', 'multipart/form-data; bouZndary=') + .end((err) => (err ? reject(err) : resolve())); + }); +}); + +test('formidable() throw if not at least 1 built-in plugin in options.enabledPlugins', () => { + try { + formidable({ enabledPlugins: [] }); + } catch (err) { + expect(err).toBeTruthy(); + expect(err.message).toMatch(/expect at least 1 enabled builtin/); + } +}); diff --git a/test/unit/formidable.test.js b/test/unit/formidable.test.js index 2c08cec8..823d4a36 100644 --- a/test/unit/formidable.test.js +++ b/test/unit/formidable.test.js @@ -17,47 +17,47 @@ function makeHeader(filename) { } ['IncomingForm', 'Formidable', 'formidable'].forEach((name) => { - test(`${name}#_fileName with regular characters`, () => { + test(`${name}#_getFileName with regular characters`, () => { const filename = 'foo.txt'; const form = getForm(name); - expect(form._fileName(makeHeader(filename))).toBe('foo.txt'); + expect(form._getFileName(makeHeader(filename))).toBe('foo.txt'); }); - test(`${name}#_fileName with unescaped quote`, () => { + test(`${name}#_getFileName with unescaped quote`, () => { const filename = 'my".txt'; const form = getForm(name); - expect(form._fileName(makeHeader(filename))).toBe('my".txt'); + expect(form._getFileName(makeHeader(filename))).toBe('my".txt'); }); - test(`${name}#_fileName with escaped quote`, () => { + test(`${name}#_getFileName with escaped quote`, () => { const filename = 'my%22.txt'; const form = getForm(name); - expect(form._fileName(makeHeader(filename))).toBe('my".txt'); + expect(form._getFileName(makeHeader(filename))).toBe('my".txt'); }); - test(`${name}#_fileName with bad quote and additional sub-header`, () => { + test(`${name}#_getFileName with bad quote and additional sub-header`, () => { const filename = 'my".txt'; const form = getForm(name); const header = `${makeHeader(filename)}; foo="bar"`; - expect(form._fileName(header)).toBe(filename); + expect(form._getFileName(header)).toBe(filename); }); - test(`${name}#_fileName with semicolon`, () => { + test(`${name}#_getFileName with semicolon`, () => { const filename = 'my;.txt'; const form = getForm(name); - expect(form._fileName(makeHeader(filename))).toBe('my;.txt'); + expect(form._getFileName(makeHeader(filename))).toBe('my;.txt'); }); - test(`${name}#_fileName with utf8 character`, () => { + test(`${name}#_getFileName with utf8 character`, () => { const filename = 'my☃.txt'; const form = getForm(name); - expect(form._fileName(makeHeader(filename))).toBe('my☃.txt'); + expect(form._getFileName(makeHeader(filename))).toBe('my☃.txt'); }); test(`${name}#_uploadPath strips harmful characters from extension when keepExtensions`, () => { diff --git a/yarn.lock b/yarn.lock index 71513781..2e7e2345 100644 --- a/yarn.lock +++ b/yarn.lock @@ -628,6 +628,14 @@ abab@^2.0.0: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== +accepts@^1.3.5: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + acorn-globals@^4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" @@ -731,6 +739,11 @@ any-observable@^0.3.0: resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== +any-promise@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -828,6 +841,11 @@ arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -1022,6 +1040,14 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cache-content-type@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" + integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA== + dependencies: + mime-types "^2.1.18" + ylru "^1.2.0" + caching-transform@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-4.0.0.tgz#00d297a4206d71e2163c39eaffa8157ac0651f0f" @@ -1253,7 +1279,7 @@ compare-versions@^3.5.1: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" integrity sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg== -component-emitter@^1.2.1: +component-emitter@^1.2.0, component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -1273,6 +1299,18 @@ contains-path@^0.1.0: resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= +content-disposition@~0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + conventional-changelog-angular@^1.3.3: version "1.6.6" resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz#b27f2b315c16d0a1f23eb181309d0e6a4698ea0f" @@ -1310,6 +1348,19 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +cookiejar@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== + +cookies@~0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" + integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== + dependencies: + depd "~2.0.0" + keygrip "~1.1.0" + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -1425,6 +1476,13 @@ debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -1432,6 +1490,13 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" +debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + decamelize-keys@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -1455,6 +1520,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-equal@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -1501,6 +1571,26 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@^1.1.2, depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + detect-indent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" @@ -1511,6 +1601,14 @@ detect-newline@3.1.0, detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +dezalgo@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + diff-sequences@^25.1.0: version "25.1.0" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.1.0.tgz#fd29a46f1c913fd66c22645dc75bffbe43051f32" @@ -1560,6 +1658,11 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" @@ -1575,6 +1678,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +encodeurl@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -1589,6 +1697,11 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +error-inject@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" + integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc= + es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2: version "1.17.4" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" @@ -1620,6 +1733,11 @@ es6-error@^4.0.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -1874,7 +1992,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@~3.0.2: +extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -2066,6 +2184,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^2.3.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -2075,6 +2202,11 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formidable@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" + integrity sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg== + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -2082,6 +2214,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + fromentries@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.2.0.tgz#e6aa06f240d6267f913cea422075ef88b63e7897" @@ -2334,6 +2471,25 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.0.tgz#71e87f931de3fe09e56661ab9a29aadec707b491" integrity sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig== +http-assert@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878" + integrity sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw== + dependencies: + deep-equal "~1.0.1" + http-errors "~1.7.2" + +http-errors@^1.6.3, http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -2428,7 +2584,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2579,6 +2735,11 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-generator-function@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" + integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== + is-glob@^4.0.0, is-glob@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" @@ -3224,6 +3385,13 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +keygrip@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" + integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== + dependencies: + tsscmp "1.0.6" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -3253,6 +3421,56 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +koa-compose@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" + integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec= + dependencies: + any-promise "^1.1.0" + +koa-compose@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" + integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== + +koa-convert@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" + integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA= + dependencies: + co "^4.6.0" + koa-compose "^3.0.0" + +koa@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.11.0.tgz#fe5a51c46f566d27632dd5dc8fd5d7dd44f935a4" + integrity sha512-EpR9dElBTDlaDgyhDMiLkXrPwp6ZqgAIBvhhmxQ9XN4TFgW+gEz6tkcsNI6BnUbUftrKDjVFj4lW2/J2aNBMMA== + dependencies: + accepts "^1.3.5" + cache-content-type "^1.0.0" + content-disposition "~0.5.2" + content-type "^1.0.4" + cookies "~0.8.0" + debug "~3.1.0" + delegates "^1.0.0" + depd "^1.1.2" + destroy "^1.0.4" + encodeurl "^1.0.2" + error-inject "^1.0.0" + escape-html "^1.0.3" + fresh "~0.5.2" + http-assert "^1.3.0" + http-errors "^1.6.3" + is-generator-function "^1.0.7" + koa-compose "^4.1.0" + koa-convert "^1.2.0" + on-finished "^2.3.0" + only "~0.0.2" + parseurl "^1.3.2" + statuses "^1.5.0" + type-is "^1.6.16" + vary "^1.1.2" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -3478,6 +3696,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + meow@5.0.0, meow@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" @@ -3503,6 +3726,11 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== +methods@^1.1.1, methods@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -3535,13 +3763,18 @@ mime-db@1.43.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.26" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== dependencies: mime-db "1.43.0" +mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -3629,6 +3862,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -3823,6 +4061,13 @@ object.values@^1.1.0: function-bind "^1.1.1" has "^1.0.3" +on-finished@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -3844,6 +4089,11 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +only@~0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" + integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q= + opencollective-postinstall@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" @@ -3978,6 +4228,11 @@ parse5@5.1.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== +parseurl@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -4172,6 +4427,11 @@ q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qs@^6.5.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" + integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -4230,7 +4490,7 @@ read-pkg@^3.0.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@~2.3.6: +readable-stream@^2.3.5, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -4477,16 +4737,16 @@ rxjs@^6.3.3, rxjs@^6.5.3: dependencies: tslib "^1.9.0" +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -4561,6 +4821,11 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -4789,6 +5054,11 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +"statuses@>= 1.5.0 < 2", statuses@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" @@ -4939,6 +5209,30 @@ strip-json-comments@^3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== +superagent@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" + integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.2.0" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.3.5" + +supertest@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-4.0.2.tgz#c2234dbdd6dc79b6f15b99c8d6577b90e4ce3f36" + integrity sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ== + dependencies: + methods "^1.1.2" + superagent "^3.8.3" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -5087,6 +5381,11 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -5126,6 +5425,11 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== +tsscmp@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" + integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -5155,6 +5459,14 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-is@^1.6.16: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -5249,6 +5561,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -5438,3 +5755,8 @@ yargs@^15.0.0, yargs@^15.0.2: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^16.1.0" + +ylru@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f" + integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==