Browse Source

add saving avatar function on server and send url response

serg1557733 1 year ago
parent
commit
76b803c331
100 changed files with 19076 additions and 30 deletions
  1. 23 24
      backend/app.js
  2. 12 0
      backend/node_modules/append-field/index.js
  3. 53 0
      backend/node_modules/append-field/lib/parse-path.js
  4. 64 0
      backend/node_modules/append-field/lib/set-value.js
  5. 19 0
      backend/node_modules/append-field/test/forms.js
  6. 26 2
      backend/node_modules/body-parser/lib/read.js
  7. 10 4
      backend/node_modules/body-parser/lib/types/json.js
  8. 538 0
      backend/node_modules/body-parser/node_modules/depd/index.js
  9. 77 0
      backend/node_modules/body-parser/node_modules/depd/lib/browser/index.js
  10. 209 0
      backend/node_modules/body-parser/node_modules/destroy/index.js
  11. 289 0
      backend/node_modules/body-parser/node_modules/http-errors/index.js
  12. 234 0
      backend/node_modules/body-parser/node_modules/on-finished/index.js
  13. 2044 0
      backend/node_modules/body-parser/node_modules/qs/dist/qs.js
  14. 23 0
      backend/node_modules/body-parser/node_modules/qs/lib/formats.js
  15. 11 0
      backend/node_modules/body-parser/node_modules/qs/lib/index.js
  16. 263 0
      backend/node_modules/body-parser/node_modules/qs/lib/parse.js
  17. 317 0
      backend/node_modules/body-parser/node_modules/qs/lib/stringify.js
  18. 252 0
      backend/node_modules/body-parser/node_modules/qs/lib/utils.js
  19. 841 0
      backend/node_modules/body-parser/node_modules/qs/test/parse.js
  20. 865 0
      backend/node_modules/body-parser/node_modules/qs/test/stringify.js
  21. 136 0
      backend/node_modules/body-parser/node_modules/qs/test/utils.js
  22. 146 0
      backend/node_modules/body-parser/node_modules/statuses/index.js
  23. 72 0
      backend/node_modules/buffer-from/index.js
  24. 5 0
      backend/node_modules/busboy/.eslintrc.js
  25. 149 0
      backend/node_modules/busboy/bench/bench-multipart-fields-100mb-big.js
  26. 143 0
      backend/node_modules/busboy/bench/bench-multipart-fields-100mb-small.js
  27. 154 0
      backend/node_modules/busboy/bench/bench-multipart-files-100mb-big.js
  28. 148 0
      backend/node_modules/busboy/bench/bench-multipart-files-100mb-small.js
  29. 101 0
      backend/node_modules/busboy/bench/bench-urlencoded-fields-100pairs-small.js
  30. 84 0
      backend/node_modules/busboy/bench/bench-urlencoded-fields-900pairs-small-alt.js
  31. 57 0
      backend/node_modules/busboy/lib/index.js
  32. 653 0
      backend/node_modules/busboy/lib/types/multipart.js
  33. 350 0
      backend/node_modules/busboy/lib/types/urlencoded.js
  34. 596 0
      backend/node_modules/busboy/lib/utils.js
  35. 109 0
      backend/node_modules/busboy/test/common.js
  36. 94 0
      backend/node_modules/busboy/test/test-types-multipart-charsets.js
  37. 102 0
      backend/node_modules/busboy/test/test-types-multipart-stream-pause.js
  38. 1053 0
      backend/node_modules/busboy/test/test-types-multipart.js
  39. 488 0
      backend/node_modules/busboy/test/test-types-urlencoded.js
  40. 20 0
      backend/node_modules/busboy/test/test.js
  41. 4 0
      backend/node_modules/bytes/index.js
  42. 15 0
      backend/node_modules/call-bind/callBound.js
  43. 47 0
      backend/node_modules/call-bind/index.js
  44. 55 0
      backend/node_modules/call-bind/test/callBound.js
  45. 66 0
      backend/node_modules/call-bind/test/index.js
  46. 144 0
      backend/node_modules/concat-stream/index.js
  47. 1 0
      backend/node_modules/concat-stream/node_modules/readable-stream/duplex-browser.js
  48. 1 0
      backend/node_modules/concat-stream/node_modules/readable-stream/duplex.js
  49. 131 0
      backend/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_duplex.js
  50. 47 0
      backend/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_passthrough.js
  51. 1019 0
      backend/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_readable.js
  52. 214 0
      backend/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_transform.js
  53. 687 0
      backend/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_writable.js
  54. 79 0
      backend/node_modules/concat-stream/node_modules/readable-stream/lib/internal/streams/BufferList.js
  55. 74 0
      backend/node_modules/concat-stream/node_modules/readable-stream/lib/internal/streams/destroy.js
  56. 1 0
      backend/node_modules/concat-stream/node_modules/readable-stream/lib/internal/streams/stream-browser.js
  57. 1 0
      backend/node_modules/concat-stream/node_modules/readable-stream/lib/internal/streams/stream.js
  58. 1 0
      backend/node_modules/concat-stream/node_modules/readable-stream/passthrough.js
  59. 7 0
      backend/node_modules/concat-stream/node_modules/readable-stream/readable-browser.js
  60. 19 0
      backend/node_modules/concat-stream/node_modules/readable-stream/readable.js
  61. 1 0
      backend/node_modules/concat-stream/node_modules/readable-stream/transform.js
  62. 1 0
      backend/node_modules/concat-stream/node_modules/readable-stream/writable-browser.js
  63. 8 0
      backend/node_modules/concat-stream/node_modules/readable-stream/writable.js
  64. 62 0
      backend/node_modules/concat-stream/node_modules/safe-buffer/index.js
  65. 296 0
      backend/node_modules/concat-stream/node_modules/string_decoder/lib/string_decoder.js
  66. 107 0
      backend/node_modules/core-util-is/lib/util.js
  67. 41 0
      backend/node_modules/express-fileupload/example/server.js
  68. 65 0
      backend/node_modules/express-fileupload/lib/fileFactory.js
  69. 39 0
      backend/node_modules/express-fileupload/lib/index.js
  70. 34 0
      backend/node_modules/express-fileupload/lib/isEligibleRequest.js
  71. 42 0
      backend/node_modules/express-fileupload/lib/memHandler.js
  72. 168 0
      backend/node_modules/express-fileupload/lib/processMultipart.js
  73. 35 0
      backend/node_modules/express-fileupload/lib/processNested.js
  74. 64 0
      backend/node_modules/express-fileupload/lib/tempFileHandler.js
  75. 26 0
      backend/node_modules/express-fileupload/lib/uploadtimer.js
  76. 311 0
      backend/node_modules/express-fileupload/lib/utilities.js
  77. 78 0
      backend/node_modules/express-fileupload/test/fileFactory.spec.js
  78. 95 0
      backend/node_modules/express-fileupload/test/fileLimitUploads.spec.js
  79. 85 0
      backend/node_modules/express-fileupload/test/multipartFields.spec.js
  80. 451 0
      backend/node_modules/express-fileupload/test/multipartUploads.spec.js
  81. 219 0
      backend/node_modules/express-fileupload/test/options.spec.js
  82. 59 0
      backend/node_modules/express-fileupload/test/processNested.spec.js
  83. 275 0
      backend/node_modules/express-fileupload/test/server.js
  84. 132 0
      backend/node_modules/express-fileupload/test/tempFile.spec.js
  85. 28 0
      backend/node_modules/express-fileupload/test/uploadtimer.spec.js
  86. 468 0
      backend/node_modules/express-fileupload/test/utilities.spec.js
  87. 157 0
      backend/node_modules/express/node_modules/body-parser/index.js
  88. 181 0
      backend/node_modules/express/node_modules/body-parser/lib/read.js
  89. 230 0
      backend/node_modules/express/node_modules/body-parser/lib/types/json.js
  90. 101 0
      backend/node_modules/express/node_modules/body-parser/lib/types/raw.js
  91. 121 0
      backend/node_modules/express/node_modules/body-parser/lib/types/text.js
  92. 284 0
      backend/node_modules/express/node_modules/body-parser/lib/types/urlencoded.js
  93. 166 0
      backend/node_modules/express/node_modules/bytes/index.js
  94. 286 0
      backend/node_modules/express/node_modules/raw-body/index.js
  95. 52 0
      backend/node_modules/function-bind/implementation.js
  96. 5 0
      backend/node_modules/function-bind/index.js
  97. 252 0
      backend/node_modules/function-bind/test/index.js
  98. 334 0
      backend/node_modules/get-intrinsic/index.js
  99. 274 0
      backend/node_modules/get-intrinsic/test/GetIntrinsic.js
  100. 0 0
      backend/node_modules/has-symbols/index.js

+ 23 - 24
backend/app.js

@@ -10,13 +10,14 @@ const jwt = require('jsonwebtoken');
 const bcrypt = require('bcrypt');
 require('dotenv').config(); // add dotnv for config
 const Uuid = require('uuid'); //lib for unic id generate
-
+const fileupload = require('express-fileupload');
 
 
 const server = http.createServer(app);
 app.use(cors());
-//app.use(express.json());
-app.use(express.static('static')); //folder for static files
+app.use(express.json());
+app.use(fileupload())
+app.use(express.static('avatars')); //folder for static files
 
 const io = require("socket.io")(server, {
     cors: {
@@ -28,6 +29,8 @@ const randomColor = require('randomcolor');
 const PORT = process.env.PORT || 5000;
 const TOKEN_KEY = process.env.TOKEN_KEY || 'rGH4r@3DKOg06hgj'; 
 const HASH_KEY = 7;
+const STATIC_PATH = process.env. STATIC_PATH || 'avatars';
+
 
 const generateToken = (id, userName, isAdmin) => {
     const payload = {
@@ -70,7 +73,8 @@ app.post('/login', async (req, res) => {
                 hashPassword,
                 isAdmin: !await User.count().exec(),
                 isBanned: false,
-                isMutted: false
+                isMutted: false, 
+                avatar: ''
             });
 
             await user.save()
@@ -95,32 +99,27 @@ app.post('/login', async (req, res) => {
     }
 })
 
-
-const STATIC_PATH = process.env. STATIC_PATH || 'static';
-
-const loadUserAvatar = async (req, res) =>  {
-
-    console.log(req.files)
+app.post('/avatar', async (req, res) =>  {
+    if (!req.files || Object.keys(req.files).length === 0) {
+        return res.status(400).json('No files were uploaded.');
+      }
     try {
-        const file = req.files;
-        // const user = await getOneUser(req.userName);
-        // console.log(user)
+        const file = req.files.file;
+        const user = jwt.verify(req.body.token, TOKEN_KEY);
         const avatarFileName = Uuid.v4() + '.jpeg';
-
-        console.log(STATIC_PATH + '\\' + avatarFileName, req);
-
-        file.mv(STATIC_PATH + '\\' + avatarFileName)
-        // console.log(STATIC_PATH + '\\' + avatarFileName, req);
-        // user.avatar = avatarFileName;
-        // user.save;
-        return res.json({ message:'Avatar was uploud succesfully...'})
+        file.mv(STATIC_PATH + '\/' + avatarFileName)
+        const userFromDb = await User.findOneAndUpdate({userName: user.userName},{ $set: {'avatar': avatarFileName}},   {
+            new: true
+          });
+        return res.json({ message:'Avatar was uploud succesfully...', avatarUrl: avatarFileName})
         
     } catch (error) {
-        res.status(400).json({message: `Error uppload file to serverp: ${error}`});
+        res.status(500).json({message: `Error uppload file to serverp: ${error}`});
     }
-}
+})
+
+
 
-app.post('/avatar', loadUserAvatar);
 
 io.use( async (socket, next) => {
     const token = socket.handshake.auth.token; 

+ 12 - 0
backend/node_modules/append-field/index.js

@@ -0,0 +1,12 @@
+var parsePath = require('./lib/parse-path')
+var setValue = require('./lib/set-value')
+
+function appendField (store, key, value) {
+  var steps = parsePath(key)
+
+  steps.reduce(function (context, step) {
+    return setValue(context, step, context[step.key], value)
+  }, store)
+}
+
+module.exports = appendField

+ 53 - 0
backend/node_modules/append-field/lib/parse-path.js

@@ -0,0 +1,53 @@
+var reFirstKey = /^[^\[]*/
+var reDigitPath = /^\[(\d+)\]/
+var reNormalPath = /^\[([^\]]+)\]/
+
+function parsePath (key) {
+  function failure () {
+    return [{ type: 'object', key: key, last: true }]
+  }
+
+  var firstKey = reFirstKey.exec(key)[0]
+  if (!firstKey) return failure()
+
+  var len = key.length
+  var pos = firstKey.length
+  var tail = { type: 'object', key: firstKey }
+  var steps = [tail]
+
+  while (pos < len) {
+    var m
+
+    if (key[pos] === '[' && key[pos + 1] === ']') {
+      pos += 2
+      tail.append = true
+      if (pos !== len) return failure()
+      continue
+    }
+
+    m = reDigitPath.exec(key.substring(pos))
+    if (m !== null) {
+      pos += m[0].length
+      tail.nextType = 'array'
+      tail = { type: 'array', key: parseInt(m[1], 10) }
+      steps.push(tail)
+      continue
+    }
+
+    m = reNormalPath.exec(key.substring(pos))
+    if (m !== null) {
+      pos += m[0].length
+      tail.nextType = 'object'
+      tail = { type: 'object', key: m[1] }
+      steps.push(tail)
+      continue
+    }
+
+    return failure()
+  }
+
+  tail.last = true
+  return steps
+}
+
+module.exports = parsePath

+ 64 - 0
backend/node_modules/append-field/lib/set-value.js

@@ -0,0 +1,64 @@
+function valueType (value) {
+  if (value === undefined) return 'undefined'
+  if (Array.isArray(value)) return 'array'
+  if (typeof value === 'object') return 'object'
+  return 'scalar'
+}
+
+function setLastValue (context, step, currentValue, entryValue) {
+  switch (valueType(currentValue)) {
+    case 'undefined':
+      if (step.append) {
+        context[step.key] = [entryValue]
+      } else {
+        context[step.key] = entryValue
+      }
+      break
+    case 'array':
+      context[step.key].push(entryValue)
+      break
+    case 'object':
+      return setLastValue(currentValue, { type: 'object', key: '', last: true }, currentValue[''], entryValue)
+    case 'scalar':
+      context[step.key] = [context[step.key], entryValue]
+      break
+  }
+
+  return context
+}
+
+function setValue (context, step, currentValue, entryValue) {
+  if (step.last) return setLastValue(context, step, currentValue, entryValue)
+
+  var obj
+  switch (valueType(currentValue)) {
+    case 'undefined':
+      if (step.nextType === 'array') {
+        context[step.key] = []
+      } else {
+        context[step.key] = Object.create(null)
+      }
+      return context[step.key]
+    case 'object':
+      return context[step.key]
+    case 'array':
+      if (step.nextType === 'array') {
+        return currentValue
+      }
+
+      obj = Object.create(null)
+      context[step.key] = obj
+      currentValue.forEach(function (item, i) {
+        if (item !== undefined) obj['' + i] = item
+      })
+
+      return obj
+    case 'scalar':
+      obj = Object.create(null)
+      obj[''] = currentValue
+      context[step.key] = obj
+      return obj
+  }
+}
+
+module.exports = setValue

+ 19 - 0
backend/node_modules/append-field/test/forms.js

@@ -0,0 +1,19 @@
+/* eslint-env mocha */
+
+var assert = require('assert')
+var appendField = require('../')
+var testData = require('testdata-w3c-json-form')
+
+describe('Append Field', function () {
+  for (var test of testData) {
+    it('handles ' + test.name, function () {
+      var store = Object.create(null)
+
+      for (var field of test.fields) {
+        appendField(store, field.key, field.value)
+      }
+
+      assert.deepEqual(store, test.expected)
+    })
+  }
+})

+ 26 - 2
backend/node_modules/body-parser/lib/read.js

@@ -12,9 +12,11 @@
  */
 
 var createError = require('http-errors')
+var destroy = require('destroy')
 var getBody = require('raw-body')
 var iconv = require('iconv-lite')
 var onFinished = require('on-finished')
+var unpipe = require('unpipe')
 var zlib = require('zlib')
 
 /**
@@ -89,9 +91,14 @@ function read (req, res, next, parse, debug, options) {
         _error = createError(400, error)
       }
 
+      // unpipe from stream and destroy
+      if (stream !== req) {
+        unpipe(req)
+        destroy(stream, true)
+      }
+
       // read off entire request
-      stream.resume()
-      onFinished(req, function onfinished () {
+      dump(req, function onfinished () {
         next(createError(400, _error))
       })
       return
@@ -179,3 +186,20 @@ function contentstream (req, debug, inflate) {
 
   return stream
 }
+
+/**
+ * Dump the contents of a request.
+ *
+ * @param {object} req
+ * @param {function} callback
+ * @api private
+ */
+
+function dump (req, callback) {
+  if (onFinished.isFinished(req)) {
+    callback(null)
+  } else {
+    onFinished(req, callback)
+    req.resume()
+  }
+}

+ 10 - 4
backend/node_modules/body-parser/lib/types/json.js

@@ -37,7 +37,7 @@ module.exports = json
  *            %x0D )              ; Carriage return
  */
 
-var FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*(.)/ // eslint-disable-line no-control-regex
+var FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/ // eslint-disable-line no-control-regex
 
 /**
  * Create a middleware to parse JSON bodies.
@@ -122,7 +122,7 @@ function json (options) {
 
     // assert charset per RFC 7159 sec 8.1
     var charset = getCharset(req) || 'utf-8'
-    if (charset.substr(0, 4) !== 'utf-') {
+    if (charset.slice(0, 4) !== 'utf-') {
       debug('invalid charset')
       next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
         charset: charset,
@@ -152,7 +152,9 @@ function json (options) {
 
 function createStrictSyntaxError (str, char) {
   var index = str.indexOf(char)
-  var partial = str.substring(0, index) + '#'
+  var partial = index !== -1
+    ? str.substring(0, index) + '#'
+    : ''
 
   try {
     JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation')
@@ -173,7 +175,11 @@ function createStrictSyntaxError (str, char) {
  */
 
 function firstchar (str) {
-  return FIRST_CHAR_REGEXP.exec(str)[1]
+  var match = FIRST_CHAR_REGEXP.exec(str)
+
+  return match
+    ? match[1]
+    : undefined
 }
 
 /**

+ 538 - 0
backend/node_modules/body-parser/node_modules/depd/index.js

@@ -0,0 +1,538 @@
+/*!
+ * depd
+ * Copyright(c) 2014-2018 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var relative = require('path').relative
+
+/**
+ * Module exports.
+ */
+
+module.exports = depd
+
+/**
+ * Get the path to base files on.
+ */
+
+var basePath = process.cwd()
+
+/**
+ * Determine if namespace is contained in the string.
+ */
+
+function containsNamespace (str, namespace) {
+  var vals = str.split(/[ ,]+/)
+  var ns = String(namespace).toLowerCase()
+
+  for (var i = 0; i < vals.length; i++) {
+    var val = vals[i]
+
+    // namespace contained
+    if (val && (val === '*' || val.toLowerCase() === ns)) {
+      return true
+    }
+  }
+
+  return false
+}
+
+/**
+ * Convert a data descriptor to accessor descriptor.
+ */
+
+function convertDataDescriptorToAccessor (obj, prop, message) {
+  var descriptor = Object.getOwnPropertyDescriptor(obj, prop)
+  var value = descriptor.value
+
+  descriptor.get = function getter () { return value }
+
+  if (descriptor.writable) {
+    descriptor.set = function setter (val) { return (value = val) }
+  }
+
+  delete descriptor.value
+  delete descriptor.writable
+
+  Object.defineProperty(obj, prop, descriptor)
+
+  return descriptor
+}
+
+/**
+ * Create arguments string to keep arity.
+ */
+
+function createArgumentsString (arity) {
+  var str = ''
+
+  for (var i = 0; i < arity; i++) {
+    str += ', arg' + i
+  }
+
+  return str.substr(2)
+}
+
+/**
+ * Create stack string from stack.
+ */
+
+function createStackString (stack) {
+  var str = this.name + ': ' + this.namespace
+
+  if (this.message) {
+    str += ' deprecated ' + this.message
+  }
+
+  for (var i = 0; i < stack.length; i++) {
+    str += '\n    at ' + stack[i].toString()
+  }
+
+  return str
+}
+
+/**
+ * Create deprecate for namespace in caller.
+ */
+
+function depd (namespace) {
+  if (!namespace) {
+    throw new TypeError('argument namespace is required')
+  }
+
+  var stack = getStack()
+  var site = callSiteLocation(stack[1])
+  var file = site[0]
+
+  function deprecate (message) {
+    // call to self as log
+    log.call(deprecate, message)
+  }
+
+  deprecate._file = file
+  deprecate._ignored = isignored(namespace)
+  deprecate._namespace = namespace
+  deprecate._traced = istraced(namespace)
+  deprecate._warned = Object.create(null)
+
+  deprecate.function = wrapfunction
+  deprecate.property = wrapproperty
+
+  return deprecate
+}
+
+/**
+ * Determine if event emitter has listeners of a given type.
+ *
+ * The way to do this check is done three different ways in Node.js >= 0.8
+ * so this consolidates them into a minimal set using instance methods.
+ *
+ * @param {EventEmitter} emitter
+ * @param {string} type
+ * @returns {boolean}
+ * @private
+ */
+
+function eehaslisteners (emitter, type) {
+  var count = typeof emitter.listenerCount !== 'function'
+    ? emitter.listeners(type).length
+    : emitter.listenerCount(type)
+
+  return count > 0
+}
+
+/**
+ * Determine if namespace is ignored.
+ */
+
+function isignored (namespace) {
+  if (process.noDeprecation) {
+    // --no-deprecation support
+    return true
+  }
+
+  var str = process.env.NO_DEPRECATION || ''
+
+  // namespace ignored
+  return containsNamespace(str, namespace)
+}
+
+/**
+ * Determine if namespace is traced.
+ */
+
+function istraced (namespace) {
+  if (process.traceDeprecation) {
+    // --trace-deprecation support
+    return true
+  }
+
+  var str = process.env.TRACE_DEPRECATION || ''
+
+  // namespace traced
+  return containsNamespace(str, namespace)
+}
+
+/**
+ * Display deprecation message.
+ */
+
+function log (message, site) {
+  var haslisteners = eehaslisteners(process, 'deprecation')
+
+  // abort early if no destination
+  if (!haslisteners && this._ignored) {
+    return
+  }
+
+  var caller
+  var callFile
+  var callSite
+  var depSite
+  var i = 0
+  var seen = false
+  var stack = getStack()
+  var file = this._file
+
+  if (site) {
+    // provided site
+    depSite = site
+    callSite = callSiteLocation(stack[1])
+    callSite.name = depSite.name
+    file = callSite[0]
+  } else {
+    // get call site
+    i = 2
+    depSite = callSiteLocation(stack[i])
+    callSite = depSite
+  }
+
+  // get caller of deprecated thing in relation to file
+  for (; i < stack.length; i++) {
+    caller = callSiteLocation(stack[i])
+    callFile = caller[0]
+
+    if (callFile === file) {
+      seen = true
+    } else if (callFile === this._file) {
+      file = this._file
+    } else if (seen) {
+      break
+    }
+  }
+
+  var key = caller
+    ? depSite.join(':') + '__' + caller.join(':')
+    : undefined
+
+  if (key !== undefined && key in this._warned) {
+    // already warned
+    return
+  }
+
+  this._warned[key] = true
+
+  // generate automatic message from call site
+  var msg = message
+  if (!msg) {
+    msg = callSite === depSite || !callSite.name
+      ? defaultMessage(depSite)
+      : defaultMessage(callSite)
+  }
+
+  // emit deprecation if listeners exist
+  if (haslisteners) {
+    var err = DeprecationError(this._namespace, msg, stack.slice(i))
+    process.emit('deprecation', err)
+    return
+  }
+
+  // format and write message
+  var format = process.stderr.isTTY
+    ? formatColor
+    : formatPlain
+  var output = format.call(this, msg, caller, stack.slice(i))
+  process.stderr.write(output + '\n', 'utf8')
+}
+
+/**
+ * Get call site location as array.
+ */
+
+function callSiteLocation (callSite) {
+  var file = callSite.getFileName() || '<anonymous>'
+  var line = callSite.getLineNumber()
+  var colm = callSite.getColumnNumber()
+
+  if (callSite.isEval()) {
+    file = callSite.getEvalOrigin() + ', ' + file
+  }
+
+  var site = [file, line, colm]
+
+  site.callSite = callSite
+  site.name = callSite.getFunctionName()
+
+  return site
+}
+
+/**
+ * Generate a default message from the site.
+ */
+
+function defaultMessage (site) {
+  var callSite = site.callSite
+  var funcName = site.name
+
+  // make useful anonymous name
+  if (!funcName) {
+    funcName = '<anonymous@' + formatLocation(site) + '>'
+  }
+
+  var context = callSite.getThis()
+  var typeName = context && callSite.getTypeName()
+
+  // ignore useless type name
+  if (typeName === 'Object') {
+    typeName = undefined
+  }
+
+  // make useful type name
+  if (typeName === 'Function') {
+    typeName = context.name || typeName
+  }
+
+  return typeName && callSite.getMethodName()
+    ? typeName + '.' + funcName
+    : funcName
+}
+
+/**
+ * Format deprecation message without color.
+ */
+
+function formatPlain (msg, caller, stack) {
+  var timestamp = new Date().toUTCString()
+
+  var formatted = timestamp +
+    ' ' + this._namespace +
+    ' deprecated ' + msg
+
+  // add stack trace
+  if (this._traced) {
+    for (var i = 0; i < stack.length; i++) {
+      formatted += '\n    at ' + stack[i].toString()
+    }
+
+    return formatted
+  }
+
+  if (caller) {
+    formatted += ' at ' + formatLocation(caller)
+  }
+
+  return formatted
+}
+
+/**
+ * Format deprecation message with color.
+ */
+
+function formatColor (msg, caller, stack) {
+  var formatted = '\x1b[36;1m' + this._namespace + '\x1b[22;39m' + // bold cyan
+    ' \x1b[33;1mdeprecated\x1b[22;39m' + // bold yellow
+    ' \x1b[0m' + msg + '\x1b[39m' // reset
+
+  // add stack trace
+  if (this._traced) {
+    for (var i = 0; i < stack.length; i++) {
+      formatted += '\n    \x1b[36mat ' + stack[i].toString() + '\x1b[39m' // cyan
+    }
+
+    return formatted
+  }
+
+  if (caller) {
+    formatted += ' \x1b[36m' + formatLocation(caller) + '\x1b[39m' // cyan
+  }
+
+  return formatted
+}
+
+/**
+ * Format call site location.
+ */
+
+function formatLocation (callSite) {
+  return relative(basePath, callSite[0]) +
+    ':' + callSite[1] +
+    ':' + callSite[2]
+}
+
+/**
+ * Get the stack as array of call sites.
+ */
+
+function getStack () {
+  var limit = Error.stackTraceLimit
+  var obj = {}
+  var prep = Error.prepareStackTrace
+
+  Error.prepareStackTrace = prepareObjectStackTrace
+  Error.stackTraceLimit = Math.max(10, limit)
+
+  // capture the stack
+  Error.captureStackTrace(obj)
+
+  // slice this function off the top
+  var stack = obj.stack.slice(1)
+
+  Error.prepareStackTrace = prep
+  Error.stackTraceLimit = limit
+
+  return stack
+}
+
+/**
+ * Capture call site stack from v8.
+ */
+
+function prepareObjectStackTrace (obj, stack) {
+  return stack
+}
+
+/**
+ * Return a wrapped function in a deprecation message.
+ */
+
+function wrapfunction (fn, message) {
+  if (typeof fn !== 'function') {
+    throw new TypeError('argument fn must be a function')
+  }
+
+  var args = createArgumentsString(fn.length)
+  var stack = getStack()
+  var site = callSiteLocation(stack[1])
+
+  site.name = fn.name
+
+  // eslint-disable-next-line no-new-func
+  var deprecatedfn = new Function('fn', 'log', 'deprecate', 'message', 'site',
+    '"use strict"\n' +
+    'return function (' + args + ') {' +
+    'log.call(deprecate, message, site)\n' +
+    'return fn.apply(this, arguments)\n' +
+    '}')(fn, log, this, message, site)
+
+  return deprecatedfn
+}
+
+/**
+ * Wrap property in a deprecation message.
+ */
+
+function wrapproperty (obj, prop, message) {
+  if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
+    throw new TypeError('argument obj must be object')
+  }
+
+  var descriptor = Object.getOwnPropertyDescriptor(obj, prop)
+
+  if (!descriptor) {
+    throw new TypeError('must call property on owner object')
+  }
+
+  if (!descriptor.configurable) {
+    throw new TypeError('property must be configurable')
+  }
+
+  var deprecate = this
+  var stack = getStack()
+  var site = callSiteLocation(stack[1])
+
+  // set site name
+  site.name = prop
+
+  // convert data descriptor
+  if ('value' in descriptor) {
+    descriptor = convertDataDescriptorToAccessor(obj, prop, message)
+  }
+
+  var get = descriptor.get
+  var set = descriptor.set
+
+  // wrap getter
+  if (typeof get === 'function') {
+    descriptor.get = function getter () {
+      log.call(deprecate, message, site)
+      return get.apply(this, arguments)
+    }
+  }
+
+  // wrap setter
+  if (typeof set === 'function') {
+    descriptor.set = function setter () {
+      log.call(deprecate, message, site)
+      return set.apply(this, arguments)
+    }
+  }
+
+  Object.defineProperty(obj, prop, descriptor)
+}
+
+/**
+ * Create DeprecationError for deprecation
+ */
+
+function DeprecationError (namespace, message, stack) {
+  var error = new Error()
+  var stackString
+
+  Object.defineProperty(error, 'constructor', {
+    value: DeprecationError
+  })
+
+  Object.defineProperty(error, 'message', {
+    configurable: true,
+    enumerable: false,
+    value: message,
+    writable: true
+  })
+
+  Object.defineProperty(error, 'name', {
+    enumerable: false,
+    configurable: true,
+    value: 'DeprecationError',
+    writable: true
+  })
+
+  Object.defineProperty(error, 'namespace', {
+    configurable: true,
+    enumerable: false,
+    value: namespace,
+    writable: true
+  })
+
+  Object.defineProperty(error, 'stack', {
+    configurable: true,
+    enumerable: false,
+    get: function () {
+      if (stackString !== undefined) {
+        return stackString
+      }
+
+      // prepare stack trace
+      return (stackString = createStackString.call(this, stack))
+    },
+    set: function setter (val) {
+      stackString = val
+    }
+  })
+
+  return error
+}

+ 77 - 0
backend/node_modules/body-parser/node_modules/depd/lib/browser/index.js

@@ -0,0 +1,77 @@
+/*!
+ * depd
+ * Copyright(c) 2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = depd
+
+/**
+ * Create deprecate for namespace in caller.
+ */
+
+function depd (namespace) {
+  if (!namespace) {
+    throw new TypeError('argument namespace is required')
+  }
+
+  function deprecate (message) {
+    // no-op in browser
+  }
+
+  deprecate._file = undefined
+  deprecate._ignored = true
+  deprecate._namespace = namespace
+  deprecate._traced = false
+  deprecate._warned = Object.create(null)
+
+  deprecate.function = wrapfunction
+  deprecate.property = wrapproperty
+
+  return deprecate
+}
+
+/**
+ * Return a wrapped function in a deprecation message.
+ *
+ * This is a no-op version of the wrapper, which does nothing but call
+ * validation.
+ */
+
+function wrapfunction (fn, message) {
+  if (typeof fn !== 'function') {
+    throw new TypeError('argument fn must be a function')
+  }
+
+  return fn
+}
+
+/**
+ * Wrap property in a deprecation message.
+ *
+ * This is a no-op version of the wrapper, which does nothing but call
+ * validation.
+ */
+
+function wrapproperty (obj, prop, message) {
+  if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
+    throw new TypeError('argument obj must be object')
+  }
+
+  var descriptor = Object.getOwnPropertyDescriptor(obj, prop)
+
+  if (!descriptor) {
+    throw new TypeError('must call property on owner object')
+  }
+
+  if (!descriptor.configurable) {
+    throw new TypeError('property must be configurable')
+  }
+}

+ 209 - 0
backend/node_modules/body-parser/node_modules/destroy/index.js

@@ -0,0 +1,209 @@
+/*!
+ * destroy
+ * Copyright(c) 2014 Jonathan Ong
+ * Copyright(c) 2015-2022 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var EventEmitter = require('events').EventEmitter
+var ReadStream = require('fs').ReadStream
+var Stream = require('stream')
+var Zlib = require('zlib')
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = destroy
+
+/**
+ * Destroy the given stream, and optionally suppress any future `error` events.
+ *
+ * @param {object} stream
+ * @param {boolean} suppress
+ * @public
+ */
+
+function destroy (stream, suppress) {
+  if (isFsReadStream(stream)) {
+    destroyReadStream(stream)
+  } else if (isZlibStream(stream)) {
+    destroyZlibStream(stream)
+  } else if (hasDestroy(stream)) {
+    stream.destroy()
+  }
+
+  if (isEventEmitter(stream) && suppress) {
+    stream.removeAllListeners('error')
+    stream.addListener('error', noop)
+  }
+
+  return stream
+}
+
+/**
+ * Destroy a ReadStream.
+ *
+ * @param {object} stream
+ * @private
+ */
+
+function destroyReadStream (stream) {
+  stream.destroy()
+
+  if (typeof stream.close === 'function') {
+    // node.js core bug work-around
+    stream.on('open', onOpenClose)
+  }
+}
+
+/**
+ * Close a Zlib stream.
+ *
+ * Zlib streams below Node.js 4.5.5 have a buggy implementation
+ * of .close() when zlib encountered an error.
+ *
+ * @param {object} stream
+ * @private
+ */
+
+function closeZlibStream (stream) {
+  if (stream._hadError === true) {
+    var prop = stream._binding === null
+      ? '_binding'
+      : '_handle'
+
+    stream[prop] = {
+      close: function () { this[prop] = null }
+    }
+  }
+
+  stream.close()
+}
+
+/**
+ * Destroy a Zlib stream.
+ *
+ * Zlib streams don't have a destroy function in Node.js 6. On top of that
+ * simply calling destroy on a zlib stream in Node.js 8+ will result in a
+ * memory leak. So until that is fixed, we need to call both close AND destroy.
+ *
+ * PR to fix memory leak: https://github.com/nodejs/node/pull/23734
+ *
+ * In Node.js 6+8, it's important that destroy is called before close as the
+ * stream would otherwise emit the error 'zlib binding closed'.
+ *
+ * @param {object} stream
+ * @private
+ */
+
+function destroyZlibStream (stream) {
+  if (typeof stream.destroy === 'function') {
+    // node.js core bug work-around
+    // istanbul ignore if: node.js 0.8
+    if (stream._binding) {
+      // node.js < 0.10.0
+      stream.destroy()
+      if (stream._processing) {
+        stream._needDrain = true
+        stream.once('drain', onDrainClearBinding)
+      } else {
+        stream._binding.clear()
+      }
+    } else if (stream._destroy && stream._destroy !== Stream.Transform.prototype._destroy) {
+      // node.js >= 12, ^11.1.0, ^10.15.1
+      stream.destroy()
+    } else if (stream._destroy && typeof stream.close === 'function') {
+      // node.js 7, 8
+      stream.destroyed = true
+      stream.close()
+    } else {
+      // fallback
+      // istanbul ignore next
+      stream.destroy()
+    }
+  } else if (typeof stream.close === 'function') {
+    // node.js < 8 fallback
+    closeZlibStream(stream)
+  }
+}
+
+/**
+ * Determine if stream has destroy.
+ * @private
+ */
+
+function hasDestroy (stream) {
+  return stream instanceof Stream &&
+    typeof stream.destroy === 'function'
+}
+
+/**
+ * Determine if val is EventEmitter.
+ * @private
+ */
+
+function isEventEmitter (val) {
+  return val instanceof EventEmitter
+}
+
+/**
+ * Determine if stream is fs.ReadStream stream.
+ * @private
+ */
+
+function isFsReadStream (stream) {
+  return stream instanceof ReadStream
+}
+
+/**
+ * Determine if stream is Zlib stream.
+ * @private
+ */
+
+function isZlibStream (stream) {
+  return stream instanceof Zlib.Gzip ||
+    stream instanceof Zlib.Gunzip ||
+    stream instanceof Zlib.Deflate ||
+    stream instanceof Zlib.DeflateRaw ||
+    stream instanceof Zlib.Inflate ||
+    stream instanceof Zlib.InflateRaw ||
+    stream instanceof Zlib.Unzip
+}
+
+/**
+ * No-op function.
+ * @private
+ */
+
+function noop () {}
+
+/**
+ * On drain handler to clear binding.
+ * @private
+ */
+
+// istanbul ignore next: node.js 0.8
+function onDrainClearBinding () {
+  this._binding.clear()
+}
+
+/**
+ * On open handler to close stream.
+ * @private
+ */
+
+function onOpenClose () {
+  if (typeof this.fd === 'number') {
+    // actually close down the fd
+    this.close()
+  }
+}

+ 289 - 0
backend/node_modules/body-parser/node_modules/http-errors/index.js

@@ -0,0 +1,289 @@
+/*!
+ * http-errors
+ * Copyright(c) 2014 Jonathan Ong
+ * Copyright(c) 2016 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var deprecate = require('depd')('http-errors')
+var setPrototypeOf = require('setprototypeof')
+var statuses = require('statuses')
+var inherits = require('inherits')
+var toIdentifier = require('toidentifier')
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = createError
+module.exports.HttpError = createHttpErrorConstructor()
+module.exports.isHttpError = createIsHttpErrorFunction(module.exports.HttpError)
+
+// Populate exports for all constructors
+populateConstructorExports(module.exports, statuses.codes, module.exports.HttpError)
+
+/**
+ * Get the code class of a status code.
+ * @private
+ */
+
+function codeClass (status) {
+  return Number(String(status).charAt(0) + '00')
+}
+
+/**
+ * Create a new HTTP Error.
+ *
+ * @returns {Error}
+ * @public
+ */
+
+function createError () {
+  // so much arity going on ~_~
+  var err
+  var msg
+  var status = 500
+  var props = {}
+  for (var i = 0; i < arguments.length; i++) {
+    var arg = arguments[i]
+    var type = typeof arg
+    if (type === 'object' && arg instanceof Error) {
+      err = arg
+      status = err.status || err.statusCode || status
+    } else if (type === 'number' && i === 0) {
+      status = arg
+    } else if (type === 'string') {
+      msg = arg
+    } else if (type === 'object') {
+      props = arg
+    } else {
+      throw new TypeError('argument #' + (i + 1) + ' unsupported type ' + type)
+    }
+  }
+
+  if (typeof status === 'number' && (status < 400 || status >= 600)) {
+    deprecate('non-error status code; use only 4xx or 5xx status codes')
+  }
+
+  if (typeof status !== 'number' ||
+    (!statuses.message[status] && (status < 400 || status >= 600))) {
+    status = 500
+  }
+
+  // constructor
+  var HttpError = createError[status] || createError[codeClass(status)]
+
+  if (!err) {
+    // create error
+    err = HttpError
+      ? new HttpError(msg)
+      : new Error(msg || statuses.message[status])
+    Error.captureStackTrace(err, createError)
+  }
+
+  if (!HttpError || !(err instanceof HttpError) || err.status !== status) {
+    // add properties to generic error
+    err.expose = status < 500
+    err.status = err.statusCode = status
+  }
+
+  for (var key in props) {
+    if (key !== 'status' && key !== 'statusCode') {
+      err[key] = props[key]
+    }
+  }
+
+  return err
+}
+
+/**
+ * Create HTTP error abstract base class.
+ * @private
+ */
+
+function createHttpErrorConstructor () {
+  function HttpError () {
+    throw new TypeError('cannot construct abstract class')
+  }
+
+  inherits(HttpError, Error)
+
+  return HttpError
+}
+
+/**
+ * Create a constructor for a client error.
+ * @private
+ */
+
+function createClientErrorConstructor (HttpError, name, code) {
+  var className = toClassName(name)
+
+  function ClientError (message) {
+    // create the error object
+    var msg = message != null ? message : statuses.message[code]
+    var err = new Error(msg)
+
+    // capture a stack trace to the construction point
+    Error.captureStackTrace(err, ClientError)
+
+    // adjust the [[Prototype]]
+    setPrototypeOf(err, ClientError.prototype)
+
+    // redefine the error message
+    Object.defineProperty(err, 'message', {
+      enumerable: true,
+      configurable: true,
+      value: msg,
+      writable: true
+    })
+
+    // redefine the error name
+    Object.defineProperty(err, 'name', {
+      enumerable: false,
+      configurable: true,
+      value: className,
+      writable: true
+    })
+
+    return err
+  }
+
+  inherits(ClientError, HttpError)
+  nameFunc(ClientError, className)
+
+  ClientError.prototype.status = code
+  ClientError.prototype.statusCode = code
+  ClientError.prototype.expose = true
+
+  return ClientError
+}
+
+/**
+ * Create function to test is a value is a HttpError.
+ * @private
+ */
+
+function createIsHttpErrorFunction (HttpError) {
+  return function isHttpError (val) {
+    if (!val || typeof val !== 'object') {
+      return false
+    }
+
+    if (val instanceof HttpError) {
+      return true
+    }
+
+    return val instanceof Error &&
+      typeof val.expose === 'boolean' &&
+      typeof val.statusCode === 'number' && val.status === val.statusCode
+  }
+}
+
+/**
+ * Create a constructor for a server error.
+ * @private
+ */
+
+function createServerErrorConstructor (HttpError, name, code) {
+  var className = toClassName(name)
+
+  function ServerError (message) {
+    // create the error object
+    var msg = message != null ? message : statuses.message[code]
+    var err = new Error(msg)
+
+    // capture a stack trace to the construction point
+    Error.captureStackTrace(err, ServerError)
+
+    // adjust the [[Prototype]]
+    setPrototypeOf(err, ServerError.prototype)
+
+    // redefine the error message
+    Object.defineProperty(err, 'message', {
+      enumerable: true,
+      configurable: true,
+      value: msg,
+      writable: true
+    })
+
+    // redefine the error name
+    Object.defineProperty(err, 'name', {
+      enumerable: false,
+      configurable: true,
+      value: className,
+      writable: true
+    })
+
+    return err
+  }
+
+  inherits(ServerError, HttpError)
+  nameFunc(ServerError, className)
+
+  ServerError.prototype.status = code
+  ServerError.prototype.statusCode = code
+  ServerError.prototype.expose = false
+
+  return ServerError
+}
+
+/**
+ * Set the name of a function, if possible.
+ * @private
+ */
+
+function nameFunc (func, name) {
+  var desc = Object.getOwnPropertyDescriptor(func, 'name')
+
+  if (desc && desc.configurable) {
+    desc.value = name
+    Object.defineProperty(func, 'name', desc)
+  }
+}
+
+/**
+ * Populate the exports object with constructors for every error class.
+ * @private
+ */
+
+function populateConstructorExports (exports, codes, HttpError) {
+  codes.forEach(function forEachCode (code) {
+    var CodeError
+    var name = toIdentifier(statuses.message[code])
+
+    switch (codeClass(code)) {
+      case 400:
+        CodeError = createClientErrorConstructor(HttpError, name, code)
+        break
+      case 500:
+        CodeError = createServerErrorConstructor(HttpError, name, code)
+        break
+    }
+
+    if (CodeError) {
+      // export the constructor
+      exports[code] = CodeError
+      exports[name] = CodeError
+    }
+  })
+}
+
+/**
+ * Get a class name from a name identifier.
+ * @private
+ */
+
+function toClassName (name) {
+  return name.substr(-5) !== 'Error'
+    ? name + 'Error'
+    : name
+}

+ 234 - 0
backend/node_modules/body-parser/node_modules/on-finished/index.js

@@ -0,0 +1,234 @@
+/*!
+ * on-finished
+ * Copyright(c) 2013 Jonathan Ong
+ * Copyright(c) 2014 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = onFinished
+module.exports.isFinished = isFinished
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var asyncHooks = tryRequireAsyncHooks()
+var first = require('ee-first')
+
+/**
+ * Variables.
+ * @private
+ */
+
+/* istanbul ignore next */
+var defer = typeof setImmediate === 'function'
+  ? setImmediate
+  : function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) }
+
+/**
+ * Invoke callback when the response has finished, useful for
+ * cleaning up resources afterwards.
+ *
+ * @param {object} msg
+ * @param {function} listener
+ * @return {object}
+ * @public
+ */
+
+function onFinished (msg, listener) {
+  if (isFinished(msg) !== false) {
+    defer(listener, null, msg)
+    return msg
+  }
+
+  // attach the listener to the message
+  attachListener(msg, wrap(listener))
+
+  return msg
+}
+
+/**
+ * Determine if message is already finished.
+ *
+ * @param {object} msg
+ * @return {boolean}
+ * @public
+ */
+
+function isFinished (msg) {
+  var socket = msg.socket
+
+  if (typeof msg.finished === 'boolean') {
+    // OutgoingMessage
+    return Boolean(msg.finished || (socket && !socket.writable))
+  }
+
+  if (typeof msg.complete === 'boolean') {
+    // IncomingMessage
+    return Boolean(msg.upgrade || !socket || !socket.readable || (msg.complete && !msg.readable))
+  }
+
+  // don't know
+  return undefined
+}
+
+/**
+ * Attach a finished listener to the message.
+ *
+ * @param {object} msg
+ * @param {function} callback
+ * @private
+ */
+
+function attachFinishedListener (msg, callback) {
+  var eeMsg
+  var eeSocket
+  var finished = false
+
+  function onFinish (error) {
+    eeMsg.cancel()
+    eeSocket.cancel()
+
+    finished = true
+    callback(error)
+  }
+
+  // finished on first message event
+  eeMsg = eeSocket = first([[msg, 'end', 'finish']], onFinish)
+
+  function onSocket (socket) {
+    // remove listener
+    msg.removeListener('socket', onSocket)
+
+    if (finished) return
+    if (eeMsg !== eeSocket) return
+
+    // finished on first socket event
+    eeSocket = first([[socket, 'error', 'close']], onFinish)
+  }
+
+  if (msg.socket) {
+    // socket already assigned
+    onSocket(msg.socket)
+    return
+  }
+
+  // wait for socket to be assigned
+  msg.on('socket', onSocket)
+
+  if (msg.socket === undefined) {
+    // istanbul ignore next: node.js 0.8 patch
+    patchAssignSocket(msg, onSocket)
+  }
+}
+
+/**
+ * Attach the listener to the message.
+ *
+ * @param {object} msg
+ * @return {function}
+ * @private
+ */
+
+function attachListener (msg, listener) {
+  var attached = msg.__onFinished
+
+  // create a private single listener with queue
+  if (!attached || !attached.queue) {
+    attached = msg.__onFinished = createListener(msg)
+    attachFinishedListener(msg, attached)
+  }
+
+  attached.queue.push(listener)
+}
+
+/**
+ * Create listener on message.
+ *
+ * @param {object} msg
+ * @return {function}
+ * @private
+ */
+
+function createListener (msg) {
+  function listener (err) {
+    if (msg.__onFinished === listener) msg.__onFinished = null
+    if (!listener.queue) return
+
+    var queue = listener.queue
+    listener.queue = null
+
+    for (var i = 0; i < queue.length; i++) {
+      queue[i](err, msg)
+    }
+  }
+
+  listener.queue = []
+
+  return listener
+}
+
+/**
+ * Patch ServerResponse.prototype.assignSocket for node.js 0.8.
+ *
+ * @param {ServerResponse} res
+ * @param {function} callback
+ * @private
+ */
+
+// istanbul ignore next: node.js 0.8 patch
+function patchAssignSocket (res, callback) {
+  var assignSocket = res.assignSocket
+
+  if (typeof assignSocket !== 'function') return
+
+  // res.on('socket', callback) is broken in 0.8
+  res.assignSocket = function _assignSocket (socket) {
+    assignSocket.call(this, socket)
+    callback(socket)
+  }
+}
+
+/**
+ * Try to require async_hooks
+ * @private
+ */
+
+function tryRequireAsyncHooks () {
+  try {
+    return require('async_hooks')
+  } catch (e) {
+    return {}
+  }
+}
+
+/**
+ * Wrap function with async resource, if possible.
+ * AsyncResource.bind static method backported.
+ * @private
+ */
+
+function wrap (fn) {
+  var res
+
+  // create anonymous resource
+  if (asyncHooks.AsyncResource) {
+    res = new asyncHooks.AsyncResource(fn.name || 'bound-anonymous-fn')
+  }
+
+  // incompatible node.js
+  if (!res || !res.runInAsyncScope) {
+    return fn
+  }
+
+  // return bound function
+  return res.runInAsyncScope.bind(res, fn, null)
+}

File diff suppressed because it is too large
+ 2044 - 0
backend/node_modules/body-parser/node_modules/qs/dist/qs.js


+ 23 - 0
backend/node_modules/body-parser/node_modules/qs/lib/formats.js

@@ -0,0 +1,23 @@
+'use strict';
+
+var replace = String.prototype.replace;
+var percentTwenties = /%20/g;
+
+var Format = {
+    RFC1738: 'RFC1738',
+    RFC3986: 'RFC3986'
+};
+
+module.exports = {
+    'default': Format.RFC3986,
+    formatters: {
+        RFC1738: function (value) {
+            return replace.call(value, percentTwenties, '+');
+        },
+        RFC3986: function (value) {
+            return String(value);
+        }
+    },
+    RFC1738: Format.RFC1738,
+    RFC3986: Format.RFC3986
+};

+ 11 - 0
backend/node_modules/body-parser/node_modules/qs/lib/index.js

@@ -0,0 +1,11 @@
+'use strict';
+
+var stringify = require('./stringify');
+var parse = require('./parse');
+var formats = require('./formats');
+
+module.exports = {
+    formats: formats,
+    parse: parse,
+    stringify: stringify
+};

+ 263 - 0
backend/node_modules/body-parser/node_modules/qs/lib/parse.js

@@ -0,0 +1,263 @@
+'use strict';
+
+var utils = require('./utils');
+
+var has = Object.prototype.hasOwnProperty;
+var isArray = Array.isArray;
+
+var defaults = {
+    allowDots: false,
+    allowPrototypes: false,
+    allowSparse: false,
+    arrayLimit: 20,
+    charset: 'utf-8',
+    charsetSentinel: false,
+    comma: false,
+    decoder: utils.decode,
+    delimiter: '&',
+    depth: 5,
+    ignoreQueryPrefix: false,
+    interpretNumericEntities: false,
+    parameterLimit: 1000,
+    parseArrays: true,
+    plainObjects: false,
+    strictNullHandling: false
+};
+
+var interpretNumericEntities = function (str) {
+    return str.replace(/&#(\d+);/g, function ($0, numberStr) {
+        return String.fromCharCode(parseInt(numberStr, 10));
+    });
+};
+
+var parseArrayValue = function (val, options) {
+    if (val && typeof val === 'string' && options.comma && val.indexOf(',') > -1) {
+        return val.split(',');
+    }
+
+    return val;
+};
+
+// This is what browsers will submit when the ✓ character occurs in an
+// application/x-www-form-urlencoded body and the encoding of the page containing
+// the form is iso-8859-1, or when the submitted form has an accept-charset
+// attribute of iso-8859-1. Presumably also with other charsets that do not contain
+// the ✓ character, such as us-ascii.
+var isoSentinel = 'utf8=%26%2310003%3B'; // encodeURIComponent('&#10003;')
+
+// These are the percent-encoded utf-8 octets representing a checkmark, indicating that the request actually is utf-8 encoded.
+var charsetSentinel = 'utf8=%E2%9C%93'; // encodeURIComponent('✓')
+
+var parseValues = function parseQueryStringValues(str, options) {
+    var obj = {};
+    var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str;
+    var limit = options.parameterLimit === Infinity ? undefined : options.parameterLimit;
+    var parts = cleanStr.split(options.delimiter, limit);
+    var skipIndex = -1; // Keep track of where the utf8 sentinel was found
+    var i;
+
+    var charset = options.charset;
+    if (options.charsetSentinel) {
+        for (i = 0; i < parts.length; ++i) {
+            if (parts[i].indexOf('utf8=') === 0) {
+                if (parts[i] === charsetSentinel) {
+                    charset = 'utf-8';
+                } else if (parts[i] === isoSentinel) {
+                    charset = 'iso-8859-1';
+                }
+                skipIndex = i;
+                i = parts.length; // The eslint settings do not allow break;
+            }
+        }
+    }
+
+    for (i = 0; i < parts.length; ++i) {
+        if (i === skipIndex) {
+            continue;
+        }
+        var part = parts[i];
+
+        var bracketEqualsPos = part.indexOf(']=');
+        var pos = bracketEqualsPos === -1 ? part.indexOf('=') : bracketEqualsPos + 1;
+
+        var key, val;
+        if (pos === -1) {
+            key = options.decoder(part, defaults.decoder, charset, 'key');
+            val = options.strictNullHandling ? null : '';
+        } else {
+            key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key');
+            val = utils.maybeMap(
+                parseArrayValue(part.slice(pos + 1), options),
+                function (encodedVal) {
+                    return options.decoder(encodedVal, defaults.decoder, charset, 'value');
+                }
+            );
+        }
+
+        if (val && options.interpretNumericEntities && charset === 'iso-8859-1') {
+            val = interpretNumericEntities(val);
+        }
+
+        if (part.indexOf('[]=') > -1) {
+            val = isArray(val) ? [val] : val;
+        }
+
+        if (has.call(obj, key)) {
+            obj[key] = utils.combine(obj[key], val);
+        } else {
+            obj[key] = val;
+        }
+    }
+
+    return obj;
+};
+
+var parseObject = function (chain, val, options, valuesParsed) {
+    var leaf = valuesParsed ? val : parseArrayValue(val, options);
+
+    for (var i = chain.length - 1; i >= 0; --i) {
+        var obj;
+        var root = chain[i];
+
+        if (root === '[]' && options.parseArrays) {
+            obj = [].concat(leaf);
+        } else {
+            obj = options.plainObjects ? Object.create(null) : {};
+            var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root;
+            var index = parseInt(cleanRoot, 10);
+            if (!options.parseArrays && cleanRoot === '') {
+                obj = { 0: leaf };
+            } else if (
+                !isNaN(index)
+                && root !== cleanRoot
+                && String(index) === cleanRoot
+                && index >= 0
+                && (options.parseArrays && index <= options.arrayLimit)
+            ) {
+                obj = [];
+                obj[index] = leaf;
+            } else if (cleanRoot !== '__proto__') {
+                obj[cleanRoot] = leaf;
+            }
+        }
+
+        leaf = obj;
+    }
+
+    return leaf;
+};
+
+var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesParsed) {
+    if (!givenKey) {
+        return;
+    }
+
+    // Transform dot notation to bracket notation
+    var key = options.allowDots ? givenKey.replace(/\.([^.[]+)/g, '[$1]') : givenKey;
+
+    // The regex chunks
+
+    var brackets = /(\[[^[\]]*])/;
+    var child = /(\[[^[\]]*])/g;
+
+    // Get the parent
+
+    var segment = options.depth > 0 && brackets.exec(key);
+    var parent = segment ? key.slice(0, segment.index) : key;
+
+    // Stash the parent if it exists
+
+    var keys = [];
+    if (parent) {
+        // If we aren't using plain objects, optionally prefix keys that would overwrite object prototype properties
+        if (!options.plainObjects && has.call(Object.prototype, parent)) {
+            if (!options.allowPrototypes) {
+                return;
+            }
+        }
+
+        keys.push(parent);
+    }
+
+    // Loop through children appending to the array until we hit depth
+
+    var i = 0;
+    while (options.depth > 0 && (segment = child.exec(key)) !== null && i < options.depth) {
+        i += 1;
+        if (!options.plainObjects && has.call(Object.prototype, segment[1].slice(1, -1))) {
+            if (!options.allowPrototypes) {
+                return;
+            }
+        }
+        keys.push(segment[1]);
+    }
+
+    // If there's a remainder, just add whatever is left
+
+    if (segment) {
+        keys.push('[' + key.slice(segment.index) + ']');
+    }
+
+    return parseObject(keys, val, options, valuesParsed);
+};
+
+var normalizeParseOptions = function normalizeParseOptions(opts) {
+    if (!opts) {
+        return defaults;
+    }
+
+    if (opts.decoder !== null && opts.decoder !== undefined && typeof opts.decoder !== 'function') {
+        throw new TypeError('Decoder has to be a function.');
+    }
+
+    if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
+        throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
+    }
+    var charset = typeof opts.charset === 'undefined' ? defaults.charset : opts.charset;
+
+    return {
+        allowDots: typeof opts.allowDots === 'undefined' ? defaults.allowDots : !!opts.allowDots,
+        allowPrototypes: typeof opts.allowPrototypes === 'boolean' ? opts.allowPrototypes : defaults.allowPrototypes,
+        allowSparse: typeof opts.allowSparse === 'boolean' ? opts.allowSparse : defaults.allowSparse,
+        arrayLimit: typeof opts.arrayLimit === 'number' ? opts.arrayLimit : defaults.arrayLimit,
+        charset: charset,
+        charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel,
+        comma: typeof opts.comma === 'boolean' ? opts.comma : defaults.comma,
+        decoder: typeof opts.decoder === 'function' ? opts.decoder : defaults.decoder,
+        delimiter: typeof opts.delimiter === 'string' || utils.isRegExp(opts.delimiter) ? opts.delimiter : defaults.delimiter,
+        // eslint-disable-next-line no-implicit-coercion, no-extra-parens
+        depth: (typeof opts.depth === 'number' || opts.depth === false) ? +opts.depth : defaults.depth,
+        ignoreQueryPrefix: opts.ignoreQueryPrefix === true,
+        interpretNumericEntities: typeof opts.interpretNumericEntities === 'boolean' ? opts.interpretNumericEntities : defaults.interpretNumericEntities,
+        parameterLimit: typeof opts.parameterLimit === 'number' ? opts.parameterLimit : defaults.parameterLimit,
+        parseArrays: opts.parseArrays !== false,
+        plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects,
+        strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling
+    };
+};
+
+module.exports = function (str, opts) {
+    var options = normalizeParseOptions(opts);
+
+    if (str === '' || str === null || typeof str === 'undefined') {
+        return options.plainObjects ? Object.create(null) : {};
+    }
+
+    var tempObj = typeof str === 'string' ? parseValues(str, options) : str;
+    var obj = options.plainObjects ? Object.create(null) : {};
+
+    // Iterate over the keys and setup the new object
+
+    var keys = Object.keys(tempObj);
+    for (var i = 0; i < keys.length; ++i) {
+        var key = keys[i];
+        var newObj = parseKeys(key, tempObj[key], options, typeof str === 'string');
+        obj = utils.merge(obj, newObj, options);
+    }
+
+    if (options.allowSparse === true) {
+        return obj;
+    }
+
+    return utils.compact(obj);
+};

+ 317 - 0
backend/node_modules/body-parser/node_modules/qs/lib/stringify.js

@@ -0,0 +1,317 @@
+'use strict';
+
+var getSideChannel = require('side-channel');
+var utils = require('./utils');
+var formats = require('./formats');
+var has = Object.prototype.hasOwnProperty;
+
+var arrayPrefixGenerators = {
+    brackets: function brackets(prefix) {
+        return prefix + '[]';
+    },
+    comma: 'comma',
+    indices: function indices(prefix, key) {
+        return prefix + '[' + key + ']';
+    },
+    repeat: function repeat(prefix) {
+        return prefix;
+    }
+};
+
+var isArray = Array.isArray;
+var split = String.prototype.split;
+var push = Array.prototype.push;
+var pushToArray = function (arr, valueOrArray) {
+    push.apply(arr, isArray(valueOrArray) ? valueOrArray : [valueOrArray]);
+};
+
+var toISO = Date.prototype.toISOString;
+
+var defaultFormat = formats['default'];
+var defaults = {
+    addQueryPrefix: false,
+    allowDots: false,
+    charset: 'utf-8',
+    charsetSentinel: false,
+    delimiter: '&',
+    encode: true,
+    encoder: utils.encode,
+    encodeValuesOnly: false,
+    format: defaultFormat,
+    formatter: formats.formatters[defaultFormat],
+    // deprecated
+    indices: false,
+    serializeDate: function serializeDate(date) {
+        return toISO.call(date);
+    },
+    skipNulls: false,
+    strictNullHandling: false
+};
+
+var isNonNullishPrimitive = function isNonNullishPrimitive(v) {
+    return typeof v === 'string'
+        || typeof v === 'number'
+        || typeof v === 'boolean'
+        || typeof v === 'symbol'
+        || typeof v === 'bigint';
+};
+
+var sentinel = {};
+
+var stringify = function stringify(
+    object,
+    prefix,
+    generateArrayPrefix,
+    strictNullHandling,
+    skipNulls,
+    encoder,
+    filter,
+    sort,
+    allowDots,
+    serializeDate,
+    format,
+    formatter,
+    encodeValuesOnly,
+    charset,
+    sideChannel
+) {
+    var obj = object;
+
+    var tmpSc = sideChannel;
+    var step = 0;
+    var findFlag = false;
+    while ((tmpSc = tmpSc.get(sentinel)) !== void undefined && !findFlag) {
+        // Where object last appeared in the ref tree
+        var pos = tmpSc.get(object);
+        step += 1;
+        if (typeof pos !== 'undefined') {
+            if (pos === step) {
+                throw new RangeError('Cyclic object value');
+            } else {
+                findFlag = true; // Break while
+            }
+        }
+        if (typeof tmpSc.get(sentinel) === 'undefined') {
+            step = 0;
+        }
+    }
+
+    if (typeof filter === 'function') {
+        obj = filter(prefix, obj);
+    } else if (obj instanceof Date) {
+        obj = serializeDate(obj);
+    } else if (generateArrayPrefix === 'comma' && isArray(obj)) {
+        obj = utils.maybeMap(obj, function (value) {
+            if (value instanceof Date) {
+                return serializeDate(value);
+            }
+            return value;
+        });
+    }
+
+    if (obj === null) {
+        if (strictNullHandling) {
+            return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key', format) : prefix;
+        }
+
+        obj = '';
+    }
+
+    if (isNonNullishPrimitive(obj) || utils.isBuffer(obj)) {
+        if (encoder) {
+            var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder, charset, 'key', format);
+            if (generateArrayPrefix === 'comma' && encodeValuesOnly) {
+                var valuesArray = split.call(String(obj), ',');
+                var valuesJoined = '';
+                for (var i = 0; i < valuesArray.length; ++i) {
+                    valuesJoined += (i === 0 ? '' : ',') + formatter(encoder(valuesArray[i], defaults.encoder, charset, 'value', format));
+                }
+                return [formatter(keyValue) + '=' + valuesJoined];
+            }
+            return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder, charset, 'value', format))];
+        }
+        return [formatter(prefix) + '=' + formatter(String(obj))];
+    }
+
+    var values = [];
+
+    if (typeof obj === 'undefined') {
+        return values;
+    }
+
+    var objKeys;
+    if (generateArrayPrefix === 'comma' && isArray(obj)) {
+        // we need to join elements in
+        objKeys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }];
+    } else if (isArray(filter)) {
+        objKeys = filter;
+    } else {
+        var keys = Object.keys(obj);
+        objKeys = sort ? keys.sort(sort) : keys;
+    }
+
+    for (var j = 0; j < objKeys.length; ++j) {
+        var key = objKeys[j];
+        var value = typeof key === 'object' && typeof key.value !== 'undefined' ? key.value : obj[key];
+
+        if (skipNulls && value === null) {
+            continue;
+        }
+
+        var keyPrefix = isArray(obj)
+            ? typeof generateArrayPrefix === 'function' ? generateArrayPrefix(prefix, key) : prefix
+            : prefix + (allowDots ? '.' + key : '[' + key + ']');
+
+        sideChannel.set(object, step);
+        var valueSideChannel = getSideChannel();
+        valueSideChannel.set(sentinel, sideChannel);
+        pushToArray(values, stringify(
+            value,
+            keyPrefix,
+            generateArrayPrefix,
+            strictNullHandling,
+            skipNulls,
+            encoder,
+            filter,
+            sort,
+            allowDots,
+            serializeDate,
+            format,
+            formatter,
+            encodeValuesOnly,
+            charset,
+            valueSideChannel
+        ));
+    }
+
+    return values;
+};
+
+var normalizeStringifyOptions = function normalizeStringifyOptions(opts) {
+    if (!opts) {
+        return defaults;
+    }
+
+    if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') {
+        throw new TypeError('Encoder has to be a function.');
+    }
+
+    var charset = opts.charset || defaults.charset;
+    if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
+        throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
+    }
+
+    var format = formats['default'];
+    if (typeof opts.format !== 'undefined') {
+        if (!has.call(formats.formatters, opts.format)) {
+            throw new TypeError('Unknown format option provided.');
+        }
+        format = opts.format;
+    }
+    var formatter = formats.formatters[format];
+
+    var filter = defaults.filter;
+    if (typeof opts.filter === 'function' || isArray(opts.filter)) {
+        filter = opts.filter;
+    }
+
+    return {
+        addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix,
+        allowDots: typeof opts.allowDots === 'undefined' ? defaults.allowDots : !!opts.allowDots,
+        charset: charset,
+        charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel,
+        delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter,
+        encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode,
+        encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder,
+        encodeValuesOnly: typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly,
+        filter: filter,
+        format: format,
+        formatter: formatter,
+        serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate,
+        skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls,
+        sort: typeof opts.sort === 'function' ? opts.sort : null,
+        strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling
+    };
+};
+
+module.exports = function (object, opts) {
+    var obj = object;
+    var options = normalizeStringifyOptions(opts);
+
+    var objKeys;
+    var filter;
+
+    if (typeof options.filter === 'function') {
+        filter = options.filter;
+        obj = filter('', obj);
+    } else if (isArray(options.filter)) {
+        filter = options.filter;
+        objKeys = filter;
+    }
+
+    var keys = [];
+
+    if (typeof obj !== 'object' || obj === null) {
+        return '';
+    }
+
+    var arrayFormat;
+    if (opts && opts.arrayFormat in arrayPrefixGenerators) {
+        arrayFormat = opts.arrayFormat;
+    } else if (opts && 'indices' in opts) {
+        arrayFormat = opts.indices ? 'indices' : 'repeat';
+    } else {
+        arrayFormat = 'indices';
+    }
+
+    var generateArrayPrefix = arrayPrefixGenerators[arrayFormat];
+
+    if (!objKeys) {
+        objKeys = Object.keys(obj);
+    }
+
+    if (options.sort) {
+        objKeys.sort(options.sort);
+    }
+
+    var sideChannel = getSideChannel();
+    for (var i = 0; i < objKeys.length; ++i) {
+        var key = objKeys[i];
+
+        if (options.skipNulls && obj[key] === null) {
+            continue;
+        }
+        pushToArray(keys, stringify(
+            obj[key],
+            key,
+            generateArrayPrefix,
+            options.strictNullHandling,
+            options.skipNulls,
+            options.encode ? options.encoder : null,
+            options.filter,
+            options.sort,
+            options.allowDots,
+            options.serializeDate,
+            options.format,
+            options.formatter,
+            options.encodeValuesOnly,
+            options.charset,
+            sideChannel
+        ));
+    }
+
+    var joined = keys.join(options.delimiter);
+    var prefix = options.addQueryPrefix === true ? '?' : '';
+
+    if (options.charsetSentinel) {
+        if (options.charset === 'iso-8859-1') {
+            // encodeURIComponent('&#10003;'), the "numeric entity" representation of a checkmark
+            prefix += 'utf8=%26%2310003%3B&';
+        } else {
+            // encodeURIComponent('✓')
+            prefix += 'utf8=%E2%9C%93&';
+        }
+    }
+
+    return joined.length > 0 ? prefix + joined : '';
+};

+ 252 - 0
backend/node_modules/body-parser/node_modules/qs/lib/utils.js

@@ -0,0 +1,252 @@
+'use strict';
+
+var formats = require('./formats');
+
+var has = Object.prototype.hasOwnProperty;
+var isArray = Array.isArray;
+
+var hexTable = (function () {
+    var array = [];
+    for (var i = 0; i < 256; ++i) {
+        array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase());
+    }
+
+    return array;
+}());
+
+var compactQueue = function compactQueue(queue) {
+    while (queue.length > 1) {
+        var item = queue.pop();
+        var obj = item.obj[item.prop];
+
+        if (isArray(obj)) {
+            var compacted = [];
+
+            for (var j = 0; j < obj.length; ++j) {
+                if (typeof obj[j] !== 'undefined') {
+                    compacted.push(obj[j]);
+                }
+            }
+
+            item.obj[item.prop] = compacted;
+        }
+    }
+};
+
+var arrayToObject = function arrayToObject(source, options) {
+    var obj = options && options.plainObjects ? Object.create(null) : {};
+    for (var i = 0; i < source.length; ++i) {
+        if (typeof source[i] !== 'undefined') {
+            obj[i] = source[i];
+        }
+    }
+
+    return obj;
+};
+
+var merge = function merge(target, source, options) {
+    /* eslint no-param-reassign: 0 */
+    if (!source) {
+        return target;
+    }
+
+    if (typeof source !== 'object') {
+        if (isArray(target)) {
+            target.push(source);
+        } else if (target && typeof target === 'object') {
+            if ((options && (options.plainObjects || options.allowPrototypes)) || !has.call(Object.prototype, source)) {
+                target[source] = true;
+            }
+        } else {
+            return [target, source];
+        }
+
+        return target;
+    }
+
+    if (!target || typeof target !== 'object') {
+        return [target].concat(source);
+    }
+
+    var mergeTarget = target;
+    if (isArray(target) && !isArray(source)) {
+        mergeTarget = arrayToObject(target, options);
+    }
+
+    if (isArray(target) && isArray(source)) {
+        source.forEach(function (item, i) {
+            if (has.call(target, i)) {
+                var targetItem = target[i];
+                if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') {
+                    target[i] = merge(targetItem, item, options);
+                } else {
+                    target.push(item);
+                }
+            } else {
+                target[i] = item;
+            }
+        });
+        return target;
+    }
+
+    return Object.keys(source).reduce(function (acc, key) {
+        var value = source[key];
+
+        if (has.call(acc, key)) {
+            acc[key] = merge(acc[key], value, options);
+        } else {
+            acc[key] = value;
+        }
+        return acc;
+    }, mergeTarget);
+};
+
+var assign = function assignSingleSource(target, source) {
+    return Object.keys(source).reduce(function (acc, key) {
+        acc[key] = source[key];
+        return acc;
+    }, target);
+};
+
+var decode = function (str, decoder, charset) {
+    var strWithoutPlus = str.replace(/\+/g, ' ');
+    if (charset === 'iso-8859-1') {
+        // unescape never throws, no try...catch needed:
+        return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape);
+    }
+    // utf-8
+    try {
+        return decodeURIComponent(strWithoutPlus);
+    } catch (e) {
+        return strWithoutPlus;
+    }
+};
+
+var encode = function encode(str, defaultEncoder, charset, kind, format) {
+    // This code was originally written by Brian White (mscdex) for the io.js core querystring library.
+    // It has been adapted here for stricter adherence to RFC 3986
+    if (str.length === 0) {
+        return str;
+    }
+
+    var string = str;
+    if (typeof str === 'symbol') {
+        string = Symbol.prototype.toString.call(str);
+    } else if (typeof str !== 'string') {
+        string = String(str);
+    }
+
+    if (charset === 'iso-8859-1') {
+        return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) {
+            return '%26%23' + parseInt($0.slice(2), 16) + '%3B';
+        });
+    }
+
+    var out = '';
+    for (var i = 0; i < string.length; ++i) {
+        var c = string.charCodeAt(i);
+
+        if (
+            c === 0x2D // -
+            || c === 0x2E // .
+            || c === 0x5F // _
+            || c === 0x7E // ~
+            || (c >= 0x30 && c <= 0x39) // 0-9
+            || (c >= 0x41 && c <= 0x5A) // a-z
+            || (c >= 0x61 && c <= 0x7A) // A-Z
+            || (format === formats.RFC1738 && (c === 0x28 || c === 0x29)) // ( )
+        ) {
+            out += string.charAt(i);
+            continue;
+        }
+
+        if (c < 0x80) {
+            out = out + hexTable[c];
+            continue;
+        }
+
+        if (c < 0x800) {
+            out = out + (hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)]);
+            continue;
+        }
+
+        if (c < 0xD800 || c >= 0xE000) {
+            out = out + (hexTable[0xE0 | (c >> 12)] + hexTable[0x80 | ((c >> 6) & 0x3F)] + hexTable[0x80 | (c & 0x3F)]);
+            continue;
+        }
+
+        i += 1;
+        c = 0x10000 + (((c & 0x3FF) << 10) | (string.charCodeAt(i) & 0x3FF));
+        /* eslint operator-linebreak: [2, "before"] */
+        out += hexTable[0xF0 | (c >> 18)]
+            + hexTable[0x80 | ((c >> 12) & 0x3F)]
+            + hexTable[0x80 | ((c >> 6) & 0x3F)]
+            + hexTable[0x80 | (c & 0x3F)];
+    }
+
+    return out;
+};
+
+var compact = function compact(value) {
+    var queue = [{ obj: { o: value }, prop: 'o' }];
+    var refs = [];
+
+    for (var i = 0; i < queue.length; ++i) {
+        var item = queue[i];
+        var obj = item.obj[item.prop];
+
+        var keys = Object.keys(obj);
+        for (var j = 0; j < keys.length; ++j) {
+            var key = keys[j];
+            var val = obj[key];
+            if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) {
+                queue.push({ obj: obj, prop: key });
+                refs.push(val);
+            }
+        }
+    }
+
+    compactQueue(queue);
+
+    return value;
+};
+
+var isRegExp = function isRegExp(obj) {
+    return Object.prototype.toString.call(obj) === '[object RegExp]';
+};
+
+var isBuffer = function isBuffer(obj) {
+    if (!obj || typeof obj !== 'object') {
+        return false;
+    }
+
+    return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj));
+};
+
+var combine = function combine(a, b) {
+    return [].concat(a, b);
+};
+
+var maybeMap = function maybeMap(val, fn) {
+    if (isArray(val)) {
+        var mapped = [];
+        for (var i = 0; i < val.length; i += 1) {
+            mapped.push(fn(val[i]));
+        }
+        return mapped;
+    }
+    return fn(val);
+};
+
+module.exports = {
+    arrayToObject: arrayToObject,
+    assign: assign,
+    combine: combine,
+    compact: compact,
+    decode: decode,
+    encode: encode,
+    isBuffer: isBuffer,
+    isRegExp: isRegExp,
+    maybeMap: maybeMap,
+    merge: merge
+};

+ 841 - 0
backend/node_modules/body-parser/node_modules/qs/test/parse.js

@@ -0,0 +1,841 @@
+'use strict';
+
+var test = require('tape');
+var qs = require('../');
+var utils = require('../lib/utils');
+var iconv = require('iconv-lite');
+var SaferBuffer = require('safer-buffer').Buffer;
+
+test('parse()', function (t) {
+    t.test('parses a simple string', function (st) {
+        st.deepEqual(qs.parse('0=foo'), { 0: 'foo' });
+        st.deepEqual(qs.parse('foo=c++'), { foo: 'c  ' });
+        st.deepEqual(qs.parse('a[>=]=23'), { a: { '>=': '23' } });
+        st.deepEqual(qs.parse('a[<=>]==23'), { a: { '<=>': '=23' } });
+        st.deepEqual(qs.parse('a[==]=23'), { a: { '==': '23' } });
+        st.deepEqual(qs.parse('foo', { strictNullHandling: true }), { foo: null });
+        st.deepEqual(qs.parse('foo'), { foo: '' });
+        st.deepEqual(qs.parse('foo='), { foo: '' });
+        st.deepEqual(qs.parse('foo=bar'), { foo: 'bar' });
+        st.deepEqual(qs.parse(' foo = bar = baz '), { ' foo ': ' bar = baz ' });
+        st.deepEqual(qs.parse('foo=bar=baz'), { foo: 'bar=baz' });
+        st.deepEqual(qs.parse('foo=bar&bar=baz'), { foo: 'bar', bar: 'baz' });
+        st.deepEqual(qs.parse('foo2=bar2&baz2='), { foo2: 'bar2', baz2: '' });
+        st.deepEqual(qs.parse('foo=bar&baz', { strictNullHandling: true }), { foo: 'bar', baz: null });
+        st.deepEqual(qs.parse('foo=bar&baz'), { foo: 'bar', baz: '' });
+        st.deepEqual(qs.parse('cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World'), {
+            cht: 'p3',
+            chd: 't:60,40',
+            chs: '250x100',
+            chl: 'Hello|World'
+        });
+        st.end();
+    });
+
+    t.test('arrayFormat: brackets allows only explicit arrays', function (st) {
+        st.deepEqual(qs.parse('a[]=b&a[]=c', { arrayFormat: 'brackets' }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayFormat: 'brackets' }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b,c', { arrayFormat: 'brackets' }), { a: 'b,c' });
+        st.deepEqual(qs.parse('a=b&a=c', { arrayFormat: 'brackets' }), { a: ['b', 'c'] });
+        st.end();
+    });
+
+    t.test('arrayFormat: indices allows only indexed arrays', function (st) {
+        st.deepEqual(qs.parse('a[]=b&a[]=c', { arrayFormat: 'indices' }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayFormat: 'indices' }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b,c', { arrayFormat: 'indices' }), { a: 'b,c' });
+        st.deepEqual(qs.parse('a=b&a=c', { arrayFormat: 'indices' }), { a: ['b', 'c'] });
+        st.end();
+    });
+
+    t.test('arrayFormat: comma allows only comma-separated arrays', function (st) {
+        st.deepEqual(qs.parse('a[]=b&a[]=c', { arrayFormat: 'comma' }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayFormat: 'comma' }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b,c', { arrayFormat: 'comma' }), { a: 'b,c' });
+        st.deepEqual(qs.parse('a=b&a=c', { arrayFormat: 'comma' }), { a: ['b', 'c'] });
+        st.end();
+    });
+
+    t.test('arrayFormat: repeat allows only repeated values', function (st) {
+        st.deepEqual(qs.parse('a[]=b&a[]=c', { arrayFormat: 'repeat' }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayFormat: 'repeat' }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b,c', { arrayFormat: 'repeat' }), { a: 'b,c' });
+        st.deepEqual(qs.parse('a=b&a=c', { arrayFormat: 'repeat' }), { a: ['b', 'c'] });
+        st.end();
+    });
+
+    t.test('allows enabling dot notation', function (st) {
+        st.deepEqual(qs.parse('a.b=c'), { 'a.b': 'c' });
+        st.deepEqual(qs.parse('a.b=c', { allowDots: true }), { a: { b: 'c' } });
+        st.end();
+    });
+
+    t.deepEqual(qs.parse('a[b]=c'), { a: { b: 'c' } }, 'parses a single nested string');
+    t.deepEqual(qs.parse('a[b][c]=d'), { a: { b: { c: 'd' } } }, 'parses a double nested string');
+    t.deepEqual(
+        qs.parse('a[b][c][d][e][f][g][h]=i'),
+        { a: { b: { c: { d: { e: { f: { '[g][h]': 'i' } } } } } } },
+        'defaults to a depth of 5'
+    );
+
+    t.test('only parses one level when depth = 1', function (st) {
+        st.deepEqual(qs.parse('a[b][c]=d', { depth: 1 }), { a: { b: { '[c]': 'd' } } });
+        st.deepEqual(qs.parse('a[b][c][d]=e', { depth: 1 }), { a: { b: { '[c][d]': 'e' } } });
+        st.end();
+    });
+
+    t.test('uses original key when depth = 0', function (st) {
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: 0 }), { 'a[0]': 'b', 'a[1]': 'c' });
+        st.deepEqual(qs.parse('a[0][0]=b&a[0][1]=c&a[1]=d&e=2', { depth: 0 }), { 'a[0][0]': 'b', 'a[0][1]': 'c', 'a[1]': 'd', e: '2' });
+        st.end();
+    });
+
+    t.test('uses original key when depth = false', function (st) {
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: false }), { 'a[0]': 'b', 'a[1]': 'c' });
+        st.deepEqual(qs.parse('a[0][0]=b&a[0][1]=c&a[1]=d&e=2', { depth: false }), { 'a[0][0]': 'b', 'a[0][1]': 'c', 'a[1]': 'd', e: '2' });
+        st.end();
+    });
+
+    t.deepEqual(qs.parse('a=b&a=c'), { a: ['b', 'c'] }, 'parses a simple array');
+
+    t.test('parses an explicit array', function (st) {
+        st.deepEqual(qs.parse('a[]=b'), { a: ['b'] });
+        st.deepEqual(qs.parse('a[]=b&a[]=c'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[]=b&a[]=c&a[]=d'), { a: ['b', 'c', 'd'] });
+        st.end();
+    });
+
+    t.test('parses a mix of simple and explicit arrays', function (st) {
+        st.deepEqual(qs.parse('a=b&a[]=c'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[]=b&a=c'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[0]=b&a=c'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b&a[0]=c'), { a: ['b', 'c'] });
+
+        st.deepEqual(qs.parse('a[1]=b&a=c', { arrayLimit: 20 }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[]=b&a=c', { arrayLimit: 0 }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[]=b&a=c'), { a: ['b', 'c'] });
+
+        st.deepEqual(qs.parse('a=b&a[1]=c', { arrayLimit: 20 }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b&a[]=c', { arrayLimit: 0 }), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a=b&a[]=c'), { a: ['b', 'c'] });
+
+        st.end();
+    });
+
+    t.test('parses a nested array', function (st) {
+        st.deepEqual(qs.parse('a[b][]=c&a[b][]=d'), { a: { b: ['c', 'd'] } });
+        st.deepEqual(qs.parse('a[>=]=25'), { a: { '>=': '25' } });
+        st.end();
+    });
+
+    t.test('allows to specify array indices', function (st) {
+        st.deepEqual(qs.parse('a[1]=c&a[0]=b&a[2]=d'), { a: ['b', 'c', 'd'] });
+        st.deepEqual(qs.parse('a[1]=c&a[0]=b'), { a: ['b', 'c'] });
+        st.deepEqual(qs.parse('a[1]=c', { arrayLimit: 20 }), { a: ['c'] });
+        st.deepEqual(qs.parse('a[1]=c', { arrayLimit: 0 }), { a: { 1: 'c' } });
+        st.deepEqual(qs.parse('a[1]=c'), { a: ['c'] });
+        st.end();
+    });
+
+    t.test('limits specific array indices to arrayLimit', function (st) {
+        st.deepEqual(qs.parse('a[20]=a', { arrayLimit: 20 }), { a: ['a'] });
+        st.deepEqual(qs.parse('a[21]=a', { arrayLimit: 20 }), { a: { 21: 'a' } });
+        st.end();
+    });
+
+    t.deepEqual(qs.parse('a[12b]=c'), { a: { '12b': 'c' } }, 'supports keys that begin with a number');
+
+    t.test('supports encoded = signs', function (st) {
+        st.deepEqual(qs.parse('he%3Dllo=th%3Dere'), { 'he=llo': 'th=ere' });
+        st.end();
+    });
+
+    t.test('is ok with url encoded strings', function (st) {
+        st.deepEqual(qs.parse('a[b%20c]=d'), { a: { 'b c': 'd' } });
+        st.deepEqual(qs.parse('a[b]=c%20d'), { a: { b: 'c d' } });
+        st.end();
+    });
+
+    t.test('allows brackets in the value', function (st) {
+        st.deepEqual(qs.parse('pets=["tobi"]'), { pets: '["tobi"]' });
+        st.deepEqual(qs.parse('operators=[">=", "<="]'), { operators: '[">=", "<="]' });
+        st.end();
+    });
+
+    t.test('allows empty values', function (st) {
+        st.deepEqual(qs.parse(''), {});
+        st.deepEqual(qs.parse(null), {});
+        st.deepEqual(qs.parse(undefined), {});
+        st.end();
+    });
+
+    t.test('transforms arrays to objects', function (st) {
+        st.deepEqual(qs.parse('foo[0]=bar&foo[bad]=baz'), { foo: { 0: 'bar', bad: 'baz' } });
+        st.deepEqual(qs.parse('foo[bad]=baz&foo[0]=bar'), { foo: { bad: 'baz', 0: 'bar' } });
+        st.deepEqual(qs.parse('foo[bad]=baz&foo[]=bar'), { foo: { bad: 'baz', 0: 'bar' } });
+        st.deepEqual(qs.parse('foo[]=bar&foo[bad]=baz'), { foo: { 0: 'bar', bad: 'baz' } });
+        st.deepEqual(qs.parse('foo[bad]=baz&foo[]=bar&foo[]=foo'), { foo: { bad: 'baz', 0: 'bar', 1: 'foo' } });
+        st.deepEqual(qs.parse('foo[0][a]=a&foo[0][b]=b&foo[1][a]=aa&foo[1][b]=bb'), { foo: [{ a: 'a', b: 'b' }, { a: 'aa', b: 'bb' }] });
+
+        st.deepEqual(qs.parse('a[]=b&a[t]=u&a[hasOwnProperty]=c', { allowPrototypes: false }), { a: { 0: 'b', t: 'u' } });
+        st.deepEqual(qs.parse('a[]=b&a[t]=u&a[hasOwnProperty]=c', { allowPrototypes: true }), { a: { 0: 'b', t: 'u', hasOwnProperty: 'c' } });
+        st.deepEqual(qs.parse('a[]=b&a[hasOwnProperty]=c&a[x]=y', { allowPrototypes: false }), { a: { 0: 'b', x: 'y' } });
+        st.deepEqual(qs.parse('a[]=b&a[hasOwnProperty]=c&a[x]=y', { allowPrototypes: true }), { a: { 0: 'b', hasOwnProperty: 'c', x: 'y' } });
+        st.end();
+    });
+
+    t.test('transforms arrays to objects (dot notation)', function (st) {
+        st.deepEqual(qs.parse('foo[0].baz=bar&fool.bad=baz', { allowDots: true }), { foo: [{ baz: 'bar' }], fool: { bad: 'baz' } });
+        st.deepEqual(qs.parse('foo[0].baz=bar&fool.bad.boo=baz', { allowDots: true }), { foo: [{ baz: 'bar' }], fool: { bad: { boo: 'baz' } } });
+        st.deepEqual(qs.parse('foo[0][0].baz=bar&fool.bad=baz', { allowDots: true }), { foo: [[{ baz: 'bar' }]], fool: { bad: 'baz' } });
+        st.deepEqual(qs.parse('foo[0].baz[0]=15&foo[0].bar=2', { allowDots: true }), { foo: [{ baz: ['15'], bar: '2' }] });
+        st.deepEqual(qs.parse('foo[0].baz[0]=15&foo[0].baz[1]=16&foo[0].bar=2', { allowDots: true }), { foo: [{ baz: ['15', '16'], bar: '2' }] });
+        st.deepEqual(qs.parse('foo.bad=baz&foo[0]=bar', { allowDots: true }), { foo: { bad: 'baz', 0: 'bar' } });
+        st.deepEqual(qs.parse('foo.bad=baz&foo[]=bar', { allowDots: true }), { foo: { bad: 'baz', 0: 'bar' } });
+        st.deepEqual(qs.parse('foo[]=bar&foo.bad=baz', { allowDots: true }), { foo: { 0: 'bar', bad: 'baz' } });
+        st.deepEqual(qs.parse('foo.bad=baz&foo[]=bar&foo[]=foo', { allowDots: true }), { foo: { bad: 'baz', 0: 'bar', 1: 'foo' } });
+        st.deepEqual(qs.parse('foo[0].a=a&foo[0].b=b&foo[1].a=aa&foo[1].b=bb', { allowDots: true }), { foo: [{ a: 'a', b: 'b' }, { a: 'aa', b: 'bb' }] });
+        st.end();
+    });
+
+    t.test('correctly prunes undefined values when converting an array to an object', function (st) {
+        st.deepEqual(qs.parse('a[2]=b&a[99999999]=c'), { a: { 2: 'b', 99999999: 'c' } });
+        st.end();
+    });
+
+    t.test('supports malformed uri characters', function (st) {
+        st.deepEqual(qs.parse('{%:%}', { strictNullHandling: true }), { '{%:%}': null });
+        st.deepEqual(qs.parse('{%:%}='), { '{%:%}': '' });
+        st.deepEqual(qs.parse('foo=%:%}'), { foo: '%:%}' });
+        st.end();
+    });
+
+    t.test('doesn\'t produce empty keys', function (st) {
+        st.deepEqual(qs.parse('_r=1&'), { _r: '1' });
+        st.end();
+    });
+
+    t.test('cannot access Object prototype', function (st) {
+        qs.parse('constructor[prototype][bad]=bad');
+        qs.parse('bad[constructor][prototype][bad]=bad');
+        st.equal(typeof Object.prototype.bad, 'undefined');
+        st.end();
+    });
+
+    t.test('parses arrays of objects', function (st) {
+        st.deepEqual(qs.parse('a[][b]=c'), { a: [{ b: 'c' }] });
+        st.deepEqual(qs.parse('a[0][b]=c'), { a: [{ b: 'c' }] });
+        st.end();
+    });
+
+    t.test('allows for empty strings in arrays', function (st) {
+        st.deepEqual(qs.parse('a[]=b&a[]=&a[]=c'), { a: ['b', '', 'c'] });
+
+        st.deepEqual(
+            qs.parse('a[0]=b&a[1]&a[2]=c&a[19]=', { strictNullHandling: true, arrayLimit: 20 }),
+            { a: ['b', null, 'c', ''] },
+            'with arrayLimit 20 + array indices: null then empty string works'
+        );
+        st.deepEqual(
+            qs.parse('a[]=b&a[]&a[]=c&a[]=', { strictNullHandling: true, arrayLimit: 0 }),
+            { a: ['b', null, 'c', ''] },
+            'with arrayLimit 0 + array brackets: null then empty string works'
+        );
+
+        st.deepEqual(
+            qs.parse('a[0]=b&a[1]=&a[2]=c&a[19]', { strictNullHandling: true, arrayLimit: 20 }),
+            { a: ['b', '', 'c', null] },
+            'with arrayLimit 20 + array indices: empty string then null works'
+        );
+        st.deepEqual(
+            qs.parse('a[]=b&a[]=&a[]=c&a[]', { strictNullHandling: true, arrayLimit: 0 }),
+            { a: ['b', '', 'c', null] },
+            'with arrayLimit 0 + array brackets: empty string then null works'
+        );
+
+        st.deepEqual(
+            qs.parse('a[]=&a[]=b&a[]=c'),
+            { a: ['', 'b', 'c'] },
+            'array brackets: empty strings work'
+        );
+        st.end();
+    });
+
+    t.test('compacts sparse arrays', function (st) {
+        st.deepEqual(qs.parse('a[10]=1&a[2]=2', { arrayLimit: 20 }), { a: ['2', '1'] });
+        st.deepEqual(qs.parse('a[1][b][2][c]=1', { arrayLimit: 20 }), { a: [{ b: [{ c: '1' }] }] });
+        st.deepEqual(qs.parse('a[1][2][3][c]=1', { arrayLimit: 20 }), { a: [[[{ c: '1' }]]] });
+        st.deepEqual(qs.parse('a[1][2][3][c][1]=1', { arrayLimit: 20 }), { a: [[[{ c: ['1'] }]]] });
+        st.end();
+    });
+
+    t.test('parses sparse arrays', function (st) {
+        /* eslint no-sparse-arrays: 0 */
+        st.deepEqual(qs.parse('a[4]=1&a[1]=2', { allowSparse: true }), { a: [, '2', , , '1'] });
+        st.deepEqual(qs.parse('a[1][b][2][c]=1', { allowSparse: true }), { a: [, { b: [, , { c: '1' }] }] });
+        st.deepEqual(qs.parse('a[1][2][3][c]=1', { allowSparse: true }), { a: [, [, , [, , , { c: '1' }]]] });
+        st.deepEqual(qs.parse('a[1][2][3][c][1]=1', { allowSparse: true }), { a: [, [, , [, , , { c: [, '1'] }]]] });
+        st.end();
+    });
+
+    t.test('parses semi-parsed strings', function (st) {
+        st.deepEqual(qs.parse({ 'a[b]': 'c' }), { a: { b: 'c' } });
+        st.deepEqual(qs.parse({ 'a[b]': 'c', 'a[d]': 'e' }), { a: { b: 'c', d: 'e' } });
+        st.end();
+    });
+
+    t.test('parses buffers correctly', function (st) {
+        var b = SaferBuffer.from('test');
+        st.deepEqual(qs.parse({ a: b }), { a: b });
+        st.end();
+    });
+
+    t.test('parses jquery-param strings', function (st) {
+        // readable = 'filter[0][]=int1&filter[0][]==&filter[0][]=77&filter[]=and&filter[2][]=int2&filter[2][]==&filter[2][]=8'
+        var encoded = 'filter%5B0%5D%5B%5D=int1&filter%5B0%5D%5B%5D=%3D&filter%5B0%5D%5B%5D=77&filter%5B%5D=and&filter%5B2%5D%5B%5D=int2&filter%5B2%5D%5B%5D=%3D&filter%5B2%5D%5B%5D=8';
+        var expected = { filter: [['int1', '=', '77'], 'and', ['int2', '=', '8']] };
+        st.deepEqual(qs.parse(encoded), expected);
+        st.end();
+    });
+
+    t.test('continues parsing when no parent is found', function (st) {
+        st.deepEqual(qs.parse('[]=&a=b'), { 0: '', a: 'b' });
+        st.deepEqual(qs.parse('[]&a=b', { strictNullHandling: true }), { 0: null, a: 'b' });
+        st.deepEqual(qs.parse('[foo]=bar'), { foo: 'bar' });
+        st.end();
+    });
+
+    t.test('does not error when parsing a very long array', function (st) {
+        var str = 'a[]=a';
+        while (Buffer.byteLength(str) < 128 * 1024) {
+            str = str + '&' + str;
+        }
+
+        st.doesNotThrow(function () {
+            qs.parse(str);
+        });
+
+        st.end();
+    });
+
+    t.test('should not throw when a native prototype has an enumerable property', function (st) {
+        Object.prototype.crash = '';
+        Array.prototype.crash = '';
+        st.doesNotThrow(qs.parse.bind(null, 'a=b'));
+        st.deepEqual(qs.parse('a=b'), { a: 'b' });
+        st.doesNotThrow(qs.parse.bind(null, 'a[][b]=c'));
+        st.deepEqual(qs.parse('a[][b]=c'), { a: [{ b: 'c' }] });
+        delete Object.prototype.crash;
+        delete Array.prototype.crash;
+        st.end();
+    });
+
+    t.test('parses a string with an alternative string delimiter', function (st) {
+        st.deepEqual(qs.parse('a=b;c=d', { delimiter: ';' }), { a: 'b', c: 'd' });
+        st.end();
+    });
+
+    t.test('parses a string with an alternative RegExp delimiter', function (st) {
+        st.deepEqual(qs.parse('a=b; c=d', { delimiter: /[;,] */ }), { a: 'b', c: 'd' });
+        st.end();
+    });
+
+    t.test('does not use non-splittable objects as delimiters', function (st) {
+        st.deepEqual(qs.parse('a=b&c=d', { delimiter: true }), { a: 'b', c: 'd' });
+        st.end();
+    });
+
+    t.test('allows overriding parameter limit', function (st) {
+        st.deepEqual(qs.parse('a=b&c=d', { parameterLimit: 1 }), { a: 'b' });
+        st.end();
+    });
+
+    t.test('allows setting the parameter limit to Infinity', function (st) {
+        st.deepEqual(qs.parse('a=b&c=d', { parameterLimit: Infinity }), { a: 'b', c: 'd' });
+        st.end();
+    });
+
+    t.test('allows overriding array limit', function (st) {
+        st.deepEqual(qs.parse('a[0]=b', { arrayLimit: -1 }), { a: { 0: 'b' } });
+        st.deepEqual(qs.parse('a[-1]=b', { arrayLimit: -1 }), { a: { '-1': 'b' } });
+        st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayLimit: 0 }), { a: { 0: 'b', 1: 'c' } });
+        st.end();
+    });
+
+    t.test('allows disabling array parsing', function (st) {
+        var indices = qs.parse('a[0]=b&a[1]=c', { parseArrays: false });
+        st.deepEqual(indices, { a: { 0: 'b', 1: 'c' } });
+        st.equal(Array.isArray(indices.a), false, 'parseArrays:false, indices case is not an array');
+
+        var emptyBrackets = qs.parse('a[]=b', { parseArrays: false });
+        st.deepEqual(emptyBrackets, { a: { 0: 'b' } });
+        st.equal(Array.isArray(emptyBrackets.a), false, 'parseArrays:false, empty brackets case is not an array');
+
+        st.end();
+    });
+
+    t.test('allows for query string prefix', function (st) {
+        st.deepEqual(qs.parse('?foo=bar', { ignoreQueryPrefix: true }), { foo: 'bar' });
+        st.deepEqual(qs.parse('foo=bar', { ignoreQueryPrefix: true }), { foo: 'bar' });
+        st.deepEqual(qs.parse('?foo=bar', { ignoreQueryPrefix: false }), { '?foo': 'bar' });
+        st.end();
+    });
+
+    t.test('parses an object', function (st) {
+        var input = {
+            'user[name]': { 'pop[bob]': 3 },
+            'user[email]': null
+        };
+
+        var expected = {
+            user: {
+                name: { 'pop[bob]': 3 },
+                email: null
+            }
+        };
+
+        var result = qs.parse(input);
+
+        st.deepEqual(result, expected);
+        st.end();
+    });
+
+    t.test('parses string with comma as array divider', function (st) {
+        st.deepEqual(qs.parse('foo=bar,tee', { comma: true }), { foo: ['bar', 'tee'] });
+        st.deepEqual(qs.parse('foo[bar]=coffee,tee', { comma: true }), { foo: { bar: ['coffee', 'tee'] } });
+        st.deepEqual(qs.parse('foo=', { comma: true }), { foo: '' });
+        st.deepEqual(qs.parse('foo', { comma: true }), { foo: '' });
+        st.deepEqual(qs.parse('foo', { comma: true, strictNullHandling: true }), { foo: null });
+        st.end();
+    });
+
+    t.test('parses values with comma as array divider', function (st) {
+        st.deepEqual(qs.parse({ foo: 'bar,tee' }, { comma: false }), { foo: 'bar,tee' });
+        st.deepEqual(qs.parse({ foo: 'bar,tee' }, { comma: true }), { foo: ['bar', 'tee'] });
+        st.end();
+    });
+
+    t.test('use number decoder, parses string that has one number with comma option enabled', function (st) {
+        var decoder = function (str, defaultDecoder, charset, type) {
+            if (!isNaN(Number(str))) {
+                return parseFloat(str);
+            }
+            return defaultDecoder(str, defaultDecoder, charset, type);
+        };
+
+        st.deepEqual(qs.parse('foo=1', { comma: true, decoder: decoder }), { foo: 1 });
+        st.deepEqual(qs.parse('foo=0', { comma: true, decoder: decoder }), { foo: 0 });
+
+        st.end();
+    });
+
+    t.test('parses brackets holds array of arrays when having two parts of strings with comma as array divider', function (st) {
+        st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=4,5,6', { comma: true }), { foo: [['1', '2', '3'], ['4', '5', '6']] });
+        st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=', { comma: true }), { foo: [['1', '2', '3'], ''] });
+        st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=,', { comma: true }), { foo: [['1', '2', '3'], ['', '']] });
+        st.deepEqual(qs.parse('foo[]=1,2,3&foo[]=a', { comma: true }), { foo: [['1', '2', '3'], 'a'] });
+
+        st.end();
+    });
+
+    t.test('parses comma delimited array while having percent-encoded comma treated as normal text', function (st) {
+        st.deepEqual(qs.parse('foo=a%2Cb', { comma: true }), { foo: 'a,b' });
+        st.deepEqual(qs.parse('foo=a%2C%20b,d', { comma: true }), { foo: ['a, b', 'd'] });
+        st.deepEqual(qs.parse('foo=a%2C%20b,c%2C%20d', { comma: true }), { foo: ['a, b', 'c, d'] });
+
+        st.end();
+    });
+
+    t.test('parses an object in dot notation', function (st) {
+        var input = {
+            'user.name': { 'pop[bob]': 3 },
+            'user.email.': null
+        };
+
+        var expected = {
+            user: {
+                name: { 'pop[bob]': 3 },
+                email: null
+            }
+        };
+
+        var result = qs.parse(input, { allowDots: true });
+
+        st.deepEqual(result, expected);
+        st.end();
+    });
+
+    t.test('parses an object and not child values', function (st) {
+        var input = {
+            'user[name]': { 'pop[bob]': { test: 3 } },
+            'user[email]': null
+        };
+
+        var expected = {
+            user: {
+                name: { 'pop[bob]': { test: 3 } },
+                email: null
+            }
+        };
+
+        var result = qs.parse(input);
+
+        st.deepEqual(result, expected);
+        st.end();
+    });
+
+    t.test('does not blow up when Buffer global is missing', function (st) {
+        var tempBuffer = global.Buffer;
+        delete global.Buffer;
+        var result = qs.parse('a=b&c=d');
+        global.Buffer = tempBuffer;
+        st.deepEqual(result, { a: 'b', c: 'd' });
+        st.end();
+    });
+
+    t.test('does not crash when parsing circular references', function (st) {
+        var a = {};
+        a.b = a;
+
+        var parsed;
+
+        st.doesNotThrow(function () {
+            parsed = qs.parse({ 'foo[bar]': 'baz', 'foo[baz]': a });
+        });
+
+        st.equal('foo' in parsed, true, 'parsed has "foo" property');
+        st.equal('bar' in parsed.foo, true);
+        st.equal('baz' in parsed.foo, true);
+        st.equal(parsed.foo.bar, 'baz');
+        st.deepEqual(parsed.foo.baz, a);
+        st.end();
+    });
+
+    t.test('does not crash when parsing deep objects', function (st) {
+        var parsed;
+        var str = 'foo';
+
+        for (var i = 0; i < 5000; i++) {
+            str += '[p]';
+        }
+
+        str += '=bar';
+
+        st.doesNotThrow(function () {
+            parsed = qs.parse(str, { depth: 5000 });
+        });
+
+        st.equal('foo' in parsed, true, 'parsed has "foo" property');
+
+        var depth = 0;
+        var ref = parsed.foo;
+        while ((ref = ref.p)) {
+            depth += 1;
+        }
+
+        st.equal(depth, 5000, 'parsed is 5000 properties deep');
+
+        st.end();
+    });
+
+    t.test('parses null objects correctly', { skip: !Object.create }, function (st) {
+        var a = Object.create(null);
+        a.b = 'c';
+
+        st.deepEqual(qs.parse(a), { b: 'c' });
+        var result = qs.parse({ a: a });
+        st.equal('a' in result, true, 'result has "a" property');
+        st.deepEqual(result.a, a);
+        st.end();
+    });
+
+    t.test('parses dates correctly', function (st) {
+        var now = new Date();
+        st.deepEqual(qs.parse({ a: now }), { a: now });
+        st.end();
+    });
+
+    t.test('parses regular expressions correctly', function (st) {
+        var re = /^test$/;
+        st.deepEqual(qs.parse({ a: re }), { a: re });
+        st.end();
+    });
+
+    t.test('does not allow overwriting prototype properties', function (st) {
+        st.deepEqual(qs.parse('a[hasOwnProperty]=b', { allowPrototypes: false }), {});
+        st.deepEqual(qs.parse('hasOwnProperty=b', { allowPrototypes: false }), {});
+
+        st.deepEqual(
+            qs.parse('toString', { allowPrototypes: false }),
+            {},
+            'bare "toString" results in {}'
+        );
+
+        st.end();
+    });
+
+    t.test('can allow overwriting prototype properties', function (st) {
+        st.deepEqual(qs.parse('a[hasOwnProperty]=b', { allowPrototypes: true }), { a: { hasOwnProperty: 'b' } });
+        st.deepEqual(qs.parse('hasOwnProperty=b', { allowPrototypes: true }), { hasOwnProperty: 'b' });
+
+        st.deepEqual(
+            qs.parse('toString', { allowPrototypes: true }),
+            { toString: '' },
+            'bare "toString" results in { toString: "" }'
+        );
+
+        st.end();
+    });
+
+    t.test('params starting with a closing bracket', function (st) {
+        st.deepEqual(qs.parse(']=toString'), { ']': 'toString' });
+        st.deepEqual(qs.parse(']]=toString'), { ']]': 'toString' });
+        st.deepEqual(qs.parse(']hello]=toString'), { ']hello]': 'toString' });
+        st.end();
+    });
+
+    t.test('params starting with a starting bracket', function (st) {
+        st.deepEqual(qs.parse('[=toString'), { '[': 'toString' });
+        st.deepEqual(qs.parse('[[=toString'), { '[[': 'toString' });
+        st.deepEqual(qs.parse('[hello[=toString'), { '[hello[': 'toString' });
+        st.end();
+    });
+
+    t.test('add keys to objects', function (st) {
+        st.deepEqual(
+            qs.parse('a[b]=c&a=d'),
+            { a: { b: 'c', d: true } },
+            'can add keys to objects'
+        );
+
+        st.deepEqual(
+            qs.parse('a[b]=c&a=toString'),
+            { a: { b: 'c' } },
+            'can not overwrite prototype'
+        );
+
+        st.deepEqual(
+            qs.parse('a[b]=c&a=toString', { allowPrototypes: true }),
+            { a: { b: 'c', toString: true } },
+            'can overwrite prototype with allowPrototypes true'
+        );
+
+        st.deepEqual(
+            qs.parse('a[b]=c&a=toString', { plainObjects: true }),
+            { __proto__: null, a: { __proto__: null, b: 'c', toString: true } },
+            'can overwrite prototype with plainObjects true'
+        );
+
+        st.end();
+    });
+
+    t.test('dunder proto is ignored', function (st) {
+        var payload = 'categories[__proto__]=login&categories[__proto__]&categories[length]=42';
+        var result = qs.parse(payload, { allowPrototypes: true });
+
+        st.deepEqual(
+            result,
+            {
+                categories: {
+                    length: '42'
+                }
+            },
+            'silent [[Prototype]] payload'
+        );
+
+        var plainResult = qs.parse(payload, { allowPrototypes: true, plainObjects: true });
+
+        st.deepEqual(
+            plainResult,
+            {
+                __proto__: null,
+                categories: {
+                    __proto__: null,
+                    length: '42'
+                }
+            },
+            'silent [[Prototype]] payload: plain objects'
+        );
+
+        var query = qs.parse('categories[__proto__]=cats&categories[__proto__]=dogs&categories[some][json]=toInject', { allowPrototypes: true });
+
+        st.notOk(Array.isArray(query.categories), 'is not an array');
+        st.notOk(query.categories instanceof Array, 'is not instanceof an array');
+        st.deepEqual(query.categories, { some: { json: 'toInject' } });
+        st.equal(JSON.stringify(query.categories), '{"some":{"json":"toInject"}}', 'stringifies as a non-array');
+
+        st.deepEqual(
+            qs.parse('foo[__proto__][hidden]=value&foo[bar]=stuffs', { allowPrototypes: true }),
+            {
+                foo: {
+                    bar: 'stuffs'
+                }
+            },
+            'hidden values'
+        );
+
+        st.deepEqual(
+            qs.parse('foo[__proto__][hidden]=value&foo[bar]=stuffs', { allowPrototypes: true, plainObjects: true }),
+            {
+                __proto__: null,
+                foo: {
+                    __proto__: null,
+                    bar: 'stuffs'
+                }
+            },
+            'hidden values: plain objects'
+        );
+
+        st.end();
+    });
+
+    t.test('can return null objects', { skip: !Object.create }, function (st) {
+        var expected = Object.create(null);
+        expected.a = Object.create(null);
+        expected.a.b = 'c';
+        expected.a.hasOwnProperty = 'd';
+        st.deepEqual(qs.parse('a[b]=c&a[hasOwnProperty]=d', { plainObjects: true }), expected);
+        st.deepEqual(qs.parse(null, { plainObjects: true }), Object.create(null));
+        var expectedArray = Object.create(null);
+        expectedArray.a = Object.create(null);
+        expectedArray.a[0] = 'b';
+        expectedArray.a.c = 'd';
+        st.deepEqual(qs.parse('a[]=b&a[c]=d', { plainObjects: true }), expectedArray);
+        st.end();
+    });
+
+    t.test('can parse with custom encoding', function (st) {
+        st.deepEqual(qs.parse('%8c%a7=%91%e5%8d%e3%95%7b', {
+            decoder: function (str) {
+                var reg = /%([0-9A-F]{2})/ig;
+                var result = [];
+                var parts = reg.exec(str);
+                while (parts) {
+                    result.push(parseInt(parts[1], 16));
+                    parts = reg.exec(str);
+                }
+                return String(iconv.decode(SaferBuffer.from(result), 'shift_jis'));
+            }
+        }), { 県: '大阪府' });
+        st.end();
+    });
+
+    t.test('receives the default decoder as a second argument', function (st) {
+        st.plan(1);
+        qs.parse('a', {
+            decoder: function (str, defaultDecoder) {
+                st.equal(defaultDecoder, utils.decode);
+            }
+        });
+        st.end();
+    });
+
+    t.test('throws error with wrong decoder', function (st) {
+        st['throws'](function () {
+            qs.parse({}, { decoder: 'string' });
+        }, new TypeError('Decoder has to be a function.'));
+        st.end();
+    });
+
+    t.test('does not mutate the options argument', function (st) {
+        var options = {};
+        qs.parse('a[b]=true', options);
+        st.deepEqual(options, {});
+        st.end();
+    });
+
+    t.test('throws if an invalid charset is specified', function (st) {
+        st['throws'](function () {
+            qs.parse('a=b', { charset: 'foobar' });
+        }, new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'));
+        st.end();
+    });
+
+    t.test('parses an iso-8859-1 string if asked to', function (st) {
+        st.deepEqual(qs.parse('%A2=%BD', { charset: 'iso-8859-1' }), { '¢': '½' });
+        st.end();
+    });
+
+    var urlEncodedCheckmarkInUtf8 = '%E2%9C%93';
+    var urlEncodedOSlashInUtf8 = '%C3%B8';
+    var urlEncodedNumCheckmark = '%26%2310003%3B';
+    var urlEncodedNumSmiley = '%26%239786%3B';
+
+    t.test('prefers an utf-8 charset specified by the utf8 sentinel to a default charset of iso-8859-1', function (st) {
+        st.deepEqual(qs.parse('utf8=' + urlEncodedCheckmarkInUtf8 + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true, charset: 'iso-8859-1' }), { ø: 'ø' });
+        st.end();
+    });
+
+    t.test('prefers an iso-8859-1 charset specified by the utf8 sentinel to a default charset of utf-8', function (st) {
+        st.deepEqual(qs.parse('utf8=' + urlEncodedNumCheckmark + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true, charset: 'utf-8' }), { 'ø': 'ø' });
+        st.end();
+    });
+
+    t.test('does not require the utf8 sentinel to be defined before the parameters whose decoding it affects', function (st) {
+        st.deepEqual(qs.parse('a=' + urlEncodedOSlashInUtf8 + '&utf8=' + urlEncodedNumCheckmark, { charsetSentinel: true, charset: 'utf-8' }), { a: 'ø' });
+        st.end();
+    });
+
+    t.test('should ignore an utf8 sentinel with an unknown value', function (st) {
+        st.deepEqual(qs.parse('utf8=foo&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true, charset: 'utf-8' }), { ø: 'ø' });
+        st.end();
+    });
+
+    t.test('uses the utf8 sentinel to switch to utf-8 when no default charset is given', function (st) {
+        st.deepEqual(qs.parse('utf8=' + urlEncodedCheckmarkInUtf8 + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true }), { ø: 'ø' });
+        st.end();
+    });
+
+    t.test('uses the utf8 sentinel to switch to iso-8859-1 when no default charset is given', function (st) {
+        st.deepEqual(qs.parse('utf8=' + urlEncodedNumCheckmark + '&' + urlEncodedOSlashInUtf8 + '=' + urlEncodedOSlashInUtf8, { charsetSentinel: true }), { 'ø': 'ø' });
+        st.end();
+    });
+
+    t.test('interprets numeric entities in iso-8859-1 when `interpretNumericEntities`', function (st) {
+        st.deepEqual(qs.parse('foo=' + urlEncodedNumSmiley, { charset: 'iso-8859-1', interpretNumericEntities: true }), { foo: '☺' });
+        st.end();
+    });
+
+    t.test('handles a custom decoder returning `null`, in the `iso-8859-1` charset, when `interpretNumericEntities`', function (st) {
+        st.deepEqual(qs.parse('foo=&bar=' + urlEncodedNumSmiley, {
+            charset: 'iso-8859-1',
+            decoder: function (str, defaultDecoder, charset) {
+                return str ? defaultDecoder(str, defaultDecoder, charset) : null;
+            },
+            interpretNumericEntities: true
+        }), { foo: null, bar: '☺' });
+        st.end();
+    });
+
+    t.test('does not interpret numeric entities in iso-8859-1 when `interpretNumericEntities` is absent', function (st) {
+        st.deepEqual(qs.parse('foo=' + urlEncodedNumSmiley, { charset: 'iso-8859-1' }), { foo: '&#9786;' });
+        st.end();
+    });
+
+    t.test('does not interpret numeric entities when the charset is utf-8, even when `interpretNumericEntities`', function (st) {
+        st.deepEqual(qs.parse('foo=' + urlEncodedNumSmiley, { charset: 'utf-8', interpretNumericEntities: true }), { foo: '&#9786;' });
+        st.end();
+    });
+
+    t.test('does not interpret %uXXXX syntax in iso-8859-1 mode', function (st) {
+        st.deepEqual(qs.parse('%u263A=%u263A', { charset: 'iso-8859-1' }), { '%u263A': '%u263A' });
+        st.end();
+    });
+
+    t.test('allows for decoding keys and values differently', function (st) {
+        var decoder = function (str, defaultDecoder, charset, type) {
+            if (type === 'key') {
+                return defaultDecoder(str, defaultDecoder, charset, type).toLowerCase();
+            }
+            if (type === 'value') {
+                return defaultDecoder(str, defaultDecoder, charset, type).toUpperCase();
+            }
+            throw 'this should never happen! type: ' + type;
+        };
+
+        st.deepEqual(qs.parse('KeY=vAlUe', { decoder: decoder }), { key: 'VALUE' });
+        st.end();
+    });
+
+    t.end();
+});

+ 865 - 0
backend/node_modules/body-parser/node_modules/qs/test/stringify.js

@@ -0,0 +1,865 @@
+'use strict';
+
+var test = require('tape');
+var qs = require('../');
+var utils = require('../lib/utils');
+var iconv = require('iconv-lite');
+var SaferBuffer = require('safer-buffer').Buffer;
+var hasSymbols = require('has-symbols');
+var hasBigInt = typeof BigInt === 'function';
+
+test('stringify()', function (t) {
+    t.test('stringifies a querystring object', function (st) {
+        st.equal(qs.stringify({ a: 'b' }), 'a=b');
+        st.equal(qs.stringify({ a: 1 }), 'a=1');
+        st.equal(qs.stringify({ a: 1, b: 2 }), 'a=1&b=2');
+        st.equal(qs.stringify({ a: 'A_Z' }), 'a=A_Z');
+        st.equal(qs.stringify({ a: '€' }), 'a=%E2%82%AC');
+        st.equal(qs.stringify({ a: '' }), 'a=%EE%80%80');
+        st.equal(qs.stringify({ a: 'א' }), 'a=%D7%90');
+        st.equal(qs.stringify({ a: '𐐷' }), 'a=%F0%90%90%B7');
+        st.end();
+    });
+
+    t.test('stringifies falsy values', function (st) {
+        st.equal(qs.stringify(undefined), '');
+        st.equal(qs.stringify(null), '');
+        st.equal(qs.stringify(null, { strictNullHandling: true }), '');
+        st.equal(qs.stringify(false), '');
+        st.equal(qs.stringify(0), '');
+        st.end();
+    });
+
+    t.test('stringifies symbols', { skip: !hasSymbols() }, function (st) {
+        st.equal(qs.stringify(Symbol.iterator), '');
+        st.equal(qs.stringify([Symbol.iterator]), '0=Symbol%28Symbol.iterator%29');
+        st.equal(qs.stringify({ a: Symbol.iterator }), 'a=Symbol%28Symbol.iterator%29');
+        st.equal(
+            qs.stringify({ a: [Symbol.iterator] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
+            'a[]=Symbol%28Symbol.iterator%29'
+        );
+        st.end();
+    });
+
+    t.test('stringifies bigints', { skip: !hasBigInt }, function (st) {
+        var three = BigInt(3);
+        var encodeWithN = function (value, defaultEncoder, charset) {
+            var result = defaultEncoder(value, defaultEncoder, charset);
+            return typeof value === 'bigint' ? result + 'n' : result;
+        };
+        st.equal(qs.stringify(three), '');
+        st.equal(qs.stringify([three]), '0=3');
+        st.equal(qs.stringify([three], { encoder: encodeWithN }), '0=3n');
+        st.equal(qs.stringify({ a: three }), 'a=3');
+        st.equal(qs.stringify({ a: three }, { encoder: encodeWithN }), 'a=3n');
+        st.equal(
+            qs.stringify({ a: [three] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
+            'a[]=3'
+        );
+        st.equal(
+            qs.stringify({ a: [three] }, { encodeValuesOnly: true, encoder: encodeWithN, arrayFormat: 'brackets' }),
+            'a[]=3n'
+        );
+        st.end();
+    });
+
+    t.test('adds query prefix', function (st) {
+        st.equal(qs.stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b');
+        st.end();
+    });
+
+    t.test('with query prefix, outputs blank string given an empty object', function (st) {
+        st.equal(qs.stringify({}, { addQueryPrefix: true }), '');
+        st.end();
+    });
+
+    t.test('stringifies nested falsy values', function (st) {
+        st.equal(qs.stringify({ a: { b: { c: null } } }), 'a%5Bb%5D%5Bc%5D=');
+        st.equal(qs.stringify({ a: { b: { c: null } } }, { strictNullHandling: true }), 'a%5Bb%5D%5Bc%5D');
+        st.equal(qs.stringify({ a: { b: { c: false } } }), 'a%5Bb%5D%5Bc%5D=false');
+        st.end();
+    });
+
+    t.test('stringifies a nested object', function (st) {
+        st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
+        st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }), 'a%5Bb%5D%5Bc%5D%5Bd%5D=e');
+        st.end();
+    });
+
+    t.test('stringifies a nested object with dots notation', function (st) {
+        st.equal(qs.stringify({ a: { b: 'c' } }, { allowDots: true }), 'a.b=c');
+        st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true }), 'a.b.c.d=e');
+        st.end();
+    });
+
+    t.test('stringifies an array value', function (st) {
+        st.equal(
+            qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' }),
+            'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' }),
+            'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' }),
+            'a=b%2Cc%2Cd',
+            'comma => comma'
+        );
+        st.equal(
+            qs.stringify({ a: ['b', 'c', 'd'] }),
+            'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d',
+            'default => indices'
+        );
+        st.end();
+    });
+
+    t.test('omits nulls when asked', function (st) {
+        st.equal(qs.stringify({ a: 'b', c: null }, { skipNulls: true }), 'a=b');
+        st.end();
+    });
+
+    t.test('omits nested nulls when asked', function (st) {
+        st.equal(qs.stringify({ a: { b: 'c', d: null } }, { skipNulls: true }), 'a%5Bb%5D=c');
+        st.end();
+    });
+
+    t.test('omits array indices when asked', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d');
+        st.end();
+    });
+
+    t.test('stringifies a nested array value', function (st) {
+        st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[b][0]=c&a[b][1]=d');
+        st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[b][]=c&a[b][]=d');
+        st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a[b]=c,d');
+        st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true }), 'a[b][0]=c&a[b][1]=d');
+        st.end();
+    });
+
+    t.test('stringifies a nested array value with dots notation', function (st) {
+        st.equal(
+            qs.stringify(
+                { a: { b: ['c', 'd'] } },
+                { allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }
+            ),
+            'a.b[0]=c&a.b[1]=d',
+            'indices: stringifies with dots + indices'
+        );
+        st.equal(
+            qs.stringify(
+                { a: { b: ['c', 'd'] } },
+                { allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }
+            ),
+            'a.b[]=c&a.b[]=d',
+            'brackets: stringifies with dots + brackets'
+        );
+        st.equal(
+            qs.stringify(
+                { a: { b: ['c', 'd'] } },
+                { allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }
+            ),
+            'a.b=c,d',
+            'comma: stringifies with dots + comma'
+        );
+        st.equal(
+            qs.stringify(
+                { a: { b: ['c', 'd'] } },
+                { allowDots: true, encodeValuesOnly: true }
+            ),
+            'a.b[0]=c&a.b[1]=d',
+            'default: stringifies with dots + indices'
+        );
+        st.end();
+    });
+
+    t.test('stringifies an object inside an array', function (st) {
+        st.equal(
+            qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices' }),
+            'a%5B0%5D%5Bb%5D=c', // a[0][b]=c
+            'indices => brackets'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets' }),
+            'a%5B%5D%5Bb%5D=c', // a[][b]=c
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: 'c' }] }),
+            'a%5B0%5D%5Bb%5D=c',
+            'default => indices'
+        );
+
+        st.equal(
+            qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices' }),
+            'a%5B0%5D%5Bb%5D%5Bc%5D%5B0%5D=1',
+            'indices => indices'
+        );
+
+        st.equal(
+            qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets' }),
+            'a%5B%5D%5Bb%5D%5Bc%5D%5B%5D=1',
+            'brackets => brackets'
+        );
+
+        st.equal(
+            qs.stringify({ a: [{ b: { c: [1] } }] }),
+            'a%5B0%5D%5Bb%5D%5Bc%5D%5B0%5D=1',
+            'default => indices'
+        );
+
+        st.end();
+    });
+
+    t.test('stringifies an array with mixed objects and primitives', function (st) {
+        st.equal(
+            qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' }),
+            'a[0][b]=1&a[1]=2&a[2]=3',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
+            'a[][b]=1&a[]=2&a[]=3',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }),
+            '???',
+            'brackets => brackets',
+            { skip: 'TODO: figure out what this should do' }
+        );
+        st.equal(
+            qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true }),
+            'a[0][b]=1&a[1]=2&a[2]=3',
+            'default => indices'
+        );
+
+        st.end();
+    });
+
+    t.test('stringifies an object inside an array with dots notation', function (st) {
+        st.equal(
+            qs.stringify(
+                { a: [{ b: 'c' }] },
+                { allowDots: true, encode: false, arrayFormat: 'indices' }
+            ),
+            'a[0].b=c',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify(
+                { a: [{ b: 'c' }] },
+                { allowDots: true, encode: false, arrayFormat: 'brackets' }
+            ),
+            'a[].b=c',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify(
+                { a: [{ b: 'c' }] },
+                { allowDots: true, encode: false }
+            ),
+            'a[0].b=c',
+            'default => indices'
+        );
+
+        st.equal(
+            qs.stringify(
+                { a: [{ b: { c: [1] } }] },
+                { allowDots: true, encode: false, arrayFormat: 'indices' }
+            ),
+            'a[0].b.c[0]=1',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify(
+                { a: [{ b: { c: [1] } }] },
+                { allowDots: true, encode: false, arrayFormat: 'brackets' }
+            ),
+            'a[].b.c[]=1',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify(
+                { a: [{ b: { c: [1] } }] },
+                { allowDots: true, encode: false }
+            ),
+            'a[0].b.c[0]=1',
+            'default => indices'
+        );
+
+        st.end();
+    });
+
+    t.test('does not omit object keys when indices = false', function (st) {
+        st.equal(qs.stringify({ a: [{ b: 'c' }] }, { indices: false }), 'a%5Bb%5D=c');
+        st.end();
+    });
+
+    t.test('uses indices notation for arrays when indices=true', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c'] }, { indices: true }), 'a%5B0%5D=b&a%5B1%5D=c');
+        st.end();
+    });
+
+    t.test('uses indices notation for arrays when no arrayFormat is specified', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c'] }), 'a%5B0%5D=b&a%5B1%5D=c');
+        st.end();
+    });
+
+    t.test('uses indices notation for arrays when no arrayFormat=indices', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }), 'a%5B0%5D=b&a%5B1%5D=c');
+        st.end();
+    });
+
+    t.test('uses repeat notation for arrays when no arrayFormat=repeat', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }), 'a=b&a=c');
+        st.end();
+    });
+
+    t.test('uses brackets notation for arrays when no arrayFormat=brackets', function (st) {
+        st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }), 'a%5B%5D=b&a%5B%5D=c');
+        st.end();
+    });
+
+    t.test('stringifies a complicated object', function (st) {
+        st.equal(qs.stringify({ a: { b: 'c', d: 'e' } }), 'a%5Bb%5D=c&a%5Bd%5D=e');
+        st.end();
+    });
+
+    t.test('stringifies an empty value', function (st) {
+        st.equal(qs.stringify({ a: '' }), 'a=');
+        st.equal(qs.stringify({ a: null }, { strictNullHandling: true }), 'a');
+
+        st.equal(qs.stringify({ a: '', b: '' }), 'a=&b=');
+        st.equal(qs.stringify({ a: null, b: '' }, { strictNullHandling: true }), 'a&b=');
+
+        st.equal(qs.stringify({ a: { b: '' } }), 'a%5Bb%5D=');
+        st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: true }), 'a%5Bb%5D');
+        st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: false }), 'a%5Bb%5D=');
+
+        st.end();
+    });
+
+    t.test('stringifies an empty array in different arrayFormat', function (st) {
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false }), 'b[0]=&c=c');
+        // arrayFormat default
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' }), 'b[0]=&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' }), 'b[]=&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' }), 'b=&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' }), 'b=&c=c');
+        // with strictNullHandling
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', strictNullHandling: true }), 'b[0]&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', strictNullHandling: true }), 'b[]&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', strictNullHandling: true }), 'b&c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', strictNullHandling: true }), 'b&c=c');
+        // with skipNulls
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', skipNulls: true }), 'c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', skipNulls: true }), 'c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', skipNulls: true }), 'c=c');
+        st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', skipNulls: true }), 'c=c');
+
+        st.end();
+    });
+
+    t.test('stringifies a null object', { skip: !Object.create }, function (st) {
+        var obj = Object.create(null);
+        obj.a = 'b';
+        st.equal(qs.stringify(obj), 'a=b');
+        st.end();
+    });
+
+    t.test('returns an empty string for invalid input', function (st) {
+        st.equal(qs.stringify(undefined), '');
+        st.equal(qs.stringify(false), '');
+        st.equal(qs.stringify(null), '');
+        st.equal(qs.stringify(''), '');
+        st.end();
+    });
+
+    t.test('stringifies an object with a null object as a child', { skip: !Object.create }, function (st) {
+        var obj = { a: Object.create(null) };
+
+        obj.a.b = 'c';
+        st.equal(qs.stringify(obj), 'a%5Bb%5D=c');
+        st.end();
+    });
+
+    t.test('drops keys with a value of undefined', function (st) {
+        st.equal(qs.stringify({ a: undefined }), '');
+
+        st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true }), 'a%5Bc%5D');
+        st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false }), 'a%5Bc%5D=');
+        st.equal(qs.stringify({ a: { b: undefined, c: '' } }), 'a%5Bc%5D=');
+        st.end();
+    });
+
+    t.test('url encodes values', function (st) {
+        st.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
+        st.end();
+    });
+
+    t.test('stringifies a date', function (st) {
+        var now = new Date();
+        var str = 'a=' + encodeURIComponent(now.toISOString());
+        st.equal(qs.stringify({ a: now }), str);
+        st.end();
+    });
+
+    t.test('stringifies the weird object from qs', function (st) {
+        st.equal(qs.stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' }), 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F');
+        st.end();
+    });
+
+    t.test('skips properties that are part of the object prototype', function (st) {
+        Object.prototype.crash = 'test';
+        st.equal(qs.stringify({ a: 'b' }), 'a=b');
+        st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
+        delete Object.prototype.crash;
+        st.end();
+    });
+
+    t.test('stringifies boolean values', function (st) {
+        st.equal(qs.stringify({ a: true }), 'a=true');
+        st.equal(qs.stringify({ a: { b: true } }), 'a%5Bb%5D=true');
+        st.equal(qs.stringify({ b: false }), 'b=false');
+        st.equal(qs.stringify({ b: { c: false } }), 'b%5Bc%5D=false');
+        st.end();
+    });
+
+    t.test('stringifies buffer values', function (st) {
+        st.equal(qs.stringify({ a: SaferBuffer.from('test') }), 'a=test');
+        st.equal(qs.stringify({ a: { b: SaferBuffer.from('test') } }), 'a%5Bb%5D=test');
+        st.end();
+    });
+
+    t.test('stringifies an object using an alternative delimiter', function (st) {
+        st.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
+        st.end();
+    });
+
+    t.test('does not blow up when Buffer global is missing', function (st) {
+        var tempBuffer = global.Buffer;
+        delete global.Buffer;
+        var result = qs.stringify({ a: 'b', c: 'd' });
+        global.Buffer = tempBuffer;
+        st.equal(result, 'a=b&c=d');
+        st.end();
+    });
+
+    t.test('does not crash when parsing circular references', function (st) {
+        var a = {};
+        a.b = a;
+
+        st['throws'](
+            function () { qs.stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); },
+            /RangeError: Cyclic object value/,
+            'cyclic values throw'
+        );
+
+        var circular = {
+            a: 'value'
+        };
+        circular.a = circular;
+        st['throws'](
+            function () { qs.stringify(circular); },
+            /RangeError: Cyclic object value/,
+            'cyclic values throw'
+        );
+
+        var arr = ['a'];
+        st.doesNotThrow(
+            function () { qs.stringify({ x: arr, y: arr }); },
+            'non-cyclic values do not throw'
+        );
+
+        st.end();
+    });
+
+    t.test('non-circular duplicated references can still work', function (st) {
+        var hourOfDay = {
+            'function': 'hour_of_day'
+        };
+
+        var p1 = {
+            'function': 'gte',
+            arguments: [hourOfDay, 0]
+        };
+        var p2 = {
+            'function': 'lte',
+            arguments: [hourOfDay, 23]
+        };
+
+        st.equal(
+            qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true }),
+            'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23'
+        );
+
+        st.end();
+    });
+
+    t.test('selects properties when filter=array', function (st) {
+        st.equal(qs.stringify({ a: 'b' }, { filter: ['a'] }), 'a=b');
+        st.equal(qs.stringify({ a: 1 }, { filter: [] }), '');
+
+        st.equal(
+            qs.stringify(
+                { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
+                { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }
+            ),
+            'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3',
+            'indices => indices'
+        );
+        st.equal(
+            qs.stringify(
+                { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
+                { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }
+            ),
+            'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3',
+            'brackets => brackets'
+        );
+        st.equal(
+            qs.stringify(
+                { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
+                { filter: ['a', 'b', 0, 2] }
+            ),
+            'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3',
+            'default => indices'
+        );
+
+        st.end();
+    });
+
+    t.test('supports custom representations when filter=function', function (st) {
+        var calls = 0;
+        var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } };
+        var filterFunc = function (prefix, value) {
+            calls += 1;
+            if (calls === 1) {
+                st.equal(prefix, '', 'prefix is empty');
+                st.equal(value, obj);
+            } else if (prefix === 'c') {
+                return void 0;
+            } else if (value instanceof Date) {
+                st.equal(prefix, 'e[f]');
+                return value.getTime();
+            }
+            return value;
+        };
+
+        st.equal(qs.stringify(obj, { filter: filterFunc }), 'a=b&e%5Bf%5D=1257894000000');
+        st.equal(calls, 5);
+        st.end();
+    });
+
+    t.test('can disable uri encoding', function (st) {
+        st.equal(qs.stringify({ a: 'b' }, { encode: false }), 'a=b');
+        st.equal(qs.stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c');
+        st.equal(qs.stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false }), 'a=b&c');
+        st.end();
+    });
+
+    t.test('can sort the keys', function (st) {
+        var sort = function (a, b) {
+            return a.localeCompare(b);
+        };
+        st.equal(qs.stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort }), 'a=c&b=f&z=y');
+        st.equal(qs.stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort }), 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a');
+        st.end();
+    });
+
+    t.test('can sort the keys at depth 3 or more too', function (st) {
+        var sort = function (a, b) {
+            return a.localeCompare(b);
+        };
+        st.equal(
+            qs.stringify(
+                { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' },
+                { sort: sort, encode: false }
+            ),
+            'a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb'
+        );
+        st.equal(
+            qs.stringify(
+                { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' },
+                { sort: null, encode: false }
+            ),
+            'a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b'
+        );
+        st.end();
+    });
+
+    t.test('can stringify with custom encoding', function (st) {
+        st.equal(qs.stringify({ 県: '大阪府', '': '' }, {
+            encoder: function (str) {
+                if (str.length === 0) {
+                    return '';
+                }
+                var buf = iconv.encode(str, 'shiftjis');
+                var result = [];
+                for (var i = 0; i < buf.length; ++i) {
+                    result.push(buf.readUInt8(i).toString(16));
+                }
+                return '%' + result.join('%');
+            }
+        }), '%8c%a7=%91%e5%8d%e3%95%7b&=');
+        st.end();
+    });
+
+    t.test('receives the default encoder as a second argument', function (st) {
+        st.plan(2);
+        qs.stringify({ a: 1 }, {
+            encoder: function (str, defaultEncoder) {
+                st.equal(defaultEncoder, utils.encode);
+            }
+        });
+        st.end();
+    });
+
+    t.test('throws error with wrong encoder', function (st) {
+        st['throws'](function () {
+            qs.stringify({}, { encoder: 'string' });
+        }, new TypeError('Encoder has to be a function.'));
+        st.end();
+    });
+
+    t.test('can use custom encoder for a buffer object', { skip: typeof Buffer === 'undefined' }, function (st) {
+        st.equal(qs.stringify({ a: SaferBuffer.from([1]) }, {
+            encoder: function (buffer) {
+                if (typeof buffer === 'string') {
+                    return buffer;
+                }
+                return String.fromCharCode(buffer.readUInt8(0) + 97);
+            }
+        }), 'a=b');
+
+        st.equal(qs.stringify({ a: SaferBuffer.from('a b') }, {
+            encoder: function (buffer) {
+                return buffer;
+            }
+        }), 'a=a b');
+        st.end();
+    });
+
+    t.test('serializeDate option', function (st) {
+        var date = new Date();
+        st.equal(
+            qs.stringify({ a: date }),
+            'a=' + date.toISOString().replace(/:/g, '%3A'),
+            'default is toISOString'
+        );
+
+        var mutatedDate = new Date();
+        mutatedDate.toISOString = function () {
+            throw new SyntaxError();
+        };
+        st['throws'](function () {
+            mutatedDate.toISOString();
+        }, SyntaxError);
+        st.equal(
+            qs.stringify({ a: mutatedDate }),
+            'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'),
+            'toISOString works even when method is not locally present'
+        );
+
+        var specificDate = new Date(6);
+        st.equal(
+            qs.stringify(
+                { a: specificDate },
+                { serializeDate: function (d) { return d.getTime() * 7; } }
+            ),
+            'a=42',
+            'custom serializeDate function called'
+        );
+
+        st.equal(
+            qs.stringify(
+                { a: [date] },
+                {
+                    serializeDate: function (d) { return d.getTime(); },
+                    arrayFormat: 'comma'
+                }
+            ),
+            'a=' + date.getTime(),
+            'works with arrayFormat comma'
+        );
+
+        st.end();
+    });
+
+    t.test('RFC 1738 serialization', function (st) {
+        st.equal(qs.stringify({ a: 'b c' }, { format: qs.formats.RFC1738 }), 'a=b+c');
+        st.equal(qs.stringify({ 'a b': 'c d' }, { format: qs.formats.RFC1738 }), 'a+b=c+d');
+        st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }, { format: qs.formats.RFC1738 }), 'a+b=a+b');
+
+        st.equal(qs.stringify({ 'foo(ref)': 'bar' }, { format: qs.formats.RFC1738 }), 'foo(ref)=bar');
+
+        st.end();
+    });
+
+    t.test('RFC 3986 spaces serialization', function (st) {
+        st.equal(qs.stringify({ a: 'b c' }, { format: qs.formats.RFC3986 }), 'a=b%20c');
+        st.equal(qs.stringify({ 'a b': 'c d' }, { format: qs.formats.RFC3986 }), 'a%20b=c%20d');
+        st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }, { format: qs.formats.RFC3986 }), 'a%20b=a%20b');
+
+        st.end();
+    });
+
+    t.test('Backward compatibility to RFC 3986', function (st) {
+        st.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
+        st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }), 'a%20b=a%20b');
+
+        st.end();
+    });
+
+    t.test('Edge cases and unknown formats', function (st) {
+        ['UFO1234', false, 1234, null, {}, []].forEach(
+            function (format) {
+                st['throws'](
+                    function () {
+                        qs.stringify({ a: 'b c' }, { format: format });
+                    },
+                    new TypeError('Unknown format option provided.')
+                );
+            }
+        );
+        st.end();
+    });
+
+    t.test('encodeValuesOnly', function (st) {
+        st.equal(
+            qs.stringify(
+                { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
+                { encodeValuesOnly: true }
+            ),
+            'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'
+        );
+        st.equal(
+            qs.stringify(
+                { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }
+            ),
+            'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h'
+        );
+        st.end();
+    });
+
+    t.test('encodeValuesOnly - strictNullHandling', function (st) {
+        st.equal(
+            qs.stringify(
+                { a: { b: null } },
+                { encodeValuesOnly: true, strictNullHandling: true }
+            ),
+            'a[b]'
+        );
+        st.end();
+    });
+
+    t.test('throws if an invalid charset is specified', function (st) {
+        st['throws'](function () {
+            qs.stringify({ a: 'b' }, { charset: 'foobar' });
+        }, new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'));
+        st.end();
+    });
+
+    t.test('respects a charset of iso-8859-1', function (st) {
+        st.equal(qs.stringify({ æ: 'æ' }, { charset: 'iso-8859-1' }), '%E6=%E6');
+        st.end();
+    });
+
+    t.test('encodes unrepresentable chars as numeric entities in iso-8859-1 mode', function (st) {
+        st.equal(qs.stringify({ a: '☺' }, { charset: 'iso-8859-1' }), 'a=%26%239786%3B');
+        st.end();
+    });
+
+    t.test('respects an explicit charset of utf-8 (the default)', function (st) {
+        st.equal(qs.stringify({ a: 'æ' }, { charset: 'utf-8' }), 'a=%C3%A6');
+        st.end();
+    });
+
+    t.test('adds the right sentinel when instructed to and the charset is utf-8', function (st) {
+        st.equal(qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' }), 'utf8=%E2%9C%93&a=%C3%A6');
+        st.end();
+    });
+
+    t.test('adds the right sentinel when instructed to and the charset is iso-8859-1', function (st) {
+        st.equal(qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' }), 'utf8=%26%2310003%3B&a=%E6');
+        st.end();
+    });
+
+    t.test('does not mutate the options argument', function (st) {
+        var options = {};
+        qs.stringify({}, options);
+        st.deepEqual(options, {});
+        st.end();
+    });
+
+    t.test('strictNullHandling works with custom filter', function (st) {
+        var filter = function (prefix, value) {
+            return value;
+        };
+
+        var options = { strictNullHandling: true, filter: filter };
+        st.equal(qs.stringify({ key: null }, options), 'key');
+        st.end();
+    });
+
+    t.test('strictNullHandling works with null serializeDate', function (st) {
+        var serializeDate = function () {
+            return null;
+        };
+        var options = { strictNullHandling: true, serializeDate: serializeDate };
+        var date = new Date();
+        st.equal(qs.stringify({ key: date }, options), 'key');
+        st.end();
+    });
+
+    t.test('allows for encoding keys and values differently', function (st) {
+        var encoder = function (str, defaultEncoder, charset, type) {
+            if (type === 'key') {
+                return defaultEncoder(str, defaultEncoder, charset, type).toLowerCase();
+            }
+            if (type === 'value') {
+                return defaultEncoder(str, defaultEncoder, charset, type).toUpperCase();
+            }
+            throw 'this should never happen! type: ' + type;
+        };
+
+        st.deepEqual(qs.stringify({ KeY: 'vAlUe' }, { encoder: encoder }), 'key=VALUE');
+        st.end();
+    });
+
+    t.test('objects inside arrays', function (st) {
+        var obj = { a: { b: { c: 'd', e: 'f' } } };
+        var withArray = { a: { b: [{ c: 'd', e: 'f' }] } };
+
+        st.equal(qs.stringify(obj, { encode: false }), 'a[b][c]=d&a[b][e]=f', 'no array, no arrayFormat');
+        st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'bracket' }), 'a[b][c]=d&a[b][e]=f', 'no array, bracket');
+        st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'indices' }), 'a[b][c]=d&a[b][e]=f', 'no array, indices');
+        st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'comma' }), 'a[b][c]=d&a[b][e]=f', 'no array, comma');
+
+        st.equal(qs.stringify(withArray, { encode: false }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, no arrayFormat');
+        st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'bracket' }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, bracket');
+        st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'indices' }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, indices');
+        st.equal(
+            qs.stringify(withArray, { encode: false, arrayFormat: 'comma' }),
+            '???',
+            'array, comma',
+            { skip: 'TODO: figure out what this should do' }
+        );
+
+        st.end();
+    });
+
+    t.test('stringifies sparse arrays', function (st) {
+        /* eslint no-sparse-arrays: 0 */
+        st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true }), 'a[1]=2&a[4]=1');
+        st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true }), 'a[1][b][2][c]=1');
+        st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true }), 'a[1][2][3][c]=1');
+        st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true }), 'a[1][2][3][c][1]=1');
+
+        st.end();
+    });
+
+    t.end();
+});

+ 136 - 0
backend/node_modules/body-parser/node_modules/qs/test/utils.js

@@ -0,0 +1,136 @@
+'use strict';
+
+var test = require('tape');
+var inspect = require('object-inspect');
+var SaferBuffer = require('safer-buffer').Buffer;
+var forEach = require('for-each');
+var utils = require('../lib/utils');
+
+test('merge()', function (t) {
+    t.deepEqual(utils.merge(null, true), [null, true], 'merges true into null');
+
+    t.deepEqual(utils.merge(null, [42]), [null, 42], 'merges null into an array');
+
+    t.deepEqual(utils.merge({ a: 'b' }, { a: 'c' }), { a: ['b', 'c'] }, 'merges two objects with the same key');
+
+    var oneMerged = utils.merge({ foo: 'bar' }, { foo: { first: '123' } });
+    t.deepEqual(oneMerged, { foo: ['bar', { first: '123' }] }, 'merges a standalone and an object into an array');
+
+    var twoMerged = utils.merge({ foo: ['bar', { first: '123' }] }, { foo: { second: '456' } });
+    t.deepEqual(twoMerged, { foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }, 'merges a standalone and two objects into an array');
+
+    var sandwiched = utils.merge({ foo: ['bar', { first: '123', second: '456' }] }, { foo: 'baz' });
+    t.deepEqual(sandwiched, { foo: ['bar', { first: '123', second: '456' }, 'baz'] }, 'merges an object sandwiched by two standalones into an array');
+
+    var nestedArrays = utils.merge({ foo: ['baz'] }, { foo: ['bar', 'xyzzy'] });
+    t.deepEqual(nestedArrays, { foo: ['baz', 'bar', 'xyzzy'] });
+
+    var noOptionsNonObjectSource = utils.merge({ foo: 'baz' }, 'bar');
+    t.deepEqual(noOptionsNonObjectSource, { foo: 'baz', bar: true });
+
+    t.test(
+        'avoids invoking array setters unnecessarily',
+        { skip: typeof Object.defineProperty !== 'function' },
+        function (st) {
+            var setCount = 0;
+            var getCount = 0;
+            var observed = [];
+            Object.defineProperty(observed, 0, {
+                get: function () {
+                    getCount += 1;
+                    return { bar: 'baz' };
+                },
+                set: function () { setCount += 1; }
+            });
+            utils.merge(observed, [null]);
+            st.equal(setCount, 0);
+            st.equal(getCount, 1);
+            observed[0] = observed[0]; // eslint-disable-line no-self-assign
+            st.equal(setCount, 1);
+            st.equal(getCount, 2);
+            st.end();
+        }
+    );
+
+    t.end();
+});
+
+test('assign()', function (t) {
+    var target = { a: 1, b: 2 };
+    var source = { b: 3, c: 4 };
+    var result = utils.assign(target, source);
+
+    t.equal(result, target, 'returns the target');
+    t.deepEqual(target, { a: 1, b: 3, c: 4 }, 'target and source are merged');
+    t.deepEqual(source, { b: 3, c: 4 }, 'source is untouched');
+
+    t.end();
+});
+
+test('combine()', function (t) {
+    t.test('both arrays', function (st) {
+        var a = [1];
+        var b = [2];
+        var combined = utils.combine(a, b);
+
+        st.deepEqual(a, [1], 'a is not mutated');
+        st.deepEqual(b, [2], 'b is not mutated');
+        st.notEqual(a, combined, 'a !== combined');
+        st.notEqual(b, combined, 'b !== combined');
+        st.deepEqual(combined, [1, 2], 'combined is a + b');
+
+        st.end();
+    });
+
+    t.test('one array, one non-array', function (st) {
+        var aN = 1;
+        var a = [aN];
+        var bN = 2;
+        var b = [bN];
+
+        var combinedAnB = utils.combine(aN, b);
+        st.deepEqual(b, [bN], 'b is not mutated');
+        st.notEqual(aN, combinedAnB, 'aN + b !== aN');
+        st.notEqual(a, combinedAnB, 'aN + b !== a');
+        st.notEqual(bN, combinedAnB, 'aN + b !== bN');
+        st.notEqual(b, combinedAnB, 'aN + b !== b');
+        st.deepEqual([1, 2], combinedAnB, 'first argument is array-wrapped when not an array');
+
+        var combinedABn = utils.combine(a, bN);
+        st.deepEqual(a, [aN], 'a is not mutated');
+        st.notEqual(aN, combinedABn, 'a + bN !== aN');
+        st.notEqual(a, combinedABn, 'a + bN !== a');
+        st.notEqual(bN, combinedABn, 'a + bN !== bN');
+        st.notEqual(b, combinedABn, 'a + bN !== b');
+        st.deepEqual([1, 2], combinedABn, 'second argument is array-wrapped when not an array');
+
+        st.end();
+    });
+
+    t.test('neither is an array', function (st) {
+        var combined = utils.combine(1, 2);
+        st.notEqual(1, combined, '1 + 2 !== 1');
+        st.notEqual(2, combined, '1 + 2 !== 2');
+        st.deepEqual([1, 2], combined, 'both arguments are array-wrapped when not an array');
+
+        st.end();
+    });
+
+    t.end();
+});
+
+test('isBuffer()', function (t) {
+    forEach([null, undefined, true, false, '', 'abc', 42, 0, NaN, {}, [], function () {}, /a/g], function (x) {
+        t.equal(utils.isBuffer(x), false, inspect(x) + ' is not a buffer');
+    });
+
+    var fakeBuffer = { constructor: Buffer };
+    t.equal(utils.isBuffer(fakeBuffer), false, 'fake buffer is not a buffer');
+
+    var saferBuffer = SaferBuffer.from('abc');
+    t.equal(utils.isBuffer(saferBuffer), true, 'SaferBuffer instance is a buffer');
+
+    var buffer = Buffer.from && Buffer.alloc ? Buffer.from('abc') : new Buffer('abc');
+    t.equal(utils.isBuffer(buffer), true, 'real Buffer instance is a buffer');
+    t.end();
+});

+ 146 - 0
backend/node_modules/body-parser/node_modules/statuses/index.js

@@ -0,0 +1,146 @@
+/*!
+ * statuses
+ * Copyright(c) 2014 Jonathan Ong
+ * Copyright(c) 2016 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var codes = require('./codes.json')
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = status
+
+// status code to message map
+status.message = codes
+
+// status message (lower-case) to code map
+status.code = createMessageToStatusCodeMap(codes)
+
+// array of status codes
+status.codes = createStatusCodeList(codes)
+
+// status codes for redirects
+status.redirect = {
+  300: true,
+  301: true,
+  302: true,
+  303: true,
+  305: true,
+  307: true,
+  308: true
+}
+
+// status codes for empty bodies
+status.empty = {
+  204: true,
+  205: true,
+  304: true
+}
+
+// status codes for when you should retry the request
+status.retry = {
+  502: true,
+  503: true,
+  504: true
+}
+
+/**
+ * Create a map of message to status code.
+ * @private
+ */
+
+function createMessageToStatusCodeMap (codes) {
+  var map = {}
+
+  Object.keys(codes).forEach(function forEachCode (code) {
+    var message = codes[code]
+    var status = Number(code)
+
+    // populate map
+    map[message.toLowerCase()] = status
+  })
+
+  return map
+}
+
+/**
+ * Create a list of all status codes.
+ * @private
+ */
+
+function createStatusCodeList (codes) {
+  return Object.keys(codes).map(function mapCode (code) {
+    return Number(code)
+  })
+}
+
+/**
+ * Get the status code for given message.
+ * @private
+ */
+
+function getStatusCode (message) {
+  var msg = message.toLowerCase()
+
+  if (!Object.prototype.hasOwnProperty.call(status.code, msg)) {
+    throw new Error('invalid status message: "' + message + '"')
+  }
+
+  return status.code[msg]
+}
+
+/**
+ * Get the status message for given code.
+ * @private
+ */
+
+function getStatusMessage (code) {
+  if (!Object.prototype.hasOwnProperty.call(status.message, code)) {
+    throw new Error('invalid status code: ' + code)
+  }
+
+  return status.message[code]
+}
+
+/**
+ * Get the status code.
+ *
+ * Given a number, this will throw if it is not a known status
+ * code, otherwise the code will be returned. Given a string,
+ * the string will be parsed for a number and return the code
+ * if valid, otherwise will lookup the code assuming this is
+ * the status message.
+ *
+ * @param {string|number} code
+ * @returns {number}
+ * @public
+ */
+
+function status (code) {
+  if (typeof code === 'number') {
+    return getStatusMessage(code)
+  }
+
+  if (typeof code !== 'string') {
+    throw new TypeError('code must be a number or string')
+  }
+
+  // '403'
+  var n = parseInt(code, 10)
+  if (!isNaN(n)) {
+    return getStatusMessage(n)
+  }
+
+  return getStatusCode(code)
+}

+ 72 - 0
backend/node_modules/buffer-from/index.js

@@ -0,0 +1,72 @@
+/* eslint-disable node/no-deprecated-api */
+
+var toString = Object.prototype.toString
+
+var isModern = (
+  typeof Buffer !== 'undefined' &&
+  typeof Buffer.alloc === 'function' &&
+  typeof Buffer.allocUnsafe === 'function' &&
+  typeof Buffer.from === 'function'
+)
+
+function isArrayBuffer (input) {
+  return toString.call(input).slice(8, -1) === 'ArrayBuffer'
+}
+
+function fromArrayBuffer (obj, byteOffset, length) {
+  byteOffset >>>= 0
+
+  var maxLength = obj.byteLength - byteOffset
+
+  if (maxLength < 0) {
+    throw new RangeError("'offset' is out of bounds")
+  }
+
+  if (length === undefined) {
+    length = maxLength
+  } else {
+    length >>>= 0
+
+    if (length > maxLength) {
+      throw new RangeError("'length' is out of bounds")
+    }
+  }
+
+  return isModern
+    ? Buffer.from(obj.slice(byteOffset, byteOffset + length))
+    : new Buffer(new Uint8Array(obj.slice(byteOffset, byteOffset + length)))
+}
+
+function fromString (string, encoding) {
+  if (typeof encoding !== 'string' || encoding === '') {
+    encoding = 'utf8'
+  }
+
+  if (!Buffer.isEncoding(encoding)) {
+    throw new TypeError('"encoding" must be a valid string encoding')
+  }
+
+  return isModern
+    ? Buffer.from(string, encoding)
+    : new Buffer(string, encoding)
+}
+
+function bufferFrom (value, encodingOrOffset, length) {
+  if (typeof value === 'number') {
+    throw new TypeError('"value" argument must not be a number')
+  }
+
+  if (isArrayBuffer(value)) {
+    return fromArrayBuffer(value, encodingOrOffset, length)
+  }
+
+  if (typeof value === 'string') {
+    return fromString(value, encodingOrOffset)
+  }
+
+  return isModern
+    ? Buffer.from(value)
+    : new Buffer(value)
+}
+
+module.exports = bufferFrom

+ 5 - 0
backend/node_modules/busboy/.eslintrc.js

@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = {
+  extends: '@mscdex/eslint-config',
+};

+ 149 - 0
backend/node_modules/busboy/bench/bench-multipart-fields-100mb-big.js

@@ -0,0 +1,149 @@
+'use strict';
+
+function createMultipartBuffers(boundary, sizes) {
+  const bufs = [];
+  for (let i = 0; i < sizes.length; ++i) {
+    const mb = sizes[i] * 1024 * 1024;
+    bufs.push(Buffer.from([
+      `--${boundary}`,
+      `content-disposition: form-data; name="field${i + 1}"`,
+      '',
+      '0'.repeat(mb),
+      '',
+    ].join('\r\n')));
+  }
+  bufs.push(Buffer.from([
+    `--${boundary}--`,
+    '',
+  ].join('\r\n')));
+  return bufs;
+}
+
+const boundary = '-----------------------------168072824752491622650073';
+const buffers = createMultipartBuffers(boundary, [
+  10,
+  10,
+  10,
+  20,
+  50,
+]);
+const calls = {
+  partBegin: 0,
+  headerField: 0,
+  headerValue: 0,
+  headerEnd: 0,
+  headersEnd: 0,
+  partData: 0,
+  partEnd: 0,
+  end: 0,
+};
+
+const moduleName = process.argv[2];
+switch (moduleName) {
+  case 'busboy': {
+    const busboy = require('busboy');
+
+    const parser = busboy({
+      limits: {
+        fieldSizeLimit: Infinity,
+      },
+      headers: {
+        'content-type': `multipart/form-data; boundary=${boundary}`,
+      },
+    });
+    parser.on('field', (name, val, info) => {
+      ++calls.partBegin;
+      ++calls.partData;
+      ++calls.partEnd;
+    }).on('close', () => {
+      ++calls.end;
+      console.timeEnd(moduleName);
+    });
+
+    console.time(moduleName);
+    for (const buf of buffers)
+      parser.write(buf);
+    break;
+  }
+
+  case 'formidable': {
+    const { MultipartParser } = require('formidable');
+
+    const parser = new MultipartParser();
+    parser.initWithBoundary(boundary);
+    parser.on('data', ({ name }) => {
+      ++calls[name];
+      if (name === 'end')
+        console.timeEnd(moduleName);
+    });
+
+    console.time(moduleName);
+    for (const buf of buffers)
+      parser.write(buf);
+
+    break;
+  }
+
+  case 'multiparty': {
+    const { Readable } = require('stream');
+
+    const { Form } = require('multiparty');
+
+    const form = new Form({
+      maxFieldsSize: Infinity,
+      maxFields: Infinity,
+      maxFilesSize: Infinity,
+      autoFields: false,
+      autoFiles: false,
+    });
+
+    const req = new Readable({ read: () => {} });
+    req.headers = {
+      'content-type': `multipart/form-data; boundary=${boundary}`,
+    };
+
+    function hijack(name, fn) {
+      const oldFn = form[name];
+      form[name] = function() {
+        fn();
+        return oldFn.apply(this, arguments);
+      };
+    }
+
+    hijack('onParseHeaderField', () => {
+      ++calls.headerField;
+    });
+    hijack('onParseHeaderValue', () => {
+      ++calls.headerValue;
+    });
+    hijack('onParsePartBegin', () => {
+      ++calls.partBegin;
+    });
+    hijack('onParsePartData', () => {
+      ++calls.partData;
+    });
+    hijack('onParsePartEnd', () => {
+      ++calls.partEnd;
+    });
+
+    form.on('close', () => {
+      ++calls.end;
+      console.timeEnd(moduleName);
+    }).on('part', (p) => p.resume());
+
+    console.time(moduleName);
+    form.parse(req);
+    for (const buf of buffers)
+      req.push(buf);
+    req.push(null);
+
+    break;
+  }
+
+  default:
+    if (moduleName === undefined)
+      console.error('Missing parser module name');
+    else
+      console.error(`Invalid parser module name: ${moduleName}`);
+    process.exit(1);
+}

+ 143 - 0
backend/node_modules/busboy/bench/bench-multipart-fields-100mb-small.js

@@ -0,0 +1,143 @@
+'use strict';
+
+function createMultipartBuffers(boundary, sizes) {
+  const bufs = [];
+  for (let i = 0; i < sizes.length; ++i) {
+    const mb = sizes[i] * 1024 * 1024;
+    bufs.push(Buffer.from([
+      `--${boundary}`,
+      `content-disposition: form-data; name="field${i + 1}"`,
+      '',
+      '0'.repeat(mb),
+      '',
+    ].join('\r\n')));
+  }
+  bufs.push(Buffer.from([
+    `--${boundary}--`,
+    '',
+  ].join('\r\n')));
+  return bufs;
+}
+
+const boundary = '-----------------------------168072824752491622650073';
+const buffers = createMultipartBuffers(boundary, (new Array(100)).fill(1));
+const calls = {
+  partBegin: 0,
+  headerField: 0,
+  headerValue: 0,
+  headerEnd: 0,
+  headersEnd: 0,
+  partData: 0,
+  partEnd: 0,
+  end: 0,
+};
+
+const moduleName = process.argv[2];
+switch (moduleName) {
+  case 'busboy': {
+    const busboy = require('busboy');
+
+    const parser = busboy({
+      limits: {
+        fieldSizeLimit: Infinity,
+      },
+      headers: {
+        'content-type': `multipart/form-data; boundary=${boundary}`,
+      },
+    });
+    parser.on('field', (name, val, info) => {
+      ++calls.partBegin;
+      ++calls.partData;
+      ++calls.partEnd;
+    }).on('close', () => {
+      ++calls.end;
+      console.timeEnd(moduleName);
+    });
+
+    console.time(moduleName);
+    for (const buf of buffers)
+      parser.write(buf);
+    break;
+  }
+
+  case 'formidable': {
+    const { MultipartParser } = require('formidable');
+
+    const parser = new MultipartParser();
+    parser.initWithBoundary(boundary);
+    parser.on('data', ({ name }) => {
+      ++calls[name];
+      if (name === 'end')
+        console.timeEnd(moduleName);
+    });
+
+    console.time(moduleName);
+    for (const buf of buffers)
+      parser.write(buf);
+
+    break;
+  }
+
+  case 'multiparty': {
+    const { Readable } = require('stream');
+
+    const { Form } = require('multiparty');
+
+    const form = new Form({
+      maxFieldsSize: Infinity,
+      maxFields: Infinity,
+      maxFilesSize: Infinity,
+      autoFields: false,
+      autoFiles: false,
+    });
+
+    const req = new Readable({ read: () => {} });
+    req.headers = {
+      'content-type': `multipart/form-data; boundary=${boundary}`,
+    };
+
+    function hijack(name, fn) {
+      const oldFn = form[name];
+      form[name] = function() {
+        fn();
+        return oldFn.apply(this, arguments);
+      };
+    }
+
+    hijack('onParseHeaderField', () => {
+      ++calls.headerField;
+    });
+    hijack('onParseHeaderValue', () => {
+      ++calls.headerValue;
+    });
+    hijack('onParsePartBegin', () => {
+      ++calls.partBegin;
+    });
+    hijack('onParsePartData', () => {
+      ++calls.partData;
+    });
+    hijack('onParsePartEnd', () => {
+      ++calls.partEnd;
+    });
+
+    form.on('close', () => {
+      ++calls.end;
+      console.timeEnd(moduleName);
+    }).on('part', (p) => p.resume());
+
+    console.time(moduleName);
+    form.parse(req);
+    for (const buf of buffers)
+      req.push(buf);
+    req.push(null);
+
+    break;
+  }
+
+  default:
+    if (moduleName === undefined)
+      console.error('Missing parser module name');
+    else
+      console.error(`Invalid parser module name: ${moduleName}`);
+    process.exit(1);
+}

+ 154 - 0
backend/node_modules/busboy/bench/bench-multipart-files-100mb-big.js

@@ -0,0 +1,154 @@
+'use strict';
+
+function createMultipartBuffers(boundary, sizes) {
+  const bufs = [];
+  for (let i = 0; i < sizes.length; ++i) {
+    const mb = sizes[i] * 1024 * 1024;
+    bufs.push(Buffer.from([
+      `--${boundary}`,
+      `content-disposition: form-data; name="file${i + 1}"; `
+        + `filename="random${i + 1}.bin"`,
+      'content-type: application/octet-stream',
+      '',
+      '0'.repeat(mb),
+      '',
+    ].join('\r\n')));
+  }
+  bufs.push(Buffer.from([
+    `--${boundary}--`,
+    '',
+  ].join('\r\n')));
+  return bufs;
+}
+
+const boundary = '-----------------------------168072824752491622650073';
+const buffers = createMultipartBuffers(boundary, [
+  10,
+  10,
+  10,
+  20,
+  50,
+]);
+const calls = {
+  partBegin: 0,
+  headerField: 0,
+  headerValue: 0,
+  headerEnd: 0,
+  headersEnd: 0,
+  partData: 0,
+  partEnd: 0,
+  end: 0,
+};
+
+const moduleName = process.argv[2];
+switch (moduleName) {
+  case 'busboy': {
+    const busboy = require('busboy');
+
+    const parser = busboy({
+      limits: {
+        fieldSizeLimit: Infinity,
+      },
+      headers: {
+        'content-type': `multipart/form-data; boundary=${boundary}`,
+      },
+    });
+    parser.on('file', (name, stream, info) => {
+      ++calls.partBegin;
+      stream.on('data', (chunk) => {
+        ++calls.partData;
+      }).on('end', () => {
+        ++calls.partEnd;
+      });
+    }).on('close', () => {
+      ++calls.end;
+      console.timeEnd(moduleName);
+    });
+
+    console.time(moduleName);
+    for (const buf of buffers)
+      parser.write(buf);
+    break;
+  }
+
+  case 'formidable': {
+    const { MultipartParser } = require('formidable');
+
+    const parser = new MultipartParser();
+    parser.initWithBoundary(boundary);
+    parser.on('data', ({ name }) => {
+      ++calls[name];
+      if (name === 'end')
+        console.timeEnd(moduleName);
+    });
+
+    console.time(moduleName);
+    for (const buf of buffers)
+      parser.write(buf);
+
+    break;
+  }
+
+  case 'multiparty': {
+    const { Readable } = require('stream');
+
+    const { Form } = require('multiparty');
+
+    const form = new Form({
+      maxFieldsSize: Infinity,
+      maxFields: Infinity,
+      maxFilesSize: Infinity,
+      autoFields: false,
+      autoFiles: false,
+    });
+
+    const req = new Readable({ read: () => {} });
+    req.headers = {
+      'content-type': `multipart/form-data; boundary=${boundary}`,
+    };
+
+    function hijack(name, fn) {
+      const oldFn = form[name];
+      form[name] = function() {
+        fn();
+        return oldFn.apply(this, arguments);
+      };
+    }
+
+    hijack('onParseHeaderField', () => {
+      ++calls.headerField;
+    });
+    hijack('onParseHeaderValue', () => {
+      ++calls.headerValue;
+    });
+    hijack('onParsePartBegin', () => {
+      ++calls.partBegin;
+    });
+    hijack('onParsePartData', () => {
+      ++calls.partData;
+    });
+    hijack('onParsePartEnd', () => {
+      ++calls.partEnd;
+    });
+
+    form.on('close', () => {
+      ++calls.end;
+      console.timeEnd(moduleName);
+    }).on('part', (p) => p.resume());
+
+    console.time(moduleName);
+    form.parse(req);
+    for (const buf of buffers)
+      req.push(buf);
+    req.push(null);
+
+    break;
+  }
+
+  default:
+    if (moduleName === undefined)
+      console.error('Missing parser module name');
+    else
+      console.error(`Invalid parser module name: ${moduleName}`);
+    process.exit(1);
+}

+ 148 - 0
backend/node_modules/busboy/bench/bench-multipart-files-100mb-small.js

@@ -0,0 +1,148 @@
+'use strict';
+
+function createMultipartBuffers(boundary, sizes) {
+  const bufs = [];
+  for (let i = 0; i < sizes.length; ++i) {
+    const mb = sizes[i] * 1024 * 1024;
+    bufs.push(Buffer.from([
+      `--${boundary}`,
+      `content-disposition: form-data; name="file${i + 1}"; `
+        + `filename="random${i + 1}.bin"`,
+      'content-type: application/octet-stream',
+      '',
+      '0'.repeat(mb),
+      '',
+    ].join('\r\n')));
+  }
+  bufs.push(Buffer.from([
+    `--${boundary}--`,
+    '',
+  ].join('\r\n')));
+  return bufs;
+}
+
+const boundary = '-----------------------------168072824752491622650073';
+const buffers = createMultipartBuffers(boundary, (new Array(100)).fill(1));
+const calls = {
+  partBegin: 0,
+  headerField: 0,
+  headerValue: 0,
+  headerEnd: 0,
+  headersEnd: 0,
+  partData: 0,
+  partEnd: 0,
+  end: 0,
+};
+
+const moduleName = process.argv[2];
+switch (moduleName) {
+  case 'busboy': {
+    const busboy = require('busboy');
+
+    const parser = busboy({
+      limits: {
+        fieldSizeLimit: Infinity,
+      },
+      headers: {
+        'content-type': `multipart/form-data; boundary=${boundary}`,
+      },
+    });
+    parser.on('file', (name, stream, info) => {
+      ++calls.partBegin;
+      stream.on('data', (chunk) => {
+        ++calls.partData;
+      }).on('end', () => {
+        ++calls.partEnd;
+      });
+    }).on('close', () => {
+      ++calls.end;
+      console.timeEnd(moduleName);
+    });
+
+    console.time(moduleName);
+    for (const buf of buffers)
+      parser.write(buf);
+    break;
+  }
+
+  case 'formidable': {
+    const { MultipartParser } = require('formidable');
+
+    const parser = new MultipartParser();
+    parser.initWithBoundary(boundary);
+    parser.on('data', ({ name }) => {
+      ++calls[name];
+      if (name === 'end')
+        console.timeEnd(moduleName);
+    });
+
+    console.time(moduleName);
+    for (const buf of buffers)
+      parser.write(buf);
+
+    break;
+  }
+
+  case 'multiparty': {
+    const { Readable } = require('stream');
+
+    const { Form } = require('multiparty');
+
+    const form = new Form({
+      maxFieldsSize: Infinity,
+      maxFields: Infinity,
+      maxFilesSize: Infinity,
+      autoFields: false,
+      autoFiles: false,
+    });
+
+    const req = new Readable({ read: () => {} });
+    req.headers = {
+      'content-type': `multipart/form-data; boundary=${boundary}`,
+    };
+
+    function hijack(name, fn) {
+      const oldFn = form[name];
+      form[name] = function() {
+        fn();
+        return oldFn.apply(this, arguments);
+      };
+    }
+
+    hijack('onParseHeaderField', () => {
+      ++calls.headerField;
+    });
+    hijack('onParseHeaderValue', () => {
+      ++calls.headerValue;
+    });
+    hijack('onParsePartBegin', () => {
+      ++calls.partBegin;
+    });
+    hijack('onParsePartData', () => {
+      ++calls.partData;
+    });
+    hijack('onParsePartEnd', () => {
+      ++calls.partEnd;
+    });
+
+    form.on('close', () => {
+      ++calls.end;
+      console.timeEnd(moduleName);
+    }).on('part', (p) => p.resume());
+
+    console.time(moduleName);
+    form.parse(req);
+    for (const buf of buffers)
+      req.push(buf);
+    req.push(null);
+
+    break;
+  }
+
+  default:
+    if (moduleName === undefined)
+      console.error('Missing parser module name');
+    else
+      console.error(`Invalid parser module name: ${moduleName}`);
+    process.exit(1);
+}

+ 101 - 0
backend/node_modules/busboy/bench/bench-urlencoded-fields-100pairs-small.js

@@ -0,0 +1,101 @@
+'use strict';
+
+const buffers = [
+  Buffer.from(
+    (new Array(100)).fill('').map((_, i) => `key${i}=value${i}`).join('&')
+  ),
+];
+const calls = {
+  field: 0,
+  end: 0,
+};
+
+let n = 3e3;
+
+const moduleName = process.argv[2];
+switch (moduleName) {
+  case 'busboy': {
+    const busboy = require('busboy');
+
+    console.time(moduleName);
+    (function next() {
+      const parser = busboy({
+        limits: {
+          fieldSizeLimit: Infinity,
+        },
+        headers: {
+          'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
+        },
+      });
+      parser.on('field', (name, val, info) => {
+        ++calls.field;
+      }).on('close', () => {
+        ++calls.end;
+        if (--n === 0)
+          console.timeEnd(moduleName);
+        else
+          process.nextTick(next);
+      });
+
+      for (const buf of buffers)
+        parser.write(buf);
+      parser.end();
+    })();
+    break;
+  }
+
+  case 'formidable': {
+    const QuerystringParser =
+      require('formidable/src/parsers/Querystring.js');
+
+    console.time(moduleName);
+    (function next() {
+      const parser = new QuerystringParser();
+      parser.on('data', (obj) => {
+        ++calls.field;
+      }).on('end', () => {
+        ++calls.end;
+        if (--n === 0)
+          console.timeEnd(moduleName);
+        else
+          process.nextTick(next);
+      });
+
+      for (const buf of buffers)
+        parser.write(buf);
+      parser.end();
+    })();
+    break;
+  }
+
+  case 'formidable-streaming': {
+    const QuerystringParser =
+      require('formidable/src/parsers/StreamingQuerystring.js');
+
+    console.time(moduleName);
+    (function next() {
+      const parser = new QuerystringParser();
+      parser.on('data', (obj) => {
+        ++calls.field;
+      }).on('end', () => {
+        ++calls.end;
+        if (--n === 0)
+          console.timeEnd(moduleName);
+        else
+          process.nextTick(next);
+      });
+
+      for (const buf of buffers)
+        parser.write(buf);
+      parser.end();
+    })();
+    break;
+  }
+
+  default:
+    if (moduleName === undefined)
+      console.error('Missing parser module name');
+    else
+      console.error(`Invalid parser module name: ${moduleName}`);
+    process.exit(1);
+}

+ 84 - 0
backend/node_modules/busboy/bench/bench-urlencoded-fields-900pairs-small-alt.js

@@ -0,0 +1,84 @@
+'use strict';
+
+const buffers = [
+  Buffer.from(
+    (new Array(900)).fill('').map((_, i) => `key${i}=value${i}`).join('&')
+  ),
+];
+const calls = {
+  field: 0,
+  end: 0,
+};
+
+const moduleName = process.argv[2];
+switch (moduleName) {
+  case 'busboy': {
+    const busboy = require('busboy');
+
+    console.time(moduleName);
+    const parser = busboy({
+      limits: {
+        fieldSizeLimit: Infinity,
+      },
+      headers: {
+        'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
+      },
+    });
+    parser.on('field', (name, val, info) => {
+      ++calls.field;
+    }).on('close', () => {
+      ++calls.end;
+      console.timeEnd(moduleName);
+    });
+
+    for (const buf of buffers)
+      parser.write(buf);
+    parser.end();
+    break;
+  }
+
+  case 'formidable': {
+    const QuerystringParser =
+      require('formidable/src/parsers/Querystring.js');
+
+    console.time(moduleName);
+    const parser = new QuerystringParser();
+    parser.on('data', (obj) => {
+      ++calls.field;
+    }).on('end', () => {
+      ++calls.end;
+      console.timeEnd(moduleName);
+    });
+
+    for (const buf of buffers)
+      parser.write(buf);
+    parser.end();
+    break;
+  }
+
+  case 'formidable-streaming': {
+    const QuerystringParser =
+      require('formidable/src/parsers/StreamingQuerystring.js');
+
+    console.time(moduleName);
+    const parser = new QuerystringParser();
+    parser.on('data', (obj) => {
+      ++calls.field;
+    }).on('end', () => {
+      ++calls.end;
+      console.timeEnd(moduleName);
+    });
+
+    for (const buf of buffers)
+      parser.write(buf);
+    parser.end();
+    break;
+  }
+
+  default:
+    if (moduleName === undefined)
+      console.error('Missing parser module name');
+    else
+      console.error(`Invalid parser module name: ${moduleName}`);
+    process.exit(1);
+}

+ 57 - 0
backend/node_modules/busboy/lib/index.js

@@ -0,0 +1,57 @@
+'use strict';
+
+const { parseContentType } = require('./utils.js');
+
+function getInstance(cfg) {
+  const headers = cfg.headers;
+  const conType = parseContentType(headers['content-type']);
+  if (!conType)
+    throw new Error('Malformed content type');
+
+  for (const type of TYPES) {
+    const matched = type.detect(conType);
+    if (!matched)
+      continue;
+
+    const instanceCfg = {
+      limits: cfg.limits,
+      headers,
+      conType,
+      highWaterMark: undefined,
+      fileHwm: undefined,
+      defCharset: undefined,
+      defParamCharset: undefined,
+      preservePath: false,
+    };
+    if (cfg.highWaterMark)
+      instanceCfg.highWaterMark = cfg.highWaterMark;
+    if (cfg.fileHwm)
+      instanceCfg.fileHwm = cfg.fileHwm;
+    instanceCfg.defCharset = cfg.defCharset;
+    instanceCfg.defParamCharset = cfg.defParamCharset;
+    instanceCfg.preservePath = cfg.preservePath;
+    return new type(instanceCfg);
+  }
+
+  throw new Error(`Unsupported content type: ${headers['content-type']}`);
+}
+
+// Note: types are explicitly listed here for easier bundling
+// See: https://github.com/mscdex/busboy/issues/121
+const TYPES = [
+  require('./types/multipart'),
+  require('./types/urlencoded'),
+].filter(function(typemod) { return typeof typemod.detect === 'function'; });
+
+module.exports = (cfg) => {
+  if (typeof cfg !== 'object' || cfg === null)
+    cfg = {};
+
+  if (typeof cfg.headers !== 'object'
+      || cfg.headers === null
+      || typeof cfg.headers['content-type'] !== 'string') {
+    throw new Error('Missing Content-Type');
+  }
+
+  return getInstance(cfg);
+};

+ 653 - 0
backend/node_modules/busboy/lib/types/multipart.js

@@ -0,0 +1,653 @@
+'use strict';
+
+const { Readable, Writable } = require('stream');
+
+const StreamSearch = require('streamsearch');
+
+const {
+  basename,
+  convertToUTF8,
+  getDecoder,
+  parseContentType,
+  parseDisposition,
+} = require('../utils.js');
+
+const BUF_CRLF = Buffer.from('\r\n');
+const BUF_CR = Buffer.from('\r');
+const BUF_DASH = Buffer.from('-');
+
+function noop() {}
+
+const MAX_HEADER_PAIRS = 2000; // From node
+const MAX_HEADER_SIZE = 16 * 1024; // From node (its default value)
+
+const HPARSER_NAME = 0;
+const HPARSER_PRE_OWS = 1;
+const HPARSER_VALUE = 2;
+class HeaderParser {
+  constructor(cb) {
+    this.header = Object.create(null);
+    this.pairCount = 0;
+    this.byteCount = 0;
+    this.state = HPARSER_NAME;
+    this.name = '';
+    this.value = '';
+    this.crlf = 0;
+    this.cb = cb;
+  }
+
+  reset() {
+    this.header = Object.create(null);
+    this.pairCount = 0;
+    this.byteCount = 0;
+    this.state = HPARSER_NAME;
+    this.name = '';
+    this.value = '';
+    this.crlf = 0;
+  }
+
+  push(chunk, pos, end) {
+    let start = pos;
+    while (pos < end) {
+      switch (this.state) {
+        case HPARSER_NAME: {
+          let done = false;
+          for (; pos < end; ++pos) {
+            if (this.byteCount === MAX_HEADER_SIZE)
+              return -1;
+            ++this.byteCount;
+            const code = chunk[pos];
+            if (TOKEN[code] !== 1) {
+              if (code !== 58/* ':' */)
+                return -1;
+              this.name += chunk.latin1Slice(start, pos);
+              if (this.name.length === 0)
+                return -1;
+              ++pos;
+              done = true;
+              this.state = HPARSER_PRE_OWS;
+              break;
+            }
+          }
+          if (!done) {
+            this.name += chunk.latin1Slice(start, pos);
+            break;
+          }
+          // FALLTHROUGH
+        }
+        case HPARSER_PRE_OWS: {
+          // Skip optional whitespace
+          let done = false;
+          for (; pos < end; ++pos) {
+            if (this.byteCount === MAX_HEADER_SIZE)
+              return -1;
+            ++this.byteCount;
+            const code = chunk[pos];
+            if (code !== 32/* ' ' */ && code !== 9/* '\t' */) {
+              start = pos;
+              done = true;
+              this.state = HPARSER_VALUE;
+              break;
+            }
+          }
+          if (!done)
+            break;
+          // FALLTHROUGH
+        }
+        case HPARSER_VALUE:
+          switch (this.crlf) {
+            case 0: // Nothing yet
+              for (; pos < end; ++pos) {
+                if (this.byteCount === MAX_HEADER_SIZE)
+                  return -1;
+                ++this.byteCount;
+                const code = chunk[pos];
+                if (FIELD_VCHAR[code] !== 1) {
+                  if (code !== 13/* '\r' */)
+                    return -1;
+                  ++this.crlf;
+                  break;
+                }
+              }
+              this.value += chunk.latin1Slice(start, pos++);
+              break;
+            case 1: // Received CR
+              if (this.byteCount === MAX_HEADER_SIZE)
+                return -1;
+              ++this.byteCount;
+              if (chunk[pos++] !== 10/* '\n' */)
+                return -1;
+              ++this.crlf;
+              break;
+            case 2: { // Received CR LF
+              if (this.byteCount === MAX_HEADER_SIZE)
+                return -1;
+              ++this.byteCount;
+              const code = chunk[pos];
+              if (code === 32/* ' ' */ || code === 9/* '\t' */) {
+                // Folded value
+                start = pos;
+                this.crlf = 0;
+              } else {
+                if (++this.pairCount < MAX_HEADER_PAIRS) {
+                  this.name = this.name.toLowerCase();
+                  if (this.header[this.name] === undefined)
+                    this.header[this.name] = [this.value];
+                  else
+                    this.header[this.name].push(this.value);
+                }
+                if (code === 13/* '\r' */) {
+                  ++this.crlf;
+                  ++pos;
+                } else {
+                  // Assume start of next header field name
+                  start = pos;
+                  this.crlf = 0;
+                  this.state = HPARSER_NAME;
+                  this.name = '';
+                  this.value = '';
+                }
+              }
+              break;
+            }
+            case 3: { // Received CR LF CR
+              if (this.byteCount === MAX_HEADER_SIZE)
+                return -1;
+              ++this.byteCount;
+              if (chunk[pos++] !== 10/* '\n' */)
+                return -1;
+              // End of header
+              const header = this.header;
+              this.reset();
+              this.cb(header);
+              return pos;
+            }
+          }
+          break;
+      }
+    }
+
+    return pos;
+  }
+}
+
+class FileStream extends Readable {
+  constructor(opts, owner) {
+    super(opts);
+    this.truncated = false;
+    this._readcb = null;
+    this.once('end', () => {
+      // We need to make sure that we call any outstanding _writecb() that is
+      // associated with this file so that processing of the rest of the form
+      // can continue. This may not happen if the file stream ends right after
+      // backpressure kicks in, so we force it here.
+      this._read();
+      if (--owner._fileEndsLeft === 0 && owner._finalcb) {
+        const cb = owner._finalcb;
+        owner._finalcb = null;
+        // Make sure other 'end' event handlers get a chance to be executed
+        // before busboy's 'finish' event is emitted
+        process.nextTick(cb);
+      }
+    });
+  }
+  _read(n) {
+    const cb = this._readcb;
+    if (cb) {
+      this._readcb = null;
+      cb();
+    }
+  }
+}
+
+const ignoreData = {
+  push: (chunk, pos) => {},
+  destroy: () => {},
+};
+
+function callAndUnsetCb(self, err) {
+  const cb = self._writecb;
+  self._writecb = null;
+  if (err)
+    self.destroy(err);
+  else if (cb)
+    cb();
+}
+
+function nullDecoder(val, hint) {
+  return val;
+}
+
+class Multipart extends Writable {
+  constructor(cfg) {
+    const streamOpts = {
+      autoDestroy: true,
+      emitClose: true,
+      highWaterMark: (typeof cfg.highWaterMark === 'number'
+                      ? cfg.highWaterMark
+                      : undefined),
+    };
+    super(streamOpts);
+
+    if (!cfg.conType.params || typeof cfg.conType.params.boundary !== 'string')
+      throw new Error('Multipart: Boundary not found');
+
+    const boundary = cfg.conType.params.boundary;
+    const paramDecoder = (typeof cfg.defParamCharset === 'string'
+                            && cfg.defParamCharset
+                          ? getDecoder(cfg.defParamCharset)
+                          : nullDecoder);
+    const defCharset = (cfg.defCharset || 'utf8');
+    const preservePath = cfg.preservePath;
+    const fileOpts = {
+      autoDestroy: true,
+      emitClose: true,
+      highWaterMark: (typeof cfg.fileHwm === 'number'
+                      ? cfg.fileHwm
+                      : undefined),
+    };
+
+    const limits = cfg.limits;
+    const fieldSizeLimit = (limits && typeof limits.fieldSize === 'number'
+                            ? limits.fieldSize
+                            : 1 * 1024 * 1024);
+    const fileSizeLimit = (limits && typeof limits.fileSize === 'number'
+                           ? limits.fileSize
+                           : Infinity);
+    const filesLimit = (limits && typeof limits.files === 'number'
+                        ? limits.files
+                        : Infinity);
+    const fieldsLimit = (limits && typeof limits.fields === 'number'
+                         ? limits.fields
+                         : Infinity);
+    const partsLimit = (limits && typeof limits.parts === 'number'
+                        ? limits.parts
+                        : Infinity);
+
+    let parts = -1; // Account for initial boundary
+    let fields = 0;
+    let files = 0;
+    let skipPart = false;
+
+    this._fileEndsLeft = 0;
+    this._fileStream = undefined;
+    this._complete = false;
+    let fileSize = 0;
+
+    let field;
+    let fieldSize = 0;
+    let partCharset;
+    let partEncoding;
+    let partType;
+    let partName;
+    let partTruncated = false;
+
+    let hitFilesLimit = false;
+    let hitFieldsLimit = false;
+
+    this._hparser = null;
+    const hparser = new HeaderParser((header) => {
+      this._hparser = null;
+      skipPart = false;
+
+      partType = 'text/plain';
+      partCharset = defCharset;
+      partEncoding = '7bit';
+      partName = undefined;
+      partTruncated = false;
+
+      let filename;
+      if (!header['content-disposition']) {
+        skipPart = true;
+        return;
+      }
+
+      const disp = parseDisposition(header['content-disposition'][0],
+                                    paramDecoder);
+      if (!disp || disp.type !== 'form-data') {
+        skipPart = true;
+        return;
+      }
+
+      if (disp.params) {
+        if (disp.params.name)
+          partName = disp.params.name;
+
+        if (disp.params['filename*'])
+          filename = disp.params['filename*'];
+        else if (disp.params.filename)
+          filename = disp.params.filename;
+
+        if (filename !== undefined && !preservePath)
+          filename = basename(filename);
+      }
+
+      if (header['content-type']) {
+        const conType = parseContentType(header['content-type'][0]);
+        if (conType) {
+          partType = `${conType.type}/${conType.subtype}`;
+          if (conType.params && typeof conType.params.charset === 'string')
+            partCharset = conType.params.charset.toLowerCase();
+        }
+      }
+
+      if (header['content-transfer-encoding'])
+        partEncoding = header['content-transfer-encoding'][0].toLowerCase();
+
+      if (partType === 'application/octet-stream' || filename !== undefined) {
+        // File
+
+        if (files === filesLimit) {
+          if (!hitFilesLimit) {
+            hitFilesLimit = true;
+            this.emit('filesLimit');
+          }
+          skipPart = true;
+          return;
+        }
+        ++files;
+
+        if (this.listenerCount('file') === 0) {
+          skipPart = true;
+          return;
+        }
+
+        fileSize = 0;
+        this._fileStream = new FileStream(fileOpts, this);
+        ++this._fileEndsLeft;
+        this.emit(
+          'file',
+          partName,
+          this._fileStream,
+          { filename,
+            encoding: partEncoding,
+            mimeType: partType }
+        );
+      } else {
+        // Non-file
+
+        if (fields === fieldsLimit) {
+          if (!hitFieldsLimit) {
+            hitFieldsLimit = true;
+            this.emit('fieldsLimit');
+          }
+          skipPart = true;
+          return;
+        }
+        ++fields;
+
+        if (this.listenerCount('field') === 0) {
+          skipPart = true;
+          return;
+        }
+
+        field = [];
+        fieldSize = 0;
+      }
+    });
+
+    let matchPostBoundary = 0;
+    const ssCb = (isMatch, data, start, end, isDataSafe) => {
+retrydata:
+      while (data) {
+        if (this._hparser !== null) {
+          const ret = this._hparser.push(data, start, end);
+          if (ret === -1) {
+            this._hparser = null;
+            hparser.reset();
+            this.emit('error', new Error('Malformed part header'));
+            break;
+          }
+          start = ret;
+        }
+
+        if (start === end)
+          break;
+
+        if (matchPostBoundary !== 0) {
+          if (matchPostBoundary === 1) {
+            switch (data[start]) {
+              case 45: // '-'
+                // Try matching '--' after boundary
+                matchPostBoundary = 2;
+                ++start;
+                break;
+              case 13: // '\r'
+                // Try matching CR LF before header
+                matchPostBoundary = 3;
+                ++start;
+                break;
+              default:
+                matchPostBoundary = 0;
+            }
+            if (start === end)
+              return;
+          }
+
+          if (matchPostBoundary === 2) {
+            matchPostBoundary = 0;
+            if (data[start] === 45/* '-' */) {
+              // End of multipart data
+              this._complete = true;
+              this._bparser = ignoreData;
+              return;
+            }
+            // We saw something other than '-', so put the dash we consumed
+            // "back"
+            const writecb = this._writecb;
+            this._writecb = noop;
+            ssCb(false, BUF_DASH, 0, 1, false);
+            this._writecb = writecb;
+          } else if (matchPostBoundary === 3) {
+            matchPostBoundary = 0;
+            if (data[start] === 10/* '\n' */) {
+              ++start;
+              if (parts >= partsLimit)
+                break;
+              // Prepare the header parser
+              this._hparser = hparser;
+              if (start === end)
+                break;
+              // Process the remaining data as a header
+              continue retrydata;
+            } else {
+              // We saw something other than LF, so put the CR we consumed
+              // "back"
+              const writecb = this._writecb;
+              this._writecb = noop;
+              ssCb(false, BUF_CR, 0, 1, false);
+              this._writecb = writecb;
+            }
+          }
+        }
+
+        if (!skipPart) {
+          if (this._fileStream) {
+            let chunk;
+            const actualLen = Math.min(end - start, fileSizeLimit - fileSize);
+            if (!isDataSafe) {
+              chunk = Buffer.allocUnsafe(actualLen);
+              data.copy(chunk, 0, start, start + actualLen);
+            } else {
+              chunk = data.slice(start, start + actualLen);
+            }
+
+            fileSize += chunk.length;
+            if (fileSize === fileSizeLimit) {
+              if (chunk.length > 0)
+                this._fileStream.push(chunk);
+              this._fileStream.emit('limit');
+              this._fileStream.truncated = true;
+              skipPart = true;
+            } else if (!this._fileStream.push(chunk)) {
+              if (this._writecb)
+                this._fileStream._readcb = this._writecb;
+              this._writecb = null;
+            }
+          } else if (field !== undefined) {
+            let chunk;
+            const actualLen = Math.min(
+              end - start,
+              fieldSizeLimit - fieldSize
+            );
+            if (!isDataSafe) {
+              chunk = Buffer.allocUnsafe(actualLen);
+              data.copy(chunk, 0, start, start + actualLen);
+            } else {
+              chunk = data.slice(start, start + actualLen);
+            }
+
+            fieldSize += actualLen;
+            field.push(chunk);
+            if (fieldSize === fieldSizeLimit) {
+              skipPart = true;
+              partTruncated = true;
+            }
+          }
+        }
+
+        break;
+      }
+
+      if (isMatch) {
+        matchPostBoundary = 1;
+
+        if (this._fileStream) {
+          // End the active file stream if the previous part was a file
+          this._fileStream.push(null);
+          this._fileStream = null;
+        } else if (field !== undefined) {
+          let data;
+          switch (field.length) {
+            case 0:
+              data = '';
+              break;
+            case 1:
+              data = convertToUTF8(field[0], partCharset, 0);
+              break;
+            default:
+              data = convertToUTF8(
+                Buffer.concat(field, fieldSize),
+                partCharset,
+                0
+              );
+          }
+          field = undefined;
+          fieldSize = 0;
+          this.emit(
+            'field',
+            partName,
+            data,
+            { nameTruncated: false,
+              valueTruncated: partTruncated,
+              encoding: partEncoding,
+              mimeType: partType }
+          );
+        }
+
+        if (++parts === partsLimit)
+          this.emit('partsLimit');
+      }
+    };
+    this._bparser = new StreamSearch(`\r\n--${boundary}`, ssCb);
+
+    this._writecb = null;
+    this._finalcb = null;
+
+    // Just in case there is no preamble
+    this.write(BUF_CRLF);
+  }
+
+  static detect(conType) {
+    return (conType.type === 'multipart' && conType.subtype === 'form-data');
+  }
+
+  _write(chunk, enc, cb) {
+    this._writecb = cb;
+    this._bparser.push(chunk, 0);
+    if (this._writecb)
+      callAndUnsetCb(this);
+  }
+
+  _destroy(err, cb) {
+    this._hparser = null;
+    this._bparser = ignoreData;
+    if (!err)
+      err = checkEndState(this);
+    const fileStream = this._fileStream;
+    if (fileStream) {
+      this._fileStream = null;
+      fileStream.destroy(err);
+    }
+    cb(err);
+  }
+
+  _final(cb) {
+    this._bparser.destroy();
+    if (!this._complete)
+      return cb(new Error('Unexpected end of form'));
+    if (this._fileEndsLeft)
+      this._finalcb = finalcb.bind(null, this, cb);
+    else
+      finalcb(this, cb);
+  }
+}
+
+function finalcb(self, cb, err) {
+  if (err)
+    return cb(err);
+  err = checkEndState(self);
+  cb(err);
+}
+
+function checkEndState(self) {
+  if (self._hparser)
+    return new Error('Malformed part header');
+  const fileStream = self._fileStream;
+  if (fileStream) {
+    self._fileStream = null;
+    fileStream.destroy(new Error('Unexpected end of file'));
+  }
+  if (!self._complete)
+    return new Error('Unexpected end of form');
+}
+
+const TOKEN = [
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+];
+
+const FIELD_VCHAR = [
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+];
+
+module.exports = Multipart;

+ 350 - 0
backend/node_modules/busboy/lib/types/urlencoded.js

@@ -0,0 +1,350 @@
+'use strict';
+
+const { Writable } = require('stream');
+
+const { getDecoder } = require('../utils.js');
+
+class URLEncoded extends Writable {
+  constructor(cfg) {
+    const streamOpts = {
+      autoDestroy: true,
+      emitClose: true,
+      highWaterMark: (typeof cfg.highWaterMark === 'number'
+                      ? cfg.highWaterMark
+                      : undefined),
+    };
+    super(streamOpts);
+
+    let charset = (cfg.defCharset || 'utf8');
+    if (cfg.conType.params && typeof cfg.conType.params.charset === 'string')
+      charset = cfg.conType.params.charset;
+
+    this.charset = charset;
+
+    const limits = cfg.limits;
+    this.fieldSizeLimit = (limits && typeof limits.fieldSize === 'number'
+                           ? limits.fieldSize
+                           : 1 * 1024 * 1024);
+    this.fieldsLimit = (limits && typeof limits.fields === 'number'
+                        ? limits.fields
+                        : Infinity);
+    this.fieldNameSizeLimit = (
+      limits && typeof limits.fieldNameSize === 'number'
+      ? limits.fieldNameSize
+      : 100
+    );
+
+    this._inKey = true;
+    this._keyTrunc = false;
+    this._valTrunc = false;
+    this._bytesKey = 0;
+    this._bytesVal = 0;
+    this._fields = 0;
+    this._key = '';
+    this._val = '';
+    this._byte = -2;
+    this._lastPos = 0;
+    this._encode = 0;
+    this._decoder = getDecoder(charset);
+  }
+
+  static detect(conType) {
+    return (conType.type === 'application'
+            && conType.subtype === 'x-www-form-urlencoded');
+  }
+
+  _write(chunk, enc, cb) {
+    if (this._fields >= this.fieldsLimit)
+      return cb();
+
+    let i = 0;
+    const len = chunk.length;
+    this._lastPos = 0;
+
+    // Check if we last ended mid-percent-encoded byte
+    if (this._byte !== -2) {
+      i = readPctEnc(this, chunk, i, len);
+      if (i === -1)
+        return cb(new Error('Malformed urlencoded form'));
+      if (i >= len)
+        return cb();
+      if (this._inKey)
+        ++this._bytesKey;
+      else
+        ++this._bytesVal;
+    }
+
+main:
+    while (i < len) {
+      if (this._inKey) {
+        // Parsing key
+
+        i = skipKeyBytes(this, chunk, i, len);
+
+        while (i < len) {
+          switch (chunk[i]) {
+            case 61: // '='
+              if (this._lastPos < i)
+                this._key += chunk.latin1Slice(this._lastPos, i);
+              this._lastPos = ++i;
+              this._key = this._decoder(this._key, this._encode);
+              this._encode = 0;
+              this._inKey = false;
+              continue main;
+            case 38: // '&'
+              if (this._lastPos < i)
+                this._key += chunk.latin1Slice(this._lastPos, i);
+              this._lastPos = ++i;
+              this._key = this._decoder(this._key, this._encode);
+              this._encode = 0;
+              if (this._bytesKey > 0) {
+                this.emit(
+                  'field',
+                  this._key,
+                  '',
+                  { nameTruncated: this._keyTrunc,
+                    valueTruncated: false,
+                    encoding: this.charset,
+                    mimeType: 'text/plain' }
+                );
+              }
+              this._key = '';
+              this._val = '';
+              this._keyTrunc = false;
+              this._valTrunc = false;
+              this._bytesKey = 0;
+              this._bytesVal = 0;
+              if (++this._fields >= this.fieldsLimit) {
+                this.emit('fieldsLimit');
+                return cb();
+              }
+              continue;
+            case 43: // '+'
+              if (this._lastPos < i)
+                this._key += chunk.latin1Slice(this._lastPos, i);
+              this._key += ' ';
+              this._lastPos = i + 1;
+              break;
+            case 37: // '%'
+              if (this._encode === 0)
+                this._encode = 1;
+              if (this._lastPos < i)
+                this._key += chunk.latin1Slice(this._lastPos, i);
+              this._lastPos = i + 1;
+              this._byte = -1;
+              i = readPctEnc(this, chunk, i + 1, len);
+              if (i === -1)
+                return cb(new Error('Malformed urlencoded form'));
+              if (i >= len)
+                return cb();
+              ++this._bytesKey;
+              i = skipKeyBytes(this, chunk, i, len);
+              continue;
+          }
+          ++i;
+          ++this._bytesKey;
+          i = skipKeyBytes(this, chunk, i, len);
+        }
+        if (this._lastPos < i)
+          this._key += chunk.latin1Slice(this._lastPos, i);
+      } else {
+        // Parsing value
+
+        i = skipValBytes(this, chunk, i, len);
+
+        while (i < len) {
+          switch (chunk[i]) {
+            case 38: // '&'
+              if (this._lastPos < i)
+                this._val += chunk.latin1Slice(this._lastPos, i);
+              this._lastPos = ++i;
+              this._inKey = true;
+              this._val = this._decoder(this._val, this._encode);
+              this._encode = 0;
+              if (this._bytesKey > 0 || this._bytesVal > 0) {
+                this.emit(
+                  'field',
+                  this._key,
+                  this._val,
+                  { nameTruncated: this._keyTrunc,
+                    valueTruncated: this._valTrunc,
+                    encoding: this.charset,
+                    mimeType: 'text/plain' }
+                );
+              }
+              this._key = '';
+              this._val = '';
+              this._keyTrunc = false;
+              this._valTrunc = false;
+              this._bytesKey = 0;
+              this._bytesVal = 0;
+              if (++this._fields >= this.fieldsLimit) {
+                this.emit('fieldsLimit');
+                return cb();
+              }
+              continue main;
+            case 43: // '+'
+              if (this._lastPos < i)
+                this._val += chunk.latin1Slice(this._lastPos, i);
+              this._val += ' ';
+              this._lastPos = i + 1;
+              break;
+            case 37: // '%'
+              if (this._encode === 0)
+                this._encode = 1;
+              if (this._lastPos < i)
+                this._val += chunk.latin1Slice(this._lastPos, i);
+              this._lastPos = i + 1;
+              this._byte = -1;
+              i = readPctEnc(this, chunk, i + 1, len);
+              if (i === -1)
+                return cb(new Error('Malformed urlencoded form'));
+              if (i >= len)
+                return cb();
+              ++this._bytesVal;
+              i = skipValBytes(this, chunk, i, len);
+              continue;
+          }
+          ++i;
+          ++this._bytesVal;
+          i = skipValBytes(this, chunk, i, len);
+        }
+        if (this._lastPos < i)
+          this._val += chunk.latin1Slice(this._lastPos, i);
+      }
+    }
+
+    cb();
+  }
+
+  _final(cb) {
+    if (this._byte !== -2)
+      return cb(new Error('Malformed urlencoded form'));
+    if (!this._inKey || this._bytesKey > 0 || this._bytesVal > 0) {
+      if (this._inKey)
+        this._key = this._decoder(this._key, this._encode);
+      else
+        this._val = this._decoder(this._val, this._encode);
+      this.emit(
+        'field',
+        this._key,
+        this._val,
+        { nameTruncated: this._keyTrunc,
+          valueTruncated: this._valTrunc,
+          encoding: this.charset,
+          mimeType: 'text/plain' }
+      );
+    }
+    cb();
+  }
+}
+
+function readPctEnc(self, chunk, pos, len) {
+  if (pos >= len)
+    return len;
+
+  if (self._byte === -1) {
+    // We saw a '%' but no hex characters yet
+    const hexUpper = HEX_VALUES[chunk[pos++]];
+    if (hexUpper === -1)
+      return -1;
+
+    if (hexUpper >= 8)
+      self._encode = 2; // Indicate high bits detected
+
+    if (pos < len) {
+      // Both hex characters are in this chunk
+      const hexLower = HEX_VALUES[chunk[pos++]];
+      if (hexLower === -1)
+        return -1;
+
+      if (self._inKey)
+        self._key += String.fromCharCode((hexUpper << 4) + hexLower);
+      else
+        self._val += String.fromCharCode((hexUpper << 4) + hexLower);
+
+      self._byte = -2;
+      self._lastPos = pos;
+    } else {
+      // Only one hex character was available in this chunk
+      self._byte = hexUpper;
+    }
+  } else {
+    // We saw only one hex character so far
+    const hexLower = HEX_VALUES[chunk[pos++]];
+    if (hexLower === -1)
+      return -1;
+
+    if (self._inKey)
+      self._key += String.fromCharCode((self._byte << 4) + hexLower);
+    else
+      self._val += String.fromCharCode((self._byte << 4) + hexLower);
+
+    self._byte = -2;
+    self._lastPos = pos;
+  }
+
+  return pos;
+}
+
+function skipKeyBytes(self, chunk, pos, len) {
+  // Skip bytes if we've truncated
+  if (self._bytesKey > self.fieldNameSizeLimit) {
+    if (!self._keyTrunc) {
+      if (self._lastPos < pos)
+        self._key += chunk.latin1Slice(self._lastPos, pos - 1);
+    }
+    self._keyTrunc = true;
+    for (; pos < len; ++pos) {
+      const code = chunk[pos];
+      if (code === 61/* '=' */ || code === 38/* '&' */)
+        break;
+      ++self._bytesKey;
+    }
+    self._lastPos = pos;
+  }
+
+  return pos;
+}
+
+function skipValBytes(self, chunk, pos, len) {
+  // Skip bytes if we've truncated
+  if (self._bytesVal > self.fieldSizeLimit) {
+    if (!self._valTrunc) {
+      if (self._lastPos < pos)
+        self._val += chunk.latin1Slice(self._lastPos, pos - 1);
+    }
+    self._valTrunc = true;
+    for (; pos < len; ++pos) {
+      if (chunk[pos] === 38/* '&' */)
+        break;
+      ++self._bytesVal;
+    }
+    self._lastPos = pos;
+  }
+
+  return pos;
+}
+
+/* eslint-disable no-multi-spaces */
+const HEX_VALUES = [
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+   0,  1,  2,  3,  4,  5,  6,  7,  8,  9, -1, -1, -1, -1, -1, -1,
+  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+];
+/* eslint-enable no-multi-spaces */
+
+module.exports = URLEncoded;

+ 596 - 0
backend/node_modules/busboy/lib/utils.js

@@ -0,0 +1,596 @@
+'use strict';
+
+function parseContentType(str) {
+  if (str.length === 0)
+    return;
+
+  const params = Object.create(null);
+  let i = 0;
+
+  // Parse type
+  for (; i < str.length; ++i) {
+    const code = str.charCodeAt(i);
+    if (TOKEN[code] !== 1) {
+      if (code !== 47/* '/' */ || i === 0)
+        return;
+      break;
+    }
+  }
+  // Check for type without subtype
+  if (i === str.length)
+    return;
+
+  const type = str.slice(0, i).toLowerCase();
+
+  // Parse subtype
+  const subtypeStart = ++i;
+  for (; i < str.length; ++i) {
+    const code = str.charCodeAt(i);
+    if (TOKEN[code] !== 1) {
+      // Make sure we have a subtype
+      if (i === subtypeStart)
+        return;
+
+      if (parseContentTypeParams(str, i, params) === undefined)
+        return;
+      break;
+    }
+  }
+  // Make sure we have a subtype
+  if (i === subtypeStart)
+    return;
+
+  const subtype = str.slice(subtypeStart, i).toLowerCase();
+
+  return { type, subtype, params };
+}
+
+function parseContentTypeParams(str, i, params) {
+  while (i < str.length) {
+    // Consume whitespace
+    for (; i < str.length; ++i) {
+      const code = str.charCodeAt(i);
+      if (code !== 32/* ' ' */ && code !== 9/* '\t' */)
+        break;
+    }
+
+    // Ended on whitespace
+    if (i === str.length)
+      break;
+
+    // Check for malformed parameter
+    if (str.charCodeAt(i++) !== 59/* ';' */)
+      return;
+
+    // Consume whitespace
+    for (; i < str.length; ++i) {
+      const code = str.charCodeAt(i);
+      if (code !== 32/* ' ' */ && code !== 9/* '\t' */)
+        break;
+    }
+
+    // Ended on whitespace (malformed)
+    if (i === str.length)
+      return;
+
+    let name;
+    const nameStart = i;
+    // Parse parameter name
+    for (; i < str.length; ++i) {
+      const code = str.charCodeAt(i);
+      if (TOKEN[code] !== 1) {
+        if (code !== 61/* '=' */)
+          return;
+        break;
+      }
+    }
+
+    // No value (malformed)
+    if (i === str.length)
+      return;
+
+    name = str.slice(nameStart, i);
+    ++i; // Skip over '='
+
+    // No value (malformed)
+    if (i === str.length)
+      return;
+
+    let value = '';
+    let valueStart;
+    if (str.charCodeAt(i) === 34/* '"' */) {
+      valueStart = ++i;
+      let escaping = false;
+      // Parse quoted value
+      for (; i < str.length; ++i) {
+        const code = str.charCodeAt(i);
+        if (code === 92/* '\\' */) {
+          if (escaping) {
+            valueStart = i;
+            escaping = false;
+          } else {
+            value += str.slice(valueStart, i);
+            escaping = true;
+          }
+          continue;
+        }
+        if (code === 34/* '"' */) {
+          if (escaping) {
+            valueStart = i;
+            escaping = false;
+            continue;
+          }
+          value += str.slice(valueStart, i);
+          break;
+        }
+        if (escaping) {
+          valueStart = i - 1;
+          escaping = false;
+        }
+        // Invalid unescaped quoted character (malformed)
+        if (QDTEXT[code] !== 1)
+          return;
+      }
+
+      // No end quote (malformed)
+      if (i === str.length)
+        return;
+
+      ++i; // Skip over double quote
+    } else {
+      valueStart = i;
+      // Parse unquoted value
+      for (; i < str.length; ++i) {
+        const code = str.charCodeAt(i);
+        if (TOKEN[code] !== 1) {
+          // No value (malformed)
+          if (i === valueStart)
+            return;
+          break;
+        }
+      }
+      value = str.slice(valueStart, i);
+    }
+
+    name = name.toLowerCase();
+    if (params[name] === undefined)
+      params[name] = value;
+  }
+
+  return params;
+}
+
+function parseDisposition(str, defDecoder) {
+  if (str.length === 0)
+    return;
+
+  const params = Object.create(null);
+  let i = 0;
+
+  for (; i < str.length; ++i) {
+    const code = str.charCodeAt(i);
+    if (TOKEN[code] !== 1) {
+      if (parseDispositionParams(str, i, params, defDecoder) === undefined)
+        return;
+      break;
+    }
+  }
+
+  const type = str.slice(0, i).toLowerCase();
+
+  return { type, params };
+}
+
+function parseDispositionParams(str, i, params, defDecoder) {
+  while (i < str.length) {
+    // Consume whitespace
+    for (; i < str.length; ++i) {
+      const code = str.charCodeAt(i);
+      if (code !== 32/* ' ' */ && code !== 9/* '\t' */)
+        break;
+    }
+
+    // Ended on whitespace
+    if (i === str.length)
+      break;
+
+    // Check for malformed parameter
+    if (str.charCodeAt(i++) !== 59/* ';' */)
+      return;
+
+    // Consume whitespace
+    for (; i < str.length; ++i) {
+      const code = str.charCodeAt(i);
+      if (code !== 32/* ' ' */ && code !== 9/* '\t' */)
+        break;
+    }
+
+    // Ended on whitespace (malformed)
+    if (i === str.length)
+      return;
+
+    let name;
+    const nameStart = i;
+    // Parse parameter name
+    for (; i < str.length; ++i) {
+      const code = str.charCodeAt(i);
+      if (TOKEN[code] !== 1) {
+        if (code === 61/* '=' */)
+          break;
+        return;
+      }
+    }
+
+    // No value (malformed)
+    if (i === str.length)
+      return;
+
+    let value = '';
+    let valueStart;
+    let charset;
+    //~ let lang;
+    name = str.slice(nameStart, i);
+    if (name.charCodeAt(name.length - 1) === 42/* '*' */) {
+      // Extended value
+
+      const charsetStart = ++i;
+      // Parse charset name
+      for (; i < str.length; ++i) {
+        const code = str.charCodeAt(i);
+        if (CHARSET[code] !== 1) {
+          if (code !== 39/* '\'' */)
+            return;
+          break;
+        }
+      }
+
+      // Incomplete charset (malformed)
+      if (i === str.length)
+        return;
+
+      charset = str.slice(charsetStart, i);
+      ++i; // Skip over the '\''
+
+      //~ const langStart = ++i;
+      // Parse language name
+      for (; i < str.length; ++i) {
+        const code = str.charCodeAt(i);
+        if (code === 39/* '\'' */)
+          break;
+      }
+
+      // Incomplete language (malformed)
+      if (i === str.length)
+        return;
+
+      //~ lang = str.slice(langStart, i);
+      ++i; // Skip over the '\''
+
+      // No value (malformed)
+      if (i === str.length)
+        return;
+
+      valueStart = i;
+
+      let encode = 0;
+      // Parse value
+      for (; i < str.length; ++i) {
+        const code = str.charCodeAt(i);
+        if (EXTENDED_VALUE[code] !== 1) {
+          if (code === 37/* '%' */) {
+            let hexUpper;
+            let hexLower;
+            if (i + 2 < str.length
+                && (hexUpper = HEX_VALUES[str.charCodeAt(i + 1)]) !== -1
+                && (hexLower = HEX_VALUES[str.charCodeAt(i + 2)]) !== -1) {
+              const byteVal = (hexUpper << 4) + hexLower;
+              value += str.slice(valueStart, i);
+              value += String.fromCharCode(byteVal);
+              i += 2;
+              valueStart = i + 1;
+              if (byteVal >= 128)
+                encode = 2;
+              else if (encode === 0)
+                encode = 1;
+              continue;
+            }
+            // '%' disallowed in non-percent encoded contexts (malformed)
+            return;
+          }
+          break;
+        }
+      }
+
+      value += str.slice(valueStart, i);
+      value = convertToUTF8(value, charset, encode);
+      if (value === undefined)
+        return;
+    } else {
+      // Non-extended value
+
+      ++i; // Skip over '='
+
+      // No value (malformed)
+      if (i === str.length)
+        return;
+
+      if (str.charCodeAt(i) === 34/* '"' */) {
+        valueStart = ++i;
+        let escaping = false;
+        // Parse quoted value
+        for (; i < str.length; ++i) {
+          const code = str.charCodeAt(i);
+          if (code === 92/* '\\' */) {
+            if (escaping) {
+              valueStart = i;
+              escaping = false;
+            } else {
+              value += str.slice(valueStart, i);
+              escaping = true;
+            }
+            continue;
+          }
+          if (code === 34/* '"' */) {
+            if (escaping) {
+              valueStart = i;
+              escaping = false;
+              continue;
+            }
+            value += str.slice(valueStart, i);
+            break;
+          }
+          if (escaping) {
+            valueStart = i - 1;
+            escaping = false;
+          }
+          // Invalid unescaped quoted character (malformed)
+          if (QDTEXT[code] !== 1)
+            return;
+        }
+
+        // No end quote (malformed)
+        if (i === str.length)
+          return;
+
+        ++i; // Skip over double quote
+      } else {
+        valueStart = i;
+        // Parse unquoted value
+        for (; i < str.length; ++i) {
+          const code = str.charCodeAt(i);
+          if (TOKEN[code] !== 1) {
+            // No value (malformed)
+            if (i === valueStart)
+              return;
+            break;
+          }
+        }
+        value = str.slice(valueStart, i);
+      }
+
+      value = defDecoder(value, 2);
+      if (value === undefined)
+        return;
+    }
+
+    name = name.toLowerCase();
+    if (params[name] === undefined)
+      params[name] = value;
+  }
+
+  return params;
+}
+
+function getDecoder(charset) {
+  let lc;
+  while (true) {
+    switch (charset) {
+      case 'utf-8':
+      case 'utf8':
+        return decoders.utf8;
+      case 'latin1':
+      case 'ascii': // TODO: Make these a separate, strict decoder?
+      case 'us-ascii':
+      case 'iso-8859-1':
+      case 'iso8859-1':
+      case 'iso88591':
+      case 'iso_8859-1':
+      case 'windows-1252':
+      case 'iso_8859-1:1987':
+      case 'cp1252':
+      case 'x-cp1252':
+        return decoders.latin1;
+      case 'utf16le':
+      case 'utf-16le':
+      case 'ucs2':
+      case 'ucs-2':
+        return decoders.utf16le;
+      case 'base64':
+        return decoders.base64;
+      default:
+        if (lc === undefined) {
+          lc = true;
+          charset = charset.toLowerCase();
+          continue;
+        }
+        return decoders.other.bind(charset);
+    }
+  }
+}
+
+const decoders = {
+  utf8: (data, hint) => {
+    if (data.length === 0)
+      return '';
+    if (typeof data === 'string') {
+      // If `data` never had any percent-encoded bytes or never had any that
+      // were outside of the ASCII range, then we can safely just return the
+      // input since UTF-8 is ASCII compatible
+      if (hint < 2)
+        return data;
+
+      data = Buffer.from(data, 'latin1');
+    }
+    return data.utf8Slice(0, data.length);
+  },
+
+  latin1: (data, hint) => {
+    if (data.length === 0)
+      return '';
+    if (typeof data === 'string')
+      return data;
+    return data.latin1Slice(0, data.length);
+  },
+
+  utf16le: (data, hint) => {
+    if (data.length === 0)
+      return '';
+    if (typeof data === 'string')
+      data = Buffer.from(data, 'latin1');
+    return data.ucs2Slice(0, data.length);
+  },
+
+  base64: (data, hint) => {
+    if (data.length === 0)
+      return '';
+    if (typeof data === 'string')
+      data = Buffer.from(data, 'latin1');
+    return data.base64Slice(0, data.length);
+  },
+
+  other: (data, hint) => {
+    if (data.length === 0)
+      return '';
+    if (typeof data === 'string')
+      data = Buffer.from(data, 'latin1');
+    try {
+      const decoder = new TextDecoder(this);
+      return decoder.decode(data);
+    } catch {}
+  },
+};
+
+function convertToUTF8(data, charset, hint) {
+  const decode = getDecoder(charset);
+  if (decode)
+    return decode(data, hint);
+}
+
+function basename(path) {
+  if (typeof path !== 'string')
+    return '';
+  for (let i = path.length - 1; i >= 0; --i) {
+    switch (path.charCodeAt(i)) {
+      case 0x2F: // '/'
+      case 0x5C: // '\'
+        path = path.slice(i + 1);
+        return (path === '..' || path === '.' ? '' : path);
+    }
+  }
+  return (path === '..' || path === '.' ? '' : path);
+}
+
+const TOKEN = [
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+];
+
+const QDTEXT = [
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+];
+
+const CHARSET = [
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+];
+
+const EXTENDED_VALUE = [
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
+  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+];
+
+/* eslint-disable no-multi-spaces */
+const HEX_VALUES = [
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+   0,  1,  2,  3,  4,  5,  6,  7,  8,  9, -1, -1, -1, -1, -1, -1,
+  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+];
+/* eslint-enable no-multi-spaces */
+
+module.exports = {
+  basename,
+  convertToUTF8,
+  getDecoder,
+  parseContentType,
+  parseDisposition,
+};

+ 109 - 0
backend/node_modules/busboy/test/common.js

@@ -0,0 +1,109 @@
+'use strict';
+
+const assert = require('assert');
+const { inspect } = require('util');
+
+const mustCallChecks = [];
+
+function noop() {}
+
+function runCallChecks(exitCode) {
+  if (exitCode !== 0) return;
+
+  const failed = mustCallChecks.filter((context) => {
+    if ('minimum' in context) {
+      context.messageSegment = `at least ${context.minimum}`;
+      return context.actual < context.minimum;
+    }
+    context.messageSegment = `exactly ${context.exact}`;
+    return context.actual !== context.exact;
+  });
+
+  failed.forEach((context) => {
+    console.error('Mismatched %s function calls. Expected %s, actual %d.',
+                  context.name,
+                  context.messageSegment,
+                  context.actual);
+    console.error(context.stack.split('\n').slice(2).join('\n'));
+  });
+
+  if (failed.length)
+    process.exit(1);
+}
+
+function mustCall(fn, exact) {
+  return _mustCallInner(fn, exact, 'exact');
+}
+
+function mustCallAtLeast(fn, minimum) {
+  return _mustCallInner(fn, minimum, 'minimum');
+}
+
+function _mustCallInner(fn, criteria = 1, field) {
+  if (process._exiting)
+    throw new Error('Cannot use common.mustCall*() in process exit handler');
+
+  if (typeof fn === 'number') {
+    criteria = fn;
+    fn = noop;
+  } else if (fn === undefined) {
+    fn = noop;
+  }
+
+  if (typeof criteria !== 'number')
+    throw new TypeError(`Invalid ${field} value: ${criteria}`);
+
+  const context = {
+    [field]: criteria,
+    actual: 0,
+    stack: inspect(new Error()),
+    name: fn.name || '<anonymous>'
+  };
+
+  // Add the exit listener only once to avoid listener leak warnings
+  if (mustCallChecks.length === 0)
+    process.on('exit', runCallChecks);
+
+  mustCallChecks.push(context);
+
+  function wrapped(...args) {
+    ++context.actual;
+    return fn.call(this, ...args);
+  }
+  // TODO: remove origFn?
+  wrapped.origFn = fn;
+
+  return wrapped;
+}
+
+function getCallSite(top) {
+  const originalStackFormatter = Error.prepareStackTrace;
+  Error.prepareStackTrace = (err, stack) =>
+    `${stack[0].getFileName()}:${stack[0].getLineNumber()}`;
+  const err = new Error();
+  Error.captureStackTrace(err, top);
+  // With the V8 Error API, the stack is not formatted until it is accessed
+  // eslint-disable-next-line no-unused-expressions
+  err.stack;
+  Error.prepareStackTrace = originalStackFormatter;
+  return err.stack;
+}
+
+function mustNotCall(msg) {
+  const callSite = getCallSite(mustNotCall);
+  return function mustNotCall(...args) {
+    args = args.map(inspect).join(', ');
+    const argsInfo = (args.length > 0
+                      ? `\ncalled with arguments: ${args}`
+                      : '');
+    assert.fail(
+      `${msg || 'function should not have been called'} at ${callSite}`
+        + argsInfo);
+  };
+}
+
+module.exports = {
+  mustCall,
+  mustCallAtLeast,
+  mustNotCall,
+};

+ 94 - 0
backend/node_modules/busboy/test/test-types-multipart-charsets.js

@@ -0,0 +1,94 @@
+'use strict';
+
+const assert = require('assert');
+const { inspect } = require('util');
+
+const { mustCall } = require(`${__dirname}/common.js`);
+
+const busboy = require('..');
+
+const input = Buffer.from([
+  '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
+ 'Content-Disposition: form-data; '
+   + 'name="upload_file_0"; filename="テスト.dat"',
+ 'Content-Type: application/octet-stream',
+ '',
+ 'A'.repeat(1023),
+ '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
+].join('\r\n'));
+const boundary = '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k';
+const expected = [
+  { type: 'file',
+    name: 'upload_file_0',
+    data: Buffer.from('A'.repeat(1023)),
+    info: {
+      filename: 'テスト.dat',
+      encoding: '7bit',
+      mimeType: 'application/octet-stream',
+    },
+    limited: false,
+  },
+];
+const bb = busboy({
+  defParamCharset: 'utf8',
+  headers: {
+    'content-type': `multipart/form-data; boundary=${boundary}`,
+  }
+});
+const results = [];
+
+bb.on('field', (name, val, info) => {
+  results.push({ type: 'field', name, val, info });
+});
+
+bb.on('file', (name, stream, info) => {
+  const data = [];
+  let nb = 0;
+  const file = {
+    type: 'file',
+    name,
+    data: null,
+    info,
+    limited: false,
+  };
+  results.push(file);
+  stream.on('data', (d) => {
+    data.push(d);
+    nb += d.length;
+  }).on('limit', () => {
+    file.limited = true;
+  }).on('close', () => {
+    file.data = Buffer.concat(data, nb);
+    assert.strictEqual(stream.truncated, file.limited);
+  }).once('error', (err) => {
+    file.err = err.message;
+  });
+});
+
+bb.on('error', (err) => {
+  results.push({ error: err.message });
+});
+
+bb.on('partsLimit', () => {
+  results.push('partsLimit');
+});
+
+bb.on('filesLimit', () => {
+  results.push('filesLimit');
+});
+
+bb.on('fieldsLimit', () => {
+  results.push('fieldsLimit');
+});
+
+bb.on('close', mustCall(() => {
+  assert.deepStrictEqual(
+    results,
+    expected,
+    'Results mismatch.\n'
+      + `Parsed: ${inspect(results)}\n`
+      + `Expected: ${inspect(expected)}`
+  );
+}));
+
+bb.end(input);

+ 102 - 0
backend/node_modules/busboy/test/test-types-multipart-stream-pause.js

@@ -0,0 +1,102 @@
+'use strict';
+
+const assert = require('assert');
+const { randomFillSync } = require('crypto');
+const { inspect } = require('util');
+
+const busboy = require('..');
+
+const { mustCall } = require('./common.js');
+
+const BOUNDARY = 'u2KxIV5yF1y+xUspOQCCZopaVgeV6Jxihv35XQJmuTx8X3sh';
+
+function formDataSection(key, value) {
+  return Buffer.from(
+    `\r\n--${BOUNDARY}`
+      + `\r\nContent-Disposition: form-data; name="${key}"`
+      + `\r\n\r\n${value}`
+  );
+}
+
+function formDataFile(key, filename, contentType) {
+  const buf = Buffer.allocUnsafe(100000);
+  return Buffer.concat([
+    Buffer.from(`\r\n--${BOUNDARY}\r\n`),
+    Buffer.from(`Content-Disposition: form-data; name="${key}"`
+                  + `; filename="${filename}"\r\n`),
+    Buffer.from(`Content-Type: ${contentType}\r\n\r\n`),
+    randomFillSync(buf)
+  ]);
+}
+
+const reqChunks = [
+  Buffer.concat([
+    formDataFile('file', 'file.bin', 'application/octet-stream'),
+    formDataSection('foo', 'foo value'),
+  ]),
+  formDataSection('bar', 'bar value'),
+  Buffer.from(`\r\n--${BOUNDARY}--\r\n`)
+];
+const bb = busboy({
+  headers: {
+    'content-type': `multipart/form-data; boundary=${BOUNDARY}`
+  }
+});
+const expected = [
+  { type: 'file',
+    name: 'file',
+    info: {
+      filename: 'file.bin',
+      encoding: '7bit',
+      mimeType: 'application/octet-stream',
+    },
+  },
+  { type: 'field',
+    name: 'foo',
+    val: 'foo value',
+    info: {
+      nameTruncated: false,
+      valueTruncated: false,
+      encoding: '7bit',
+      mimeType: 'text/plain',
+    },
+  },
+  { type: 'field',
+    name: 'bar',
+    val: 'bar value',
+    info: {
+      nameTruncated: false,
+      valueTruncated: false,
+      encoding: '7bit',
+      mimeType: 'text/plain',
+    },
+  },
+];
+const results = [];
+
+bb.on('field', (name, val, info) => {
+  results.push({ type: 'field', name, val, info });
+});
+
+bb.on('file', (name, stream, info) => {
+  results.push({ type: 'file', name, info });
+  // Simulate a pipe where the destination is pausing (perhaps due to waiting
+  // for file system write to finish)
+  setTimeout(() => {
+    stream.resume();
+  }, 10);
+});
+
+bb.on('close', mustCall(() => {
+  assert.deepStrictEqual(
+    results,
+    expected,
+    'Results mismatch.\n'
+      + `Parsed: ${inspect(results)}\n`
+      + `Expected: ${inspect(expected)}`
+  );
+}));
+
+for (const chunk of reqChunks)
+  bb.write(chunk);
+bb.end();

File diff suppressed because it is too large
+ 1053 - 0
backend/node_modules/busboy/test/test-types-multipart.js


+ 488 - 0
backend/node_modules/busboy/test/test-types-urlencoded.js

@@ -0,0 +1,488 @@
+'use strict';
+
+const assert = require('assert');
+const { transcode } = require('buffer');
+const { inspect } = require('util');
+
+const busboy = require('..');
+
+const active = new Map();
+
+const tests = [
+  { source: ['foo'],
+    expected: [
+      ['foo',
+       '',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Unassigned value'
+  },
+  { source: ['foo=bar'],
+    expected: [
+      ['foo',
+       'bar',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Assigned value'
+  },
+  { source: ['foo&bar=baz'],
+    expected: [
+      ['foo',
+       '',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['bar',
+       'baz',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Unassigned and assigned value'
+  },
+  { source: ['foo=bar&baz'],
+    expected: [
+      ['foo',
+       'bar',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['baz',
+       '',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Assigned and unassigned value'
+  },
+  { source: ['foo=bar&baz=bla'],
+    expected: [
+      ['foo',
+       'bar',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['baz',
+       'bla',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Two assigned values'
+  },
+  { source: ['foo&bar'],
+    expected: [
+      ['foo',
+       '',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['bar',
+       '',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Two unassigned values'
+  },
+  { source: ['foo&bar&'],
+    expected: [
+      ['foo',
+       '',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['bar',
+       '',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Two unassigned values and ampersand'
+  },
+  { source: ['foo+1=bar+baz%2Bquux'],
+    expected: [
+      ['foo 1',
+       'bar baz+quux',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Assigned key and value with (plus) space'
+  },
+  { source: ['foo=bar%20baz%21'],
+    expected: [
+      ['foo',
+       'bar baz!',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Assigned value with encoded bytes'
+  },
+  { source: ['foo%20bar=baz%20bla%21'],
+    expected: [
+      ['foo bar',
+       'baz bla!',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Assigned value with encoded bytes #2'
+  },
+  { source: ['foo=bar%20baz%21&num=1000'],
+    expected: [
+      ['foo',
+       'bar baz!',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['num',
+       '1000',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Two assigned values, one with encoded bytes'
+  },
+  { source: [
+      Array.from(transcode(Buffer.from('foo'), 'utf8', 'utf16le')).map(
+        (n) => `%${n.toString(16).padStart(2, '0')}`
+      ).join(''),
+      '=',
+      Array.from(transcode(Buffer.from('😀!'), 'utf8', 'utf16le')).map(
+        (n) => `%${n.toString(16).padStart(2, '0')}`
+      ).join(''),
+    ],
+    expected: [
+      ['foo',
+       '😀!',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'UTF-16LE',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    charset: 'UTF-16LE',
+    what: 'Encoded value with multi-byte charset'
+  },
+  { source: [
+      'foo=<',
+      Array.from(transcode(Buffer.from('©:^þ'), 'utf8', 'latin1')).map(
+        (n) => `%${n.toString(16).padStart(2, '0')}`
+      ).join(''),
+    ],
+    expected: [
+      ['foo',
+       '<©:^þ',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'ISO-8859-1',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    charset: 'ISO-8859-1',
+    what: 'Encoded value with single-byte, ASCII-compatible, non-UTF8 charset'
+  },
+  { source: ['foo=bar&baz=bla'],
+    expected: [],
+    what: 'Limits: zero fields',
+    limits: { fields: 0 }
+  },
+  { source: ['foo=bar&baz=bla'],
+    expected: [
+      ['foo',
+       'bar',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Limits: one field',
+    limits: { fields: 1 }
+  },
+  { source: ['foo=bar&baz=bla'],
+    expected: [
+      ['foo',
+       'bar',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['baz',
+       'bla',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Limits: field part lengths match limits',
+    limits: { fieldNameSize: 3, fieldSize: 3 }
+  },
+  { source: ['foo=bar&baz=bla'],
+    expected: [
+      ['fo',
+       'bar',
+       { nameTruncated: true,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['ba',
+       'bla',
+       { nameTruncated: true,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Limits: truncated field name',
+    limits: { fieldNameSize: 2 }
+  },
+  { source: ['foo=bar&baz=bla'],
+    expected: [
+      ['foo',
+       'ba',
+       { nameTruncated: false,
+         valueTruncated: true,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['baz',
+       'bl',
+       { nameTruncated: false,
+         valueTruncated: true,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Limits: truncated field value',
+    limits: { fieldSize: 2 }
+  },
+  { source: ['foo=bar&baz=bla'],
+    expected: [
+      ['fo',
+       'ba',
+       { nameTruncated: true,
+         valueTruncated: true,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['ba',
+       'bl',
+       { nameTruncated: true,
+         valueTruncated: true,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Limits: truncated field name and value',
+    limits: { fieldNameSize: 2, fieldSize: 2 }
+  },
+  { source: ['foo=bar&baz=bla'],
+    expected: [
+      ['fo',
+       '',
+       { nameTruncated: true,
+         valueTruncated: true,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['ba',
+       '',
+       { nameTruncated: true,
+         valueTruncated: true,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Limits: truncated field name and zero value limit',
+    limits: { fieldNameSize: 2, fieldSize: 0 }
+  },
+  { source: ['foo=bar&baz=bla'],
+    expected: [
+      ['',
+       '',
+       { nameTruncated: true,
+         valueTruncated: true,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+      ['',
+       '',
+       { nameTruncated: true,
+         valueTruncated: true,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Limits: truncated zero field name and zero value limit',
+    limits: { fieldNameSize: 0, fieldSize: 0 }
+  },
+  { source: ['&'],
+    expected: [],
+    what: 'Ampersand'
+  },
+  { source: ['&&&&&'],
+    expected: [],
+    what: 'Many ampersands'
+  },
+  { source: ['='],
+    expected: [
+      ['',
+       '',
+       { nameTruncated: false,
+         valueTruncated: false,
+         encoding: 'utf-8',
+         mimeType: 'text/plain' },
+      ],
+    ],
+    what: 'Assigned value, empty name and value'
+  },
+  { source: [''],
+    expected: [],
+    what: 'Nothing'
+  },
+];
+
+for (const test of tests) {
+  active.set(test, 1);
+
+  const { what } = test;
+  const charset = test.charset || 'utf-8';
+  const bb = busboy({
+    limits: test.limits,
+    headers: {
+      'content-type': `application/x-www-form-urlencoded; charset=${charset}`,
+    },
+  });
+  const results = [];
+
+  bb.on('field', (key, val, info) => {
+    results.push([key, val, info]);
+  });
+
+  bb.on('file', () => {
+    throw new Error(`[${what}] Unexpected file`);
+  });
+
+  bb.on('close', () => {
+    active.delete(test);
+
+    assert.deepStrictEqual(
+      results,
+      test.expected,
+      `[${what}] Results mismatch.\n`
+        + `Parsed: ${inspect(results)}\n`
+        + `Expected: ${inspect(test.expected)}`
+    );
+  });
+
+  for (const src of test.source) {
+    const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src);
+    bb.write(buf);
+  }
+  bb.end();
+}
+
+// Byte-by-byte versions
+for (let test of tests) {
+  test = { ...test };
+  test.what += ' (byte-by-byte)';
+  active.set(test, 1);
+
+  const { what } = test;
+  const charset = test.charset || 'utf-8';
+  const bb = busboy({
+    limits: test.limits,
+    headers: {
+      'content-type': `application/x-www-form-urlencoded; charset="${charset}"`,
+    },
+  });
+  const results = [];
+
+  bb.on('field', (key, val, info) => {
+    results.push([key, val, info]);
+  });
+
+  bb.on('file', () => {
+    throw new Error(`[${what}] Unexpected file`);
+  });
+
+  bb.on('close', () => {
+    active.delete(test);
+
+    assert.deepStrictEqual(
+      results,
+      test.expected,
+      `[${what}] Results mismatch.\n`
+        + `Parsed: ${inspect(results)}\n`
+        + `Expected: ${inspect(test.expected)}`
+    );
+  });
+
+  for (const src of test.source) {
+    const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src);
+    for (let i = 0; i < buf.length; ++i)
+      bb.write(buf.slice(i, i + 1));
+  }
+  bb.end();
+}
+
+{
+  let exception = false;
+  process.once('uncaughtException', (ex) => {
+    exception = true;
+    throw ex;
+  });
+  process.on('exit', () => {
+    if (exception || active.size === 0)
+      return;
+    process.exitCode = 1;
+    console.error('==========================');
+    console.error(`${active.size} test(s) did not finish:`);
+    console.error('==========================');
+    console.error(Array.from(active.keys()).map((v) => v.what).join('\n'));
+  });
+}

+ 20 - 0
backend/node_modules/busboy/test/test.js

@@ -0,0 +1,20 @@
+'use strict';
+
+const { spawnSync } = require('child_process');
+const { readdirSync } = require('fs');
+const { join } = require('path');
+
+const files = readdirSync(__dirname).sort();
+for (const filename of files) {
+  if (filename.startsWith('test-')) {
+    const path = join(__dirname, filename);
+    console.log(`> Running ${filename} ...`);
+    const result = spawnSync(`${process.argv0} ${path}`, {
+      shell: true,
+      stdio: 'inherit',
+      windowsHide: true
+    });
+    if (result.status !== 0)
+      process.exitCode = 1;
+  }
+}

+ 4 - 0
backend/node_modules/bytes/index.js

@@ -162,5 +162,9 @@ function parse(val) {
     unit = results[4].toLowerCase();
   }
 
+  if (isNaN(floatValue)) {
+    return null;
+  }
+
   return Math.floor(map[unit] * floatValue);
 }

+ 15 - 0
backend/node_modules/call-bind/callBound.js

@@ -0,0 +1,15 @@
+'use strict';
+
+var GetIntrinsic = require('get-intrinsic');
+
+var callBind = require('./');
+
+var $indexOf = callBind(GetIntrinsic('String.prototype.indexOf'));
+
+module.exports = function callBoundIntrinsic(name, allowMissing) {
+	var intrinsic = GetIntrinsic(name, !!allowMissing);
+	if (typeof intrinsic === 'function' && $indexOf(name, '.prototype.') > -1) {
+		return callBind(intrinsic);
+	}
+	return intrinsic;
+};

+ 47 - 0
backend/node_modules/call-bind/index.js

@@ -0,0 +1,47 @@
+'use strict';
+
+var bind = require('function-bind');
+var GetIntrinsic = require('get-intrinsic');
+
+var $apply = GetIntrinsic('%Function.prototype.apply%');
+var $call = GetIntrinsic('%Function.prototype.call%');
+var $reflectApply = GetIntrinsic('%Reflect.apply%', true) || bind.call($call, $apply);
+
+var $gOPD = GetIntrinsic('%Object.getOwnPropertyDescriptor%', true);
+var $defineProperty = GetIntrinsic('%Object.defineProperty%', true);
+var $max = GetIntrinsic('%Math.max%');
+
+if ($defineProperty) {
+	try {
+		$defineProperty({}, 'a', { value: 1 });
+	} catch (e) {
+		// IE 8 has a broken defineProperty
+		$defineProperty = null;
+	}
+}
+
+module.exports = function callBind(originalFunction) {
+	var func = $reflectApply(bind, $call, arguments);
+	if ($gOPD && $defineProperty) {
+		var desc = $gOPD(func, 'length');
+		if (desc.configurable) {
+			// original length, plus the receiver, minus any additional arguments (after the receiver)
+			$defineProperty(
+				func,
+				'length',
+				{ value: 1 + $max(0, originalFunction.length - (arguments.length - 1)) }
+			);
+		}
+	}
+	return func;
+};
+
+var applyBind = function applyBind() {
+	return $reflectApply(bind, $apply, arguments);
+};
+
+if ($defineProperty) {
+	$defineProperty(module.exports, 'apply', { value: applyBind });
+} else {
+	module.exports.apply = applyBind;
+}

+ 55 - 0
backend/node_modules/call-bind/test/callBound.js

@@ -0,0 +1,55 @@
+'use strict';
+
+var test = require('tape');
+
+var callBound = require('../callBound');
+
+test('callBound', function (t) {
+	// static primitive
+	t.equal(callBound('Array.length'), Array.length, 'Array.length yields itself');
+	t.equal(callBound('%Array.length%'), Array.length, '%Array.length% yields itself');
+
+	// static non-function object
+	t.equal(callBound('Array.prototype'), Array.prototype, 'Array.prototype yields itself');
+	t.equal(callBound('%Array.prototype%'), Array.prototype, '%Array.prototype% yields itself');
+	t.equal(callBound('Array.constructor'), Array.constructor, 'Array.constructor yields itself');
+	t.equal(callBound('%Array.constructor%'), Array.constructor, '%Array.constructor% yields itself');
+
+	// static function
+	t.equal(callBound('Date.parse'), Date.parse, 'Date.parse yields itself');
+	t.equal(callBound('%Date.parse%'), Date.parse, '%Date.parse% yields itself');
+
+	// prototype primitive
+	t.equal(callBound('Error.prototype.message'), Error.prototype.message, 'Error.prototype.message yields itself');
+	t.equal(callBound('%Error.prototype.message%'), Error.prototype.message, '%Error.prototype.message% yields itself');
+
+	// prototype function
+	t.notEqual(callBound('Object.prototype.toString'), Object.prototype.toString, 'Object.prototype.toString does not yield itself');
+	t.notEqual(callBound('%Object.prototype.toString%'), Object.prototype.toString, '%Object.prototype.toString% does not yield itself');
+	t.equal(callBound('Object.prototype.toString')(true), Object.prototype.toString.call(true), 'call-bound Object.prototype.toString calls into the original');
+	t.equal(callBound('%Object.prototype.toString%')(true), Object.prototype.toString.call(true), 'call-bound %Object.prototype.toString% calls into the original');
+
+	t['throws'](
+		function () { callBound('does not exist'); },
+		SyntaxError,
+		'nonexistent intrinsic throws'
+	);
+	t['throws'](
+		function () { callBound('does not exist', true); },
+		SyntaxError,
+		'allowMissing arg still throws for unknown intrinsic'
+	);
+
+	/* globals WeakRef: false */
+	t.test('real but absent intrinsic', { skip: typeof WeakRef !== 'undefined' }, function (st) {
+		st['throws'](
+			function () { callBound('WeakRef'); },
+			TypeError,
+			'real but absent intrinsic throws'
+		);
+		st.equal(callBound('WeakRef', true), undefined, 'allowMissing arg avoids exception');
+		st.end();
+	});
+
+	t.end();
+});

+ 66 - 0
backend/node_modules/call-bind/test/index.js

@@ -0,0 +1,66 @@
+'use strict';
+
+var callBind = require('../');
+var bind = require('function-bind');
+
+var test = require('tape');
+
+/*
+ * older engines have length nonconfigurable
+ * in io.js v3, it is configurable except on bound functions, hence the .bind()
+ */
+var functionsHaveConfigurableLengths = !!(
+	Object.getOwnPropertyDescriptor
+	&& Object.getOwnPropertyDescriptor(bind.call(function () {}), 'length').configurable
+);
+
+test('callBind', function (t) {
+	var sentinel = { sentinel: true };
+	var func = function (a, b) {
+		// eslint-disable-next-line no-invalid-this
+		return [this, a, b];
+	};
+	t.equal(func.length, 2, 'original function length is 2');
+	t.deepEqual(func(), [undefined, undefined, undefined], 'unbound func with too few args');
+	t.deepEqual(func(1, 2), [undefined, 1, 2], 'unbound func with right args');
+	t.deepEqual(func(1, 2, 3), [undefined, 1, 2], 'unbound func with too many args');
+
+	var bound = callBind(func);
+	t.equal(bound.length, func.length + 1, 'function length is preserved', { skip: !functionsHaveConfigurableLengths });
+	t.deepEqual(bound(), [undefined, undefined, undefined], 'bound func with too few args');
+	t.deepEqual(bound(1, 2), [1, 2, undefined], 'bound func with right args');
+	t.deepEqual(bound(1, 2, 3), [1, 2, 3], 'bound func with too many args');
+
+	var boundR = callBind(func, sentinel);
+	t.equal(boundR.length, func.length, 'function length is preserved', { skip: !functionsHaveConfigurableLengths });
+	t.deepEqual(boundR(), [sentinel, undefined, undefined], 'bound func with receiver, with too few args');
+	t.deepEqual(boundR(1, 2), [sentinel, 1, 2], 'bound func with receiver, with right args');
+	t.deepEqual(boundR(1, 2, 3), [sentinel, 1, 2], 'bound func with receiver, with too many args');
+
+	var boundArg = callBind(func, sentinel, 1);
+	t.equal(boundArg.length, func.length - 1, 'function length is preserved', { skip: !functionsHaveConfigurableLengths });
+	t.deepEqual(boundArg(), [sentinel, 1, undefined], 'bound func with receiver and arg, with too few args');
+	t.deepEqual(boundArg(2), [sentinel, 1, 2], 'bound func with receiver and arg, with right arg');
+	t.deepEqual(boundArg(2, 3), [sentinel, 1, 2], 'bound func with receiver and arg, with too many args');
+
+	t.test('callBind.apply', function (st) {
+		var aBound = callBind.apply(func);
+		st.deepEqual(aBound(sentinel), [sentinel, undefined, undefined], 'apply-bound func with no args');
+		st.deepEqual(aBound(sentinel, [1], 4), [sentinel, 1, undefined], 'apply-bound func with too few args');
+		st.deepEqual(aBound(sentinel, [1, 2], 4), [sentinel, 1, 2], 'apply-bound func with right args');
+
+		var aBoundArg = callBind.apply(func);
+		st.deepEqual(aBoundArg(sentinel, [1, 2, 3], 4), [sentinel, 1, 2], 'apply-bound func with too many args');
+		st.deepEqual(aBoundArg(sentinel, [1, 2], 4), [sentinel, 1, 2], 'apply-bound func with right args');
+		st.deepEqual(aBoundArg(sentinel, [1], 4), [sentinel, 1, undefined], 'apply-bound func with too few args');
+
+		var aBoundR = callBind.apply(func, sentinel);
+		st.deepEqual(aBoundR([1, 2, 3], 4), [sentinel, 1, 2], 'apply-bound func with receiver and too many args');
+		st.deepEqual(aBoundR([1, 2], 4), [sentinel, 1, 2], 'apply-bound func with receiver and right args');
+		st.deepEqual(aBoundR([1], 4), [sentinel, 1, undefined], 'apply-bound func with receiver and too few args');
+
+		st.end();
+	});
+
+	t.end();
+});

+ 144 - 0
backend/node_modules/concat-stream/index.js

@@ -0,0 +1,144 @@
+var Writable = require('readable-stream').Writable
+var inherits = require('inherits')
+var bufferFrom = require('buffer-from')
+
+if (typeof Uint8Array === 'undefined') {
+  var U8 = require('typedarray').Uint8Array
+} else {
+  var U8 = Uint8Array
+}
+
+function ConcatStream(opts, cb) {
+  if (!(this instanceof ConcatStream)) return new ConcatStream(opts, cb)
+
+  if (typeof opts === 'function') {
+    cb = opts
+    opts = {}
+  }
+  if (!opts) opts = {}
+
+  var encoding = opts.encoding
+  var shouldInferEncoding = false
+
+  if (!encoding) {
+    shouldInferEncoding = true
+  } else {
+    encoding =  String(encoding).toLowerCase()
+    if (encoding === 'u8' || encoding === 'uint8') {
+      encoding = 'uint8array'
+    }
+  }
+
+  Writable.call(this, { objectMode: true })
+
+  this.encoding = encoding
+  this.shouldInferEncoding = shouldInferEncoding
+
+  if (cb) this.on('finish', function () { cb(this.getBody()) })
+  this.body = []
+}
+
+module.exports = ConcatStream
+inherits(ConcatStream, Writable)
+
+ConcatStream.prototype._write = function(chunk, enc, next) {
+  this.body.push(chunk)
+  next()
+}
+
+ConcatStream.prototype.inferEncoding = function (buff) {
+  var firstBuffer = buff === undefined ? this.body[0] : buff;
+  if (Buffer.isBuffer(firstBuffer)) return 'buffer'
+  if (typeof Uint8Array !== 'undefined' && firstBuffer instanceof Uint8Array) return 'uint8array'
+  if (Array.isArray(firstBuffer)) return 'array'
+  if (typeof firstBuffer === 'string') return 'string'
+  if (Object.prototype.toString.call(firstBuffer) === "[object Object]") return 'object'
+  return 'buffer'
+}
+
+ConcatStream.prototype.getBody = function () {
+  if (!this.encoding && this.body.length === 0) return []
+  if (this.shouldInferEncoding) this.encoding = this.inferEncoding()
+  if (this.encoding === 'array') return arrayConcat(this.body)
+  if (this.encoding === 'string') return stringConcat(this.body)
+  if (this.encoding === 'buffer') return bufferConcat(this.body)
+  if (this.encoding === 'uint8array') return u8Concat(this.body)
+  return this.body
+}
+
+var isArray = Array.isArray || function (arr) {
+  return Object.prototype.toString.call(arr) == '[object Array]'
+}
+
+function isArrayish (arr) {
+  return /Array\]$/.test(Object.prototype.toString.call(arr))
+}
+
+function isBufferish (p) {
+  return typeof p === 'string' || isArrayish(p) || (p && typeof p.subarray === 'function')
+}
+
+function stringConcat (parts) {
+  var strings = []
+  var needsToString = false
+  for (var i = 0; i < parts.length; i++) {
+    var p = parts[i]
+    if (typeof p === 'string') {
+      strings.push(p)
+    } else if (Buffer.isBuffer(p)) {
+      strings.push(p)
+    } else if (isBufferish(p)) {
+      strings.push(bufferFrom(p))
+    } else {
+      strings.push(bufferFrom(String(p)))
+    }
+  }
+  if (Buffer.isBuffer(parts[0])) {
+    strings = Buffer.concat(strings)
+    strings = strings.toString('utf8')
+  } else {
+    strings = strings.join('')
+  }
+  return strings
+}
+
+function bufferConcat (parts) {
+  var bufs = []
+  for (var i = 0; i < parts.length; i++) {
+    var p = parts[i]
+    if (Buffer.isBuffer(p)) {
+      bufs.push(p)
+    } else if (isBufferish(p)) {
+      bufs.push(bufferFrom(p))
+    } else {
+      bufs.push(bufferFrom(String(p)))
+    }
+  }
+  return Buffer.concat(bufs)
+}
+
+function arrayConcat (parts) {
+  var res = []
+  for (var i = 0; i < parts.length; i++) {
+    res.push.apply(res, parts[i])
+  }
+  return res
+}
+
+function u8Concat (parts) {
+  var len = 0
+  for (var i = 0; i < parts.length; i++) {
+    if (typeof parts[i] === 'string') {
+      parts[i] = bufferFrom(parts[i])
+    }
+    len += parts[i].length
+  }
+  var u8 = new U8(len)
+  for (var i = 0, offset = 0; i < parts.length; i++) {
+    var part = parts[i]
+    for (var j = 0; j < part.length; j++) {
+      u8[offset++] = part[j]
+    }
+  }
+  return u8
+}

+ 1 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/duplex-browser.js

@@ -0,0 +1 @@
+module.exports = require('./lib/_stream_duplex.js');

+ 1 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/duplex.js

@@ -0,0 +1 @@
+module.exports = require('./readable').Duplex

+ 131 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_duplex.js

@@ -0,0 +1,131 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// a duplex stream is just a stream that is both readable and writable.
+// Since JS doesn't have multiple prototypal inheritance, this class
+// prototypally inherits from Readable, and then parasitically from
+// Writable.
+
+'use strict';
+
+/*<replacement>*/
+
+var pna = require('process-nextick-args');
+/*</replacement>*/
+
+/*<replacement>*/
+var objectKeys = Object.keys || function (obj) {
+  var keys = [];
+  for (var key in obj) {
+    keys.push(key);
+  }return keys;
+};
+/*</replacement>*/
+
+module.exports = Duplex;
+
+/*<replacement>*/
+var util = Object.create(require('core-util-is'));
+util.inherits = require('inherits');
+/*</replacement>*/
+
+var Readable = require('./_stream_readable');
+var Writable = require('./_stream_writable');
+
+util.inherits(Duplex, Readable);
+
+{
+  // avoid scope creep, the keys array can then be collected
+  var keys = objectKeys(Writable.prototype);
+  for (var v = 0; v < keys.length; v++) {
+    var method = keys[v];
+    if (!Duplex.prototype[method]) Duplex.prototype[method] = Writable.prototype[method];
+  }
+}
+
+function Duplex(options) {
+  if (!(this instanceof Duplex)) return new Duplex(options);
+
+  Readable.call(this, options);
+  Writable.call(this, options);
+
+  if (options && options.readable === false) this.readable = false;
+
+  if (options && options.writable === false) this.writable = false;
+
+  this.allowHalfOpen = true;
+  if (options && options.allowHalfOpen === false) this.allowHalfOpen = false;
+
+  this.once('end', onend);
+}
+
+Object.defineProperty(Duplex.prototype, 'writableHighWaterMark', {
+  // making it explicit this property is not enumerable
+  // because otherwise some prototype manipulation in
+  // userland will fail
+  enumerable: false,
+  get: function () {
+    return this._writableState.highWaterMark;
+  }
+});
+
+// the no-half-open enforcer
+function onend() {
+  // if we allow half-open state, or if the writable side ended,
+  // then we're ok.
+  if (this.allowHalfOpen || this._writableState.ended) return;
+
+  // no more data can be written.
+  // But allow more writes to happen in this tick.
+  pna.nextTick(onEndNT, this);
+}
+
+function onEndNT(self) {
+  self.end();
+}
+
+Object.defineProperty(Duplex.prototype, 'destroyed', {
+  get: function () {
+    if (this._readableState === undefined || this._writableState === undefined) {
+      return false;
+    }
+    return this._readableState.destroyed && this._writableState.destroyed;
+  },
+  set: function (value) {
+    // we ignore the value if the stream
+    // has not been initialized yet
+    if (this._readableState === undefined || this._writableState === undefined) {
+      return;
+    }
+
+    // backward compatibility, the user is explicitly
+    // managing destroyed
+    this._readableState.destroyed = value;
+    this._writableState.destroyed = value;
+  }
+});
+
+Duplex.prototype._destroy = function (err, cb) {
+  this.push(null);
+  this.end();
+
+  pna.nextTick(cb, err);
+};

+ 47 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_passthrough.js

@@ -0,0 +1,47 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// a passthrough stream.
+// basically just the most minimal sort of Transform stream.
+// Every written chunk gets output as-is.
+
+'use strict';
+
+module.exports = PassThrough;
+
+var Transform = require('./_stream_transform');
+
+/*<replacement>*/
+var util = Object.create(require('core-util-is'));
+util.inherits = require('inherits');
+/*</replacement>*/
+
+util.inherits(PassThrough, Transform);
+
+function PassThrough(options) {
+  if (!(this instanceof PassThrough)) return new PassThrough(options);
+
+  Transform.call(this, options);
+}
+
+PassThrough.prototype._transform = function (chunk, encoding, cb) {
+  cb(null, chunk);
+};

File diff suppressed because it is too large
+ 1019 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_readable.js


+ 214 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_transform.js

@@ -0,0 +1,214 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// a transform stream is a readable/writable stream where you do
+// something with the data.  Sometimes it's called a "filter",
+// but that's not a great name for it, since that implies a thing where
+// some bits pass through, and others are simply ignored.  (That would
+// be a valid example of a transform, of course.)
+//
+// While the output is causally related to the input, it's not a
+// necessarily symmetric or synchronous transformation.  For example,
+// a zlib stream might take multiple plain-text writes(), and then
+// emit a single compressed chunk some time in the future.
+//
+// Here's how this works:
+//
+// The Transform stream has all the aspects of the readable and writable
+// stream classes.  When you write(chunk), that calls _write(chunk,cb)
+// internally, and returns false if there's a lot of pending writes
+// buffered up.  When you call read(), that calls _read(n) until
+// there's enough pending readable data buffered up.
+//
+// In a transform stream, the written data is placed in a buffer.  When
+// _read(n) is called, it transforms the queued up data, calling the
+// buffered _write cb's as it consumes chunks.  If consuming a single
+// written chunk would result in multiple output chunks, then the first
+// outputted bit calls the readcb, and subsequent chunks just go into
+// the read buffer, and will cause it to emit 'readable' if necessary.
+//
+// This way, back-pressure is actually determined by the reading side,
+// since _read has to be called to start processing a new chunk.  However,
+// a pathological inflate type of transform can cause excessive buffering
+// here.  For example, imagine a stream where every byte of input is
+// interpreted as an integer from 0-255, and then results in that many
+// bytes of output.  Writing the 4 bytes {ff,ff,ff,ff} would result in
+// 1kb of data being output.  In this case, you could write a very small
+// amount of input, and end up with a very large amount of output.  In
+// such a pathological inflating mechanism, there'd be no way to tell
+// the system to stop doing the transform.  A single 4MB write could
+// cause the system to run out of memory.
+//
+// However, even in such a pathological case, only a single written chunk
+// would be consumed, and then the rest would wait (un-transformed) until
+// the results of the previous transformed chunk were consumed.
+
+'use strict';
+
+module.exports = Transform;
+
+var Duplex = require('./_stream_duplex');
+
+/*<replacement>*/
+var util = Object.create(require('core-util-is'));
+util.inherits = require('inherits');
+/*</replacement>*/
+
+util.inherits(Transform, Duplex);
+
+function afterTransform(er, data) {
+  var ts = this._transformState;
+  ts.transforming = false;
+
+  var cb = ts.writecb;
+
+  if (!cb) {
+    return this.emit('error', new Error('write callback called multiple times'));
+  }
+
+  ts.writechunk = null;
+  ts.writecb = null;
+
+  if (data != null) // single equals check for both `null` and `undefined`
+    this.push(data);
+
+  cb(er);
+
+  var rs = this._readableState;
+  rs.reading = false;
+  if (rs.needReadable || rs.length < rs.highWaterMark) {
+    this._read(rs.highWaterMark);
+  }
+}
+
+function Transform(options) {
+  if (!(this instanceof Transform)) return new Transform(options);
+
+  Duplex.call(this, options);
+
+  this._transformState = {
+    afterTransform: afterTransform.bind(this),
+    needTransform: false,
+    transforming: false,
+    writecb: null,
+    writechunk: null,
+    writeencoding: null
+  };
+
+  // start out asking for a readable event once data is transformed.
+  this._readableState.needReadable = true;
+
+  // we have implemented the _read method, and done the other things
+  // that Readable wants before the first _read call, so unset the
+  // sync guard flag.
+  this._readableState.sync = false;
+
+  if (options) {
+    if (typeof options.transform === 'function') this._transform = options.transform;
+
+    if (typeof options.flush === 'function') this._flush = options.flush;
+  }
+
+  // When the writable side finishes, then flush out anything remaining.
+  this.on('prefinish', prefinish);
+}
+
+function prefinish() {
+  var _this = this;
+
+  if (typeof this._flush === 'function') {
+    this._flush(function (er, data) {
+      done(_this, er, data);
+    });
+  } else {
+    done(this, null, null);
+  }
+}
+
+Transform.prototype.push = function (chunk, encoding) {
+  this._transformState.needTransform = false;
+  return Duplex.prototype.push.call(this, chunk, encoding);
+};
+
+// This is the part where you do stuff!
+// override this function in implementation classes.
+// 'chunk' is an input chunk.
+//
+// Call `push(newChunk)` to pass along transformed output
+// to the readable side.  You may call 'push' zero or more times.
+//
+// Call `cb(err)` when you are done with this chunk.  If you pass
+// an error, then that'll put the hurt on the whole operation.  If you
+// never call cb(), then you'll never get another chunk.
+Transform.prototype._transform = function (chunk, encoding, cb) {
+  throw new Error('_transform() is not implemented');
+};
+
+Transform.prototype._write = function (chunk, encoding, cb) {
+  var ts = this._transformState;
+  ts.writecb = cb;
+  ts.writechunk = chunk;
+  ts.writeencoding = encoding;
+  if (!ts.transforming) {
+    var rs = this._readableState;
+    if (ts.needTransform || rs.needReadable || rs.length < rs.highWaterMark) this._read(rs.highWaterMark);
+  }
+};
+
+// Doesn't matter what the args are here.
+// _transform does all the work.
+// That we got here means that the readable side wants more data.
+Transform.prototype._read = function (n) {
+  var ts = this._transformState;
+
+  if (ts.writechunk !== null && ts.writecb && !ts.transforming) {
+    ts.transforming = true;
+    this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform);
+  } else {
+    // mark that we need a transform, so that any data that comes in
+    // will get processed, now that we've asked for it.
+    ts.needTransform = true;
+  }
+};
+
+Transform.prototype._destroy = function (err, cb) {
+  var _this2 = this;
+
+  Duplex.prototype._destroy.call(this, err, function (err2) {
+    cb(err2);
+    _this2.emit('close');
+  });
+};
+
+function done(stream, er, data) {
+  if (er) return stream.emit('error', er);
+
+  if (data != null) // single equals check for both `null` and `undefined`
+    stream.push(data);
+
+  // if there's nothing in the write buffer, then that means
+  // that nothing more will ever be provided
+  if (stream._writableState.length) throw new Error('Calling transform done when ws.length != 0');
+
+  if (stream._transformState.transforming) throw new Error('Calling transform done when still transforming');
+
+  return stream.push(null);
+}

+ 687 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_writable.js

@@ -0,0 +1,687 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// A bit simpler than readable streams.
+// Implement an async ._write(chunk, encoding, cb), and it'll handle all
+// the drain event emission and buffering.
+
+'use strict';
+
+/*<replacement>*/
+
+var pna = require('process-nextick-args');
+/*</replacement>*/
+
+module.exports = Writable;
+
+/* <replacement> */
+function WriteReq(chunk, encoding, cb) {
+  this.chunk = chunk;
+  this.encoding = encoding;
+  this.callback = cb;
+  this.next = null;
+}
+
+// It seems a linked list but it is not
+// there will be only 2 of these for each stream
+function CorkedRequest(state) {
+  var _this = this;
+
+  this.next = null;
+  this.entry = null;
+  this.finish = function () {
+    onCorkedFinish(_this, state);
+  };
+}
+/* </replacement> */
+
+/*<replacement>*/
+var asyncWrite = !process.browser && ['v0.10', 'v0.9.'].indexOf(process.version.slice(0, 5)) > -1 ? setImmediate : pna.nextTick;
+/*</replacement>*/
+
+/*<replacement>*/
+var Duplex;
+/*</replacement>*/
+
+Writable.WritableState = WritableState;
+
+/*<replacement>*/
+var util = Object.create(require('core-util-is'));
+util.inherits = require('inherits');
+/*</replacement>*/
+
+/*<replacement>*/
+var internalUtil = {
+  deprecate: require('util-deprecate')
+};
+/*</replacement>*/
+
+/*<replacement>*/
+var Stream = require('./internal/streams/stream');
+/*</replacement>*/
+
+/*<replacement>*/
+
+var Buffer = require('safe-buffer').Buffer;
+var OurUint8Array = global.Uint8Array || function () {};
+function _uint8ArrayToBuffer(chunk) {
+  return Buffer.from(chunk);
+}
+function _isUint8Array(obj) {
+  return Buffer.isBuffer(obj) || obj instanceof OurUint8Array;
+}
+
+/*</replacement>*/
+
+var destroyImpl = require('./internal/streams/destroy');
+
+util.inherits(Writable, Stream);
+
+function nop() {}
+
+function WritableState(options, stream) {
+  Duplex = Duplex || require('./_stream_duplex');
+
+  options = options || {};
+
+  // Duplex streams are both readable and writable, but share
+  // the same options object.
+  // However, some cases require setting options to different
+  // values for the readable and the writable sides of the duplex stream.
+  // These options can be provided separately as readableXXX and writableXXX.
+  var isDuplex = stream instanceof Duplex;
+
+  // object stream flag to indicate whether or not this stream
+  // contains buffers or objects.
+  this.objectMode = !!options.objectMode;
+
+  if (isDuplex) this.objectMode = this.objectMode || !!options.writableObjectMode;
+
+  // the point at which write() starts returning false
+  // Note: 0 is a valid value, means that we always return false if
+  // the entire buffer is not flushed immediately on write()
+  var hwm = options.highWaterMark;
+  var writableHwm = options.writableHighWaterMark;
+  var defaultHwm = this.objectMode ? 16 : 16 * 1024;
+
+  if (hwm || hwm === 0) this.highWaterMark = hwm;else if (isDuplex && (writableHwm || writableHwm === 0)) this.highWaterMark = writableHwm;else this.highWaterMark = defaultHwm;
+
+  // cast to ints.
+  this.highWaterMark = Math.floor(this.highWaterMark);
+
+  // if _final has been called
+  this.finalCalled = false;
+
+  // drain event flag.
+  this.needDrain = false;
+  // at the start of calling end()
+  this.ending = false;
+  // when end() has been called, and returned
+  this.ended = false;
+  // when 'finish' is emitted
+  this.finished = false;
+
+  // has it been destroyed
+  this.destroyed = false;
+
+  // should we decode strings into buffers before passing to _write?
+  // this is here so that some node-core streams can optimize string
+  // handling at a lower level.
+  var noDecode = options.decodeStrings === false;
+  this.decodeStrings = !noDecode;
+
+  // Crypto is kind of old and crusty.  Historically, its default string
+  // encoding is 'binary' so we have to make this configurable.
+  // Everything else in the universe uses 'utf8', though.
+  this.defaultEncoding = options.defaultEncoding || 'utf8';
+
+  // not an actual buffer we keep track of, but a measurement
+  // of how much we're waiting to get pushed to some underlying
+  // socket or file.
+  this.length = 0;
+
+  // a flag to see when we're in the middle of a write.
+  this.writing = false;
+
+  // when true all writes will be buffered until .uncork() call
+  this.corked = 0;
+
+  // a flag to be able to tell if the onwrite cb is called immediately,
+  // or on a later tick.  We set this to true at first, because any
+  // actions that shouldn't happen until "later" should generally also
+  // not happen before the first write call.
+  this.sync = true;
+
+  // a flag to know if we're processing previously buffered items, which
+  // may call the _write() callback in the same tick, so that we don't
+  // end up in an overlapped onwrite situation.
+  this.bufferProcessing = false;
+
+  // the callback that's passed to _write(chunk,cb)
+  this.onwrite = function (er) {
+    onwrite(stream, er);
+  };
+
+  // the callback that the user supplies to write(chunk,encoding,cb)
+  this.writecb = null;
+
+  // the amount that is being written when _write is called.
+  this.writelen = 0;
+
+  this.bufferedRequest = null;
+  this.lastBufferedRequest = null;
+
+  // number of pending user-supplied write callbacks
+  // this must be 0 before 'finish' can be emitted
+  this.pendingcb = 0;
+
+  // emit prefinish if the only thing we're waiting for is _write cbs
+  // This is relevant for synchronous Transform streams
+  this.prefinished = false;
+
+  // True if the error was already emitted and should not be thrown again
+  this.errorEmitted = false;
+
+  // count buffered requests
+  this.bufferedRequestCount = 0;
+
+  // allocate the first CorkedRequest, there is always
+  // one allocated and free to use, and we maintain at most two
+  this.corkedRequestsFree = new CorkedRequest(this);
+}
+
+WritableState.prototype.getBuffer = function getBuffer() {
+  var current = this.bufferedRequest;
+  var out = [];
+  while (current) {
+    out.push(current);
+    current = current.next;
+  }
+  return out;
+};
+
+(function () {
+  try {
+    Object.defineProperty(WritableState.prototype, 'buffer', {
+      get: internalUtil.deprecate(function () {
+        return this.getBuffer();
+      }, '_writableState.buffer is deprecated. Use _writableState.getBuffer ' + 'instead.', 'DEP0003')
+    });
+  } catch (_) {}
+})();
+
+// Test _writableState for inheritance to account for Duplex streams,
+// whose prototype chain only points to Readable.
+var realHasInstance;
+if (typeof Symbol === 'function' && Symbol.hasInstance && typeof Function.prototype[Symbol.hasInstance] === 'function') {
+  realHasInstance = Function.prototype[Symbol.hasInstance];
+  Object.defineProperty(Writable, Symbol.hasInstance, {
+    value: function (object) {
+      if (realHasInstance.call(this, object)) return true;
+      if (this !== Writable) return false;
+
+      return object && object._writableState instanceof WritableState;
+    }
+  });
+} else {
+  realHasInstance = function (object) {
+    return object instanceof this;
+  };
+}
+
+function Writable(options) {
+  Duplex = Duplex || require('./_stream_duplex');
+
+  // Writable ctor is applied to Duplexes, too.
+  // `realHasInstance` is necessary because using plain `instanceof`
+  // would return false, as no `_writableState` property is attached.
+
+  // Trying to use the custom `instanceof` for Writable here will also break the
+  // Node.js LazyTransform implementation, which has a non-trivial getter for
+  // `_writableState` that would lead to infinite recursion.
+  if (!realHasInstance.call(Writable, this) && !(this instanceof Duplex)) {
+    return new Writable(options);
+  }
+
+  this._writableState = new WritableState(options, this);
+
+  // legacy.
+  this.writable = true;
+
+  if (options) {
+    if (typeof options.write === 'function') this._write = options.write;
+
+    if (typeof options.writev === 'function') this._writev = options.writev;
+
+    if (typeof options.destroy === 'function') this._destroy = options.destroy;
+
+    if (typeof options.final === 'function') this._final = options.final;
+  }
+
+  Stream.call(this);
+}
+
+// Otherwise people can pipe Writable streams, which is just wrong.
+Writable.prototype.pipe = function () {
+  this.emit('error', new Error('Cannot pipe, not readable'));
+};
+
+function writeAfterEnd(stream, cb) {
+  var er = new Error('write after end');
+  // TODO: defer error events consistently everywhere, not just the cb
+  stream.emit('error', er);
+  pna.nextTick(cb, er);
+}
+
+// Checks that a user-supplied chunk is valid, especially for the particular
+// mode the stream is in. Currently this means that `null` is never accepted
+// and undefined/non-string values are only allowed in object mode.
+function validChunk(stream, state, chunk, cb) {
+  var valid = true;
+  var er = false;
+
+  if (chunk === null) {
+    er = new TypeError('May not write null values to stream');
+  } else if (typeof chunk !== 'string' && chunk !== undefined && !state.objectMode) {
+    er = new TypeError('Invalid non-string/buffer chunk');
+  }
+  if (er) {
+    stream.emit('error', er);
+    pna.nextTick(cb, er);
+    valid = false;
+  }
+  return valid;
+}
+
+Writable.prototype.write = function (chunk, encoding, cb) {
+  var state = this._writableState;
+  var ret = false;
+  var isBuf = !state.objectMode && _isUint8Array(chunk);
+
+  if (isBuf && !Buffer.isBuffer(chunk)) {
+    chunk = _uint8ArrayToBuffer(chunk);
+  }
+
+  if (typeof encoding === 'function') {
+    cb = encoding;
+    encoding = null;
+  }
+
+  if (isBuf) encoding = 'buffer';else if (!encoding) encoding = state.defaultEncoding;
+
+  if (typeof cb !== 'function') cb = nop;
+
+  if (state.ended) writeAfterEnd(this, cb);else if (isBuf || validChunk(this, state, chunk, cb)) {
+    state.pendingcb++;
+    ret = writeOrBuffer(this, state, isBuf, chunk, encoding, cb);
+  }
+
+  return ret;
+};
+
+Writable.prototype.cork = function () {
+  var state = this._writableState;
+
+  state.corked++;
+};
+
+Writable.prototype.uncork = function () {
+  var state = this._writableState;
+
+  if (state.corked) {
+    state.corked--;
+
+    if (!state.writing && !state.corked && !state.finished && !state.bufferProcessing && state.bufferedRequest) clearBuffer(this, state);
+  }
+};
+
+Writable.prototype.setDefaultEncoding = function setDefaultEncoding(encoding) {
+  // node::ParseEncoding() requires lower case.
+  if (typeof encoding === 'string') encoding = encoding.toLowerCase();
+  if (!(['hex', 'utf8', 'utf-8', 'ascii', 'binary', 'base64', 'ucs2', 'ucs-2', 'utf16le', 'utf-16le', 'raw'].indexOf((encoding + '').toLowerCase()) > -1)) throw new TypeError('Unknown encoding: ' + encoding);
+  this._writableState.defaultEncoding = encoding;
+  return this;
+};
+
+function decodeChunk(state, chunk, encoding) {
+  if (!state.objectMode && state.decodeStrings !== false && typeof chunk === 'string') {
+    chunk = Buffer.from(chunk, encoding);
+  }
+  return chunk;
+}
+
+Object.defineProperty(Writable.prototype, 'writableHighWaterMark', {
+  // making it explicit this property is not enumerable
+  // because otherwise some prototype manipulation in
+  // userland will fail
+  enumerable: false,
+  get: function () {
+    return this._writableState.highWaterMark;
+  }
+});
+
+// if we're already writing something, then just put this
+// in the queue, and wait our turn.  Otherwise, call _write
+// If we return false, then we need a drain event, so set that flag.
+function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) {
+  if (!isBuf) {
+    var newChunk = decodeChunk(state, chunk, encoding);
+    if (chunk !== newChunk) {
+      isBuf = true;
+      encoding = 'buffer';
+      chunk = newChunk;
+    }
+  }
+  var len = state.objectMode ? 1 : chunk.length;
+
+  state.length += len;
+
+  var ret = state.length < state.highWaterMark;
+  // we must ensure that previous needDrain will not be reset to false.
+  if (!ret) state.needDrain = true;
+
+  if (state.writing || state.corked) {
+    var last = state.lastBufferedRequest;
+    state.lastBufferedRequest = {
+      chunk: chunk,
+      encoding: encoding,
+      isBuf: isBuf,
+      callback: cb,
+      next: null
+    };
+    if (last) {
+      last.next = state.lastBufferedRequest;
+    } else {
+      state.bufferedRequest = state.lastBufferedRequest;
+    }
+    state.bufferedRequestCount += 1;
+  } else {
+    doWrite(stream, state, false, len, chunk, encoding, cb);
+  }
+
+  return ret;
+}
+
+function doWrite(stream, state, writev, len, chunk, encoding, cb) {
+  state.writelen = len;
+  state.writecb = cb;
+  state.writing = true;
+  state.sync = true;
+  if (writev) stream._writev(chunk, state.onwrite);else stream._write(chunk, encoding, state.onwrite);
+  state.sync = false;
+}
+
+function onwriteError(stream, state, sync, er, cb) {
+  --state.pendingcb;
+
+  if (sync) {
+    // defer the callback if we are being called synchronously
+    // to avoid piling up things on the stack
+    pna.nextTick(cb, er);
+    // this can emit finish, and it will always happen
+    // after error
+    pna.nextTick(finishMaybe, stream, state);
+    stream._writableState.errorEmitted = true;
+    stream.emit('error', er);
+  } else {
+    // the caller expect this to happen before if
+    // it is async
+    cb(er);
+    stream._writableState.errorEmitted = true;
+    stream.emit('error', er);
+    // this can emit finish, but finish must
+    // always follow error
+    finishMaybe(stream, state);
+  }
+}
+
+function onwriteStateUpdate(state) {
+  state.writing = false;
+  state.writecb = null;
+  state.length -= state.writelen;
+  state.writelen = 0;
+}
+
+function onwrite(stream, er) {
+  var state = stream._writableState;
+  var sync = state.sync;
+  var cb = state.writecb;
+
+  onwriteStateUpdate(state);
+
+  if (er) onwriteError(stream, state, sync, er, cb);else {
+    // Check if we're actually ready to finish, but don't emit yet
+    var finished = needFinish(state);
+
+    if (!finished && !state.corked && !state.bufferProcessing && state.bufferedRequest) {
+      clearBuffer(stream, state);
+    }
+
+    if (sync) {
+      /*<replacement>*/
+      asyncWrite(afterWrite, stream, state, finished, cb);
+      /*</replacement>*/
+    } else {
+      afterWrite(stream, state, finished, cb);
+    }
+  }
+}
+
+function afterWrite(stream, state, finished, cb) {
+  if (!finished) onwriteDrain(stream, state);
+  state.pendingcb--;
+  cb();
+  finishMaybe(stream, state);
+}
+
+// Must force callback to be called on nextTick, so that we don't
+// emit 'drain' before the write() consumer gets the 'false' return
+// value, and has a chance to attach a 'drain' listener.
+function onwriteDrain(stream, state) {
+  if (state.length === 0 && state.needDrain) {
+    state.needDrain = false;
+    stream.emit('drain');
+  }
+}
+
+// if there's something in the buffer waiting, then process it
+function clearBuffer(stream, state) {
+  state.bufferProcessing = true;
+  var entry = state.bufferedRequest;
+
+  if (stream._writev && entry && entry.next) {
+    // Fast case, write everything using _writev()
+    var l = state.bufferedRequestCount;
+    var buffer = new Array(l);
+    var holder = state.corkedRequestsFree;
+    holder.entry = entry;
+
+    var count = 0;
+    var allBuffers = true;
+    while (entry) {
+      buffer[count] = entry;
+      if (!entry.isBuf) allBuffers = false;
+      entry = entry.next;
+      count += 1;
+    }
+    buffer.allBuffers = allBuffers;
+
+    doWrite(stream, state, true, state.length, buffer, '', holder.finish);
+
+    // doWrite is almost always async, defer these to save a bit of time
+    // as the hot path ends with doWrite
+    state.pendingcb++;
+    state.lastBufferedRequest = null;
+    if (holder.next) {
+      state.corkedRequestsFree = holder.next;
+      holder.next = null;
+    } else {
+      state.corkedRequestsFree = new CorkedRequest(state);
+    }
+    state.bufferedRequestCount = 0;
+  } else {
+    // Slow case, write chunks one-by-one
+    while (entry) {
+      var chunk = entry.chunk;
+      var encoding = entry.encoding;
+      var cb = entry.callback;
+      var len = state.objectMode ? 1 : chunk.length;
+
+      doWrite(stream, state, false, len, chunk, encoding, cb);
+      entry = entry.next;
+      state.bufferedRequestCount--;
+      // if we didn't call the onwrite immediately, then
+      // it means that we need to wait until it does.
+      // also, that means that the chunk and cb are currently
+      // being processed, so move the buffer counter past them.
+      if (state.writing) {
+        break;
+      }
+    }
+
+    if (entry === null) state.lastBufferedRequest = null;
+  }
+
+  state.bufferedRequest = entry;
+  state.bufferProcessing = false;
+}
+
+Writable.prototype._write = function (chunk, encoding, cb) {
+  cb(new Error('_write() is not implemented'));
+};
+
+Writable.prototype._writev = null;
+
+Writable.prototype.end = function (chunk, encoding, cb) {
+  var state = this._writableState;
+
+  if (typeof chunk === 'function') {
+    cb = chunk;
+    chunk = null;
+    encoding = null;
+  } else if (typeof encoding === 'function') {
+    cb = encoding;
+    encoding = null;
+  }
+
+  if (chunk !== null && chunk !== undefined) this.write(chunk, encoding);
+
+  // .end() fully uncorks
+  if (state.corked) {
+    state.corked = 1;
+    this.uncork();
+  }
+
+  // ignore unnecessary end() calls.
+  if (!state.ending && !state.finished) endWritable(this, state, cb);
+};
+
+function needFinish(state) {
+  return state.ending && state.length === 0 && state.bufferedRequest === null && !state.finished && !state.writing;
+}
+function callFinal(stream, state) {
+  stream._final(function (err) {
+    state.pendingcb--;
+    if (err) {
+      stream.emit('error', err);
+    }
+    state.prefinished = true;
+    stream.emit('prefinish');
+    finishMaybe(stream, state);
+  });
+}
+function prefinish(stream, state) {
+  if (!state.prefinished && !state.finalCalled) {
+    if (typeof stream._final === 'function') {
+      state.pendingcb++;
+      state.finalCalled = true;
+      pna.nextTick(callFinal, stream, state);
+    } else {
+      state.prefinished = true;
+      stream.emit('prefinish');
+    }
+  }
+}
+
+function finishMaybe(stream, state) {
+  var need = needFinish(state);
+  if (need) {
+    prefinish(stream, state);
+    if (state.pendingcb === 0) {
+      state.finished = true;
+      stream.emit('finish');
+    }
+  }
+  return need;
+}
+
+function endWritable(stream, state, cb) {
+  state.ending = true;
+  finishMaybe(stream, state);
+  if (cb) {
+    if (state.finished) pna.nextTick(cb);else stream.once('finish', cb);
+  }
+  state.ended = true;
+  stream.writable = false;
+}
+
+function onCorkedFinish(corkReq, state, err) {
+  var entry = corkReq.entry;
+  corkReq.entry = null;
+  while (entry) {
+    var cb = entry.callback;
+    state.pendingcb--;
+    cb(err);
+    entry = entry.next;
+  }
+  if (state.corkedRequestsFree) {
+    state.corkedRequestsFree.next = corkReq;
+  } else {
+    state.corkedRequestsFree = corkReq;
+  }
+}
+
+Object.defineProperty(Writable.prototype, 'destroyed', {
+  get: function () {
+    if (this._writableState === undefined) {
+      return false;
+    }
+    return this._writableState.destroyed;
+  },
+  set: function (value) {
+    // we ignore the value if the stream
+    // has not been initialized yet
+    if (!this._writableState) {
+      return;
+    }
+
+    // backward compatibility, the user is explicitly
+    // managing destroyed
+    this._writableState.destroyed = value;
+  }
+});
+
+Writable.prototype.destroy = destroyImpl.destroy;
+Writable.prototype._undestroy = destroyImpl.undestroy;
+Writable.prototype._destroy = function (err, cb) {
+  this.end();
+  cb(err);
+};

+ 79 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/lib/internal/streams/BufferList.js

@@ -0,0 +1,79 @@
+'use strict';
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var Buffer = require('safe-buffer').Buffer;
+var util = require('util');
+
+function copyBuffer(src, target, offset) {
+  src.copy(target, offset);
+}
+
+module.exports = function () {
+  function BufferList() {
+    _classCallCheck(this, BufferList);
+
+    this.head = null;
+    this.tail = null;
+    this.length = 0;
+  }
+
+  BufferList.prototype.push = function push(v) {
+    var entry = { data: v, next: null };
+    if (this.length > 0) this.tail.next = entry;else this.head = entry;
+    this.tail = entry;
+    ++this.length;
+  };
+
+  BufferList.prototype.unshift = function unshift(v) {
+    var entry = { data: v, next: this.head };
+    if (this.length === 0) this.tail = entry;
+    this.head = entry;
+    ++this.length;
+  };
+
+  BufferList.prototype.shift = function shift() {
+    if (this.length === 0) return;
+    var ret = this.head.data;
+    if (this.length === 1) this.head = this.tail = null;else this.head = this.head.next;
+    --this.length;
+    return ret;
+  };
+
+  BufferList.prototype.clear = function clear() {
+    this.head = this.tail = null;
+    this.length = 0;
+  };
+
+  BufferList.prototype.join = function join(s) {
+    if (this.length === 0) return '';
+    var p = this.head;
+    var ret = '' + p.data;
+    while (p = p.next) {
+      ret += s + p.data;
+    }return ret;
+  };
+
+  BufferList.prototype.concat = function concat(n) {
+    if (this.length === 0) return Buffer.alloc(0);
+    if (this.length === 1) return this.head.data;
+    var ret = Buffer.allocUnsafe(n >>> 0);
+    var p = this.head;
+    var i = 0;
+    while (p) {
+      copyBuffer(p.data, ret, i);
+      i += p.data.length;
+      p = p.next;
+    }
+    return ret;
+  };
+
+  return BufferList;
+}();
+
+if (util && util.inspect && util.inspect.custom) {
+  module.exports.prototype[util.inspect.custom] = function () {
+    var obj = util.inspect({ length: this.length });
+    return this.constructor.name + ' ' + obj;
+  };
+}

+ 74 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/lib/internal/streams/destroy.js

@@ -0,0 +1,74 @@
+'use strict';
+
+/*<replacement>*/
+
+var pna = require('process-nextick-args');
+/*</replacement>*/
+
+// undocumented cb() API, needed for core, not for public API
+function destroy(err, cb) {
+  var _this = this;
+
+  var readableDestroyed = this._readableState && this._readableState.destroyed;
+  var writableDestroyed = this._writableState && this._writableState.destroyed;
+
+  if (readableDestroyed || writableDestroyed) {
+    if (cb) {
+      cb(err);
+    } else if (err && (!this._writableState || !this._writableState.errorEmitted)) {
+      pna.nextTick(emitErrorNT, this, err);
+    }
+    return this;
+  }
+
+  // we set destroyed to true before firing error callbacks in order
+  // to make it re-entrance safe in case destroy() is called within callbacks
+
+  if (this._readableState) {
+    this._readableState.destroyed = true;
+  }
+
+  // if this is a duplex stream mark the writable part as destroyed as well
+  if (this._writableState) {
+    this._writableState.destroyed = true;
+  }
+
+  this._destroy(err || null, function (err) {
+    if (!cb && err) {
+      pna.nextTick(emitErrorNT, _this, err);
+      if (_this._writableState) {
+        _this._writableState.errorEmitted = true;
+      }
+    } else if (cb) {
+      cb(err);
+    }
+  });
+
+  return this;
+}
+
+function undestroy() {
+  if (this._readableState) {
+    this._readableState.destroyed = false;
+    this._readableState.reading = false;
+    this._readableState.ended = false;
+    this._readableState.endEmitted = false;
+  }
+
+  if (this._writableState) {
+    this._writableState.destroyed = false;
+    this._writableState.ended = false;
+    this._writableState.ending = false;
+    this._writableState.finished = false;
+    this._writableState.errorEmitted = false;
+  }
+}
+
+function emitErrorNT(self, err) {
+  self.emit('error', err);
+}
+
+module.exports = {
+  destroy: destroy,
+  undestroy: undestroy
+};

+ 1 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/lib/internal/streams/stream-browser.js

@@ -0,0 +1 @@
+module.exports = require('events').EventEmitter;

+ 1 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/lib/internal/streams/stream.js

@@ -0,0 +1 @@
+module.exports = require('stream');

+ 1 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/passthrough.js

@@ -0,0 +1 @@
+module.exports = require('./readable').PassThrough

+ 7 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/readable-browser.js

@@ -0,0 +1,7 @@
+exports = module.exports = require('./lib/_stream_readable.js');
+exports.Stream = exports;
+exports.Readable = exports;
+exports.Writable = require('./lib/_stream_writable.js');
+exports.Duplex = require('./lib/_stream_duplex.js');
+exports.Transform = require('./lib/_stream_transform.js');
+exports.PassThrough = require('./lib/_stream_passthrough.js');

+ 19 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/readable.js

@@ -0,0 +1,19 @@
+var Stream = require('stream');
+if (process.env.READABLE_STREAM === 'disable' && Stream) {
+  module.exports = Stream;
+  exports = module.exports = Stream.Readable;
+  exports.Readable = Stream.Readable;
+  exports.Writable = Stream.Writable;
+  exports.Duplex = Stream.Duplex;
+  exports.Transform = Stream.Transform;
+  exports.PassThrough = Stream.PassThrough;
+  exports.Stream = Stream;
+} else {
+  exports = module.exports = require('./lib/_stream_readable.js');
+  exports.Stream = Stream || exports;
+  exports.Readable = exports;
+  exports.Writable = require('./lib/_stream_writable.js');
+  exports.Duplex = require('./lib/_stream_duplex.js');
+  exports.Transform = require('./lib/_stream_transform.js');
+  exports.PassThrough = require('./lib/_stream_passthrough.js');
+}

+ 1 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/transform.js

@@ -0,0 +1 @@
+module.exports = require('./readable').Transform

+ 1 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/writable-browser.js

@@ -0,0 +1 @@
+module.exports = require('./lib/_stream_writable.js');

+ 8 - 0
backend/node_modules/concat-stream/node_modules/readable-stream/writable.js

@@ -0,0 +1,8 @@
+var Stream = require("stream")
+var Writable = require("./lib/_stream_writable.js")
+
+if (process.env.READABLE_STREAM === 'disable') {
+  module.exports = Stream && Stream.Writable || Writable
+} else {
+  module.exports = Writable
+}

+ 62 - 0
backend/node_modules/concat-stream/node_modules/safe-buffer/index.js

@@ -0,0 +1,62 @@
+/* eslint-disable node/no-deprecated-api */
+var buffer = require('buffer')
+var Buffer = buffer.Buffer
+
+// alternative to using Object.keys for old browsers
+function copyProps (src, dst) {
+  for (var key in src) {
+    dst[key] = src[key]
+  }
+}
+if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) {
+  module.exports = buffer
+} else {
+  // Copy properties from require('buffer')
+  copyProps(buffer, exports)
+  exports.Buffer = SafeBuffer
+}
+
+function SafeBuffer (arg, encodingOrOffset, length) {
+  return Buffer(arg, encodingOrOffset, length)
+}
+
+// Copy static methods from Buffer
+copyProps(Buffer, SafeBuffer)
+
+SafeBuffer.from = function (arg, encodingOrOffset, length) {
+  if (typeof arg === 'number') {
+    throw new TypeError('Argument must not be a number')
+  }
+  return Buffer(arg, encodingOrOffset, length)
+}
+
+SafeBuffer.alloc = function (size, fill, encoding) {
+  if (typeof size !== 'number') {
+    throw new TypeError('Argument must be a number')
+  }
+  var buf = Buffer(size)
+  if (fill !== undefined) {
+    if (typeof encoding === 'string') {
+      buf.fill(fill, encoding)
+    } else {
+      buf.fill(fill)
+    }
+  } else {
+    buf.fill(0)
+  }
+  return buf
+}
+
+SafeBuffer.allocUnsafe = function (size) {
+  if (typeof size !== 'number') {
+    throw new TypeError('Argument must be a number')
+  }
+  return Buffer(size)
+}
+
+SafeBuffer.allocUnsafeSlow = function (size) {
+  if (typeof size !== 'number') {
+    throw new TypeError('Argument must be a number')
+  }
+  return buffer.SlowBuffer(size)
+}

+ 296 - 0
backend/node_modules/concat-stream/node_modules/string_decoder/lib/string_decoder.js

@@ -0,0 +1,296 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+'use strict';
+
+/*<replacement>*/
+
+var Buffer = require('safe-buffer').Buffer;
+/*</replacement>*/
+
+var isEncoding = Buffer.isEncoding || function (encoding) {
+  encoding = '' + encoding;
+  switch (encoding && encoding.toLowerCase()) {
+    case 'hex':case 'utf8':case 'utf-8':case 'ascii':case 'binary':case 'base64':case 'ucs2':case 'ucs-2':case 'utf16le':case 'utf-16le':case 'raw':
+      return true;
+    default:
+      return false;
+  }
+};
+
+function _normalizeEncoding(enc) {
+  if (!enc) return 'utf8';
+  var retried;
+  while (true) {
+    switch (enc) {
+      case 'utf8':
+      case 'utf-8':
+        return 'utf8';
+      case 'ucs2':
+      case 'ucs-2':
+      case 'utf16le':
+      case 'utf-16le':
+        return 'utf16le';
+      case 'latin1':
+      case 'binary':
+        return 'latin1';
+      case 'base64':
+      case 'ascii':
+      case 'hex':
+        return enc;
+      default:
+        if (retried) return; // undefined
+        enc = ('' + enc).toLowerCase();
+        retried = true;
+    }
+  }
+};
+
+// Do not cache `Buffer.isEncoding` when checking encoding names as some
+// modules monkey-patch it to support additional encodings
+function normalizeEncoding(enc) {
+  var nenc = _normalizeEncoding(enc);
+  if (typeof nenc !== 'string' && (Buffer.isEncoding === isEncoding || !isEncoding(enc))) throw new Error('Unknown encoding: ' + enc);
+  return nenc || enc;
+}
+
+// StringDecoder provides an interface for efficiently splitting a series of
+// buffers into a series of JS strings without breaking apart multi-byte
+// characters.
+exports.StringDecoder = StringDecoder;
+function StringDecoder(encoding) {
+  this.encoding = normalizeEncoding(encoding);
+  var nb;
+  switch (this.encoding) {
+    case 'utf16le':
+      this.text = utf16Text;
+      this.end = utf16End;
+      nb = 4;
+      break;
+    case 'utf8':
+      this.fillLast = utf8FillLast;
+      nb = 4;
+      break;
+    case 'base64':
+      this.text = base64Text;
+      this.end = base64End;
+      nb = 3;
+      break;
+    default:
+      this.write = simpleWrite;
+      this.end = simpleEnd;
+      return;
+  }
+  this.lastNeed = 0;
+  this.lastTotal = 0;
+  this.lastChar = Buffer.allocUnsafe(nb);
+}
+
+StringDecoder.prototype.write = function (buf) {
+  if (buf.length === 0) return '';
+  var r;
+  var i;
+  if (this.lastNeed) {
+    r = this.fillLast(buf);
+    if (r === undefined) return '';
+    i = this.lastNeed;
+    this.lastNeed = 0;
+  } else {
+    i = 0;
+  }
+  if (i < buf.length) return r ? r + this.text(buf, i) : this.text(buf, i);
+  return r || '';
+};
+
+StringDecoder.prototype.end = utf8End;
+
+// Returns only complete characters in a Buffer
+StringDecoder.prototype.text = utf8Text;
+
+// Attempts to complete a partial non-UTF-8 character using bytes from a Buffer
+StringDecoder.prototype.fillLast = function (buf) {
+  if (this.lastNeed <= buf.length) {
+    buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed);
+    return this.lastChar.toString(this.encoding, 0, this.lastTotal);
+  }
+  buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, buf.length);
+  this.lastNeed -= buf.length;
+};
+
+// Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a
+// continuation byte. If an invalid byte is detected, -2 is returned.
+function utf8CheckByte(byte) {
+  if (byte <= 0x7F) return 0;else if (byte >> 5 === 0x06) return 2;else if (byte >> 4 === 0x0E) return 3;else if (byte >> 3 === 0x1E) return 4;
+  return byte >> 6 === 0x02 ? -1 : -2;
+}
+
+// Checks at most 3 bytes at the end of a Buffer in order to detect an
+// incomplete multi-byte UTF-8 character. The total number of bytes (2, 3, or 4)
+// needed to complete the UTF-8 character (if applicable) are returned.
+function utf8CheckIncomplete(self, buf, i) {
+  var j = buf.length - 1;
+  if (j < i) return 0;
+  var nb = utf8CheckByte(buf[j]);
+  if (nb >= 0) {
+    if (nb > 0) self.lastNeed = nb - 1;
+    return nb;
+  }
+  if (--j < i || nb === -2) return 0;
+  nb = utf8CheckByte(buf[j]);
+  if (nb >= 0) {
+    if (nb > 0) self.lastNeed = nb - 2;
+    return nb;
+  }
+  if (--j < i || nb === -2) return 0;
+  nb = utf8CheckByte(buf[j]);
+  if (nb >= 0) {
+    if (nb > 0) {
+      if (nb === 2) nb = 0;else self.lastNeed = nb - 3;
+    }
+    return nb;
+  }
+  return 0;
+}
+
+// Validates as many continuation bytes for a multi-byte UTF-8 character as
+// needed or are available. If we see a non-continuation byte where we expect
+// one, we "replace" the validated continuation bytes we've seen so far with
+// a single UTF-8 replacement character ('\ufffd'), to match v8's UTF-8 decoding
+// behavior. The continuation byte check is included three times in the case
+// where all of the continuation bytes for a character exist in the same buffer.
+// It is also done this way as a slight performance increase instead of using a
+// loop.
+function utf8CheckExtraBytes(self, buf, p) {
+  if ((buf[0] & 0xC0) !== 0x80) {
+    self.lastNeed = 0;
+    return '\ufffd';
+  }
+  if (self.lastNeed > 1 && buf.length > 1) {
+    if ((buf[1] & 0xC0) !== 0x80) {
+      self.lastNeed = 1;
+      return '\ufffd';
+    }
+    if (self.lastNeed > 2 && buf.length > 2) {
+      if ((buf[2] & 0xC0) !== 0x80) {
+        self.lastNeed = 2;
+        return '\ufffd';
+      }
+    }
+  }
+}
+
+// Attempts to complete a multi-byte UTF-8 character using bytes from a Buffer.
+function utf8FillLast(buf) {
+  var p = this.lastTotal - this.lastNeed;
+  var r = utf8CheckExtraBytes(this, buf, p);
+  if (r !== undefined) return r;
+  if (this.lastNeed <= buf.length) {
+    buf.copy(this.lastChar, p, 0, this.lastNeed);
+    return this.lastChar.toString(this.encoding, 0, this.lastTotal);
+  }
+  buf.copy(this.lastChar, p, 0, buf.length);
+  this.lastNeed -= buf.length;
+}
+
+// Returns all complete UTF-8 characters in a Buffer. If the Buffer ended on a
+// partial character, the character's bytes are buffered until the required
+// number of bytes are available.
+function utf8Text(buf, i) {
+  var total = utf8CheckIncomplete(this, buf, i);
+  if (!this.lastNeed) return buf.toString('utf8', i);
+  this.lastTotal = total;
+  var end = buf.length - (total - this.lastNeed);
+  buf.copy(this.lastChar, 0, end);
+  return buf.toString('utf8', i, end);
+}
+
+// For UTF-8, a replacement character is added when ending on a partial
+// character.
+function utf8End(buf) {
+  var r = buf && buf.length ? this.write(buf) : '';
+  if (this.lastNeed) return r + '\ufffd';
+  return r;
+}
+
+// UTF-16LE typically needs two bytes per character, but even if we have an even
+// number of bytes available, we need to check if we end on a leading/high
+// surrogate. In that case, we need to wait for the next two bytes in order to
+// decode the last character properly.
+function utf16Text(buf, i) {
+  if ((buf.length - i) % 2 === 0) {
+    var r = buf.toString('utf16le', i);
+    if (r) {
+      var c = r.charCodeAt(r.length - 1);
+      if (c >= 0xD800 && c <= 0xDBFF) {
+        this.lastNeed = 2;
+        this.lastTotal = 4;
+        this.lastChar[0] = buf[buf.length - 2];
+        this.lastChar[1] = buf[buf.length - 1];
+        return r.slice(0, -1);
+      }
+    }
+    return r;
+  }
+  this.lastNeed = 1;
+  this.lastTotal = 2;
+  this.lastChar[0] = buf[buf.length - 1];
+  return buf.toString('utf16le', i, buf.length - 1);
+}
+
+// For UTF-16LE we do not explicitly append special replacement characters if we
+// end on a partial character, we simply let v8 handle that.
+function utf16End(buf) {
+  var r = buf && buf.length ? this.write(buf) : '';
+  if (this.lastNeed) {
+    var end = this.lastTotal - this.lastNeed;
+    return r + this.lastChar.toString('utf16le', 0, end);
+  }
+  return r;
+}
+
+function base64Text(buf, i) {
+  var n = (buf.length - i) % 3;
+  if (n === 0) return buf.toString('base64', i);
+  this.lastNeed = 3 - n;
+  this.lastTotal = 3;
+  if (n === 1) {
+    this.lastChar[0] = buf[buf.length - 1];
+  } else {
+    this.lastChar[0] = buf[buf.length - 2];
+    this.lastChar[1] = buf[buf.length - 1];
+  }
+  return buf.toString('base64', i, buf.length - n);
+}
+
+function base64End(buf) {
+  var r = buf && buf.length ? this.write(buf) : '';
+  if (this.lastNeed) return r + this.lastChar.toString('base64', 0, 3 - this.lastNeed);
+  return r;
+}
+
+// Pass bytes on through for single-byte encodings (e.g. ascii, latin1, hex)
+function simpleWrite(buf) {
+  return buf.toString(this.encoding);
+}
+
+function simpleEnd(buf) {
+  return buf && buf.length ? this.write(buf) : '';
+}

+ 107 - 0
backend/node_modules/core-util-is/lib/util.js

@@ -0,0 +1,107 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// NOTE: These type checking functions intentionally don't use `instanceof`
+// because it is fragile and can be easily faked with `Object.create()`.
+
+function isArray(arg) {
+  if (Array.isArray) {
+    return Array.isArray(arg);
+  }
+  return objectToString(arg) === '[object Array]';
+}
+exports.isArray = isArray;
+
+function isBoolean(arg) {
+  return typeof arg === 'boolean';
+}
+exports.isBoolean = isBoolean;
+
+function isNull(arg) {
+  return arg === null;
+}
+exports.isNull = isNull;
+
+function isNullOrUndefined(arg) {
+  return arg == null;
+}
+exports.isNullOrUndefined = isNullOrUndefined;
+
+function isNumber(arg) {
+  return typeof arg === 'number';
+}
+exports.isNumber = isNumber;
+
+function isString(arg) {
+  return typeof arg === 'string';
+}
+exports.isString = isString;
+
+function isSymbol(arg) {
+  return typeof arg === 'symbol';
+}
+exports.isSymbol = isSymbol;
+
+function isUndefined(arg) {
+  return arg === void 0;
+}
+exports.isUndefined = isUndefined;
+
+function isRegExp(re) {
+  return objectToString(re) === '[object RegExp]';
+}
+exports.isRegExp = isRegExp;
+
+function isObject(arg) {
+  return typeof arg === 'object' && arg !== null;
+}
+exports.isObject = isObject;
+
+function isDate(d) {
+  return objectToString(d) === '[object Date]';
+}
+exports.isDate = isDate;
+
+function isError(e) {
+  return (objectToString(e) === '[object Error]' || e instanceof Error);
+}
+exports.isError = isError;
+
+function isFunction(arg) {
+  return typeof arg === 'function';
+}
+exports.isFunction = isFunction;
+
+function isPrimitive(arg) {
+  return arg === null ||
+         typeof arg === 'boolean' ||
+         typeof arg === 'number' ||
+         typeof arg === 'string' ||
+         typeof arg === 'symbol' ||  // ES6 symbol
+         typeof arg === 'undefined';
+}
+exports.isPrimitive = isPrimitive;
+
+exports.isBuffer = require('buffer').Buffer.isBuffer;
+
+function objectToString(o) {
+  return Object.prototype.toString.call(o);
+}

+ 41 - 0
backend/node_modules/express-fileupload/example/server.js

@@ -0,0 +1,41 @@
+const express = require('express');
+const fileUpload = require('../lib/index');
+const app = express();
+
+const PORT = 8000;
+app.use('/form', express.static(__dirname + '/index.html'));
+
+// default options
+app.use(fileUpload());
+
+app.get('/ping', function(req, res) {
+  res.send('pong');
+});
+
+app.post('/upload', function(req, res) {
+  let sampleFile;
+  let uploadPath;
+
+  if (!req.files || Object.keys(req.files).length === 0) {
+    res.status(400).send('No files were uploaded.');
+    return;
+  }
+
+  console.log('req.files >>>', req.files); // eslint-disable-line
+
+  sampleFile = req.files.sampleFile;
+
+  uploadPath = __dirname + '/uploads/' + sampleFile.name;
+
+  sampleFile.mv(uploadPath, function(err) {
+    if (err) {
+      return res.status(500).send(err);
+    }
+
+    res.send('File uploaded to ' + uploadPath);
+  });
+});
+
+app.listen(PORT, function() {
+  console.log('Express server listening on port ', PORT); // eslint-disable-line
+});

+ 65 - 0
backend/node_modules/express-fileupload/lib/fileFactory.js

@@ -0,0 +1,65 @@
+'use strict';
+
+const {
+  isFunc,
+  debugLog,
+  moveFile,
+  promiseCallback,
+  checkAndMakeDir,
+  saveBufferToFile
+} = require('./utilities');
+
+/**
+ * Returns Local function that moves the file to a different location on the filesystem
+ * which takes two function arguments to make it compatible w/ Promise or Callback APIs
+ * @param {String} filePath - destination file path.
+ * @param {Object} options - file factory options.
+ * @param {Object} fileUploadOptions - middleware options.
+ * @returns {Function}
+ */
+const moveFromTemp = (filePath, options, fileUploadOptions) => (resolve, reject) => {
+  debugLog(fileUploadOptions, `Moving temporary file ${options.tempFilePath} to ${filePath}`);
+  moveFile(options.tempFilePath, filePath, promiseCallback(resolve, reject));
+};
+
+/**
+ * Returns Local function that moves the file from buffer to a different location on the filesystem
+ * which takes two function arguments to make it compatible w/ Promise or Callback APIs
+ * @param {String} filePath - destination file path.
+ * @param {Object} options - file factory options.
+ * @param {Object} fileUploadOptions - middleware options.
+ * @returns {Function}
+ */
+const moveFromBuffer = (filePath, options, fileUploadOptions) => (resolve, reject) => {
+  debugLog(fileUploadOptions, `Moving uploaded buffer to ${filePath}`);
+  saveBufferToFile(options.buffer, filePath, promiseCallback(resolve, reject));
+};
+
+module.exports = (options, fileUploadOptions = {}) => {
+  // see: https://github.com/richardgirges/express-fileupload/issues/14
+  // firefox uploads empty file in case of cache miss when f5ing page.
+  // resulting in unexpected behavior. if there is no file data, the file is invalid.
+  // if (!fileUploadOptions.useTempFiles && !options.buffer.length) return;
+  
+  // Create and return file object.
+  return {
+    name: options.name,
+    data: options.buffer,
+    size: options.size,
+    encoding: options.encoding,
+    tempFilePath: options.tempFilePath,
+    truncated: options.truncated,
+    mimetype: options.mimetype,
+    md5: options.hash,
+    mv: (filePath, callback) => {
+      // Define a propper move function.
+      const moveFunc = fileUploadOptions.useTempFiles
+        ? moveFromTemp(filePath, options, fileUploadOptions)
+        : moveFromBuffer(filePath, options, fileUploadOptions);
+      // Create a folder for a file.
+      checkAndMakeDir(fileUploadOptions, filePath);
+      // If callback is passed in, use the callback API, otherwise return a promise.
+      return isFunc(callback) ? moveFunc(callback) : new Promise(moveFunc);
+    }
+  };
+};

+ 39 - 0
backend/node_modules/express-fileupload/lib/index.js

@@ -0,0 +1,39 @@
+'use strict';
+
+const path = require('path');
+const processMultipart = require('./processMultipart');
+const isEligibleRequest = require('./isEligibleRequest');
+const { buildOptions, debugLog } = require('./utilities');
+const busboy = require('busboy'); // eslint-disable-line no-unused-vars
+
+const DEFAULT_OPTIONS = {
+  debug: false,
+  uploadTimeout: 60000,
+  fileHandler: false,
+  uriDecodeFileNames: false,
+  safeFileNames: false,
+  preserveExtension: false,
+  abortOnLimit: false,
+  responseOnLimit: 'File size limit has been reached',
+  limitHandler: false,
+  createParentPath: false,
+  parseNested: false,
+  useTempFiles: false,
+  tempFileDir: path.join(process.cwd(), 'tmp')
+};
+
+/**
+ * Expose the file upload middleware
+ * @param {DEFAULT_OPTIONS & busboy.BusboyConfig} options - Middleware options.
+ * @returns {Function} - express-fileupload middleware.
+ */
+module.exports = (options) => {
+  const uploadOptions = buildOptions(DEFAULT_OPTIONS, options);
+  return (req, res, next) => {
+    if (!isEligibleRequest(req)) {
+      debugLog(uploadOptions, 'Request is not eligible for file upload!');
+      return next();
+    }
+    processMultipart(uploadOptions, req, res, next);
+  };
+};

+ 34 - 0
backend/node_modules/express-fileupload/lib/isEligibleRequest.js

@@ -0,0 +1,34 @@
+const ACCEPTABLE_CONTENT_TYPE = /^(multipart\/.+);(.*)$/i;
+const UNACCEPTABLE_METHODS = ['GET', 'HEAD'];
+
+/**
+ * Ensures the request contains a content body
+ * @param  {Object}  req Express req object
+ * @returns {Boolean}
+ */
+const hasBody = (req) => {
+  return ('transfer-encoding' in req.headers) ||
+    ('content-length' in req.headers && req.headers['content-length'] !== '0');
+};
+
+/**
+ * Ensures the request is not using a non-compliant multipart method
+ * such as GET or HEAD
+ * @param  {Object}  req Express req object
+ * @returns {Boolean}
+ */
+const hasAcceptableMethod = req => !UNACCEPTABLE_METHODS.includes(req.method);
+
+/**
+ * Ensures that only multipart requests are processed by express-fileupload
+ * @param  {Object}  req Express req object
+ * @returns {Boolean}
+ */
+const hasAcceptableContentType = req => ACCEPTABLE_CONTENT_TYPE.test(req.headers['content-type']);
+
+/**
+ * Ensures that the request in question is eligible for file uploads
+ * @param {Object} req Express req object
+ * @returns {Boolean}
+ */
+module.exports = req => hasBody(req) && hasAcceptableMethod(req) && hasAcceptableContentType(req);

+ 42 - 0
backend/node_modules/express-fileupload/lib/memHandler.js

@@ -0,0 +1,42 @@
+const crypto = require('crypto');
+const { debugLog } = require('./utilities');
+
+/**
+ * memHandler - In memory upload handler
+ * @param {Object} options
+ * @param {String} fieldname
+ * @param {String} filename
+ * @returns {Object}
+ */
+module.exports = (options, fieldname, filename) => {
+  const buffers = [];
+  const hash = crypto.createHash('md5');
+  let fileSize = 0;
+  let completed = false;
+
+  const getBuffer = () => Buffer.concat(buffers, fileSize);
+
+  return {
+    dataHandler: (data) => {
+      if (completed === true) {
+        debugLog(options, `Error: got ${fieldname}->${filename} data chunk for completed upload!`);
+        return;
+      }
+      buffers.push(data);
+      hash.update(data);
+      fileSize += data.length;
+      debugLog(options, `Uploading ${fieldname}->${filename}, bytes:${fileSize}...`);
+    },
+    getBuffer: getBuffer,
+    getFilePath: () => '',
+    getFileSize: () => fileSize,
+    getHash: () => hash.digest('hex'),
+    complete: () => {
+      debugLog(options, `Upload ${fieldname}->${filename} completed, bytes:${fileSize}.`);
+      completed = true;
+      return getBuffer();
+    },
+    cleanup: () => { completed = true; },
+    getWritePromise: () => Promise.resolve()
+  };
+};

+ 168 - 0
backend/node_modules/express-fileupload/lib/processMultipart.js

@@ -0,0 +1,168 @@
+const Busboy = require('busboy');
+const UploadTimer = require('./uploadtimer');
+const fileFactory = require('./fileFactory');
+const memHandler = require('./memHandler');
+const tempFileHandler = require('./tempFileHandler');
+const processNested = require('./processNested');
+const {
+  isFunc,
+  debugLog,
+  buildFields,
+  buildOptions,
+  parseFileName
+} = require('./utilities');
+
+const waitFlushProperty = Symbol('wait flush property symbol');
+
+/**
+ * Processes multipart request
+ * Builds a req.body object for fields
+ * Builds a req.files object for files
+ * @param  {Object}   options expressFileupload and Busboy options
+ * @param  {Object}   req     Express request object
+ * @param  {Object}   res     Express response object
+ * @param  {Function} next    Express next method
+ * @return {void}
+ */
+module.exports = (options, req, res, next) => {
+  req.files = null;
+
+  // Build busboy options and init busboy instance.
+  const busboyOptions = buildOptions(options, { headers: req.headers });
+  const busboy = Busboy(busboyOptions);
+
+  // Close connection with specified reason and http code, default: 400 Bad Request.
+  const closeConnection = (code, reason) => {
+    req.unpipe(busboy);
+    res.writeHead(code || 400, { Connection: 'close' });
+    res.end(reason || 'Bad Request');
+  };
+
+  // Express proxies sometimes attach multipart data to a buffer
+  if (req.body instanceof Buffer) {
+    req.body = Object.create(null);
+  }
+  // Build multipart req.body fields
+  busboy.on('field', (field, val) => req.body = buildFields(req.body, field, val));
+
+  // Build req.files fields
+  busboy.on('file', (field, file, info) => {
+    // Parse file name(cutting huge names, decoding, etc..).
+    const {filename:name, encoding, mimeType: mime} = info;
+    const filename = parseFileName(options, name);
+    // Define methods and handlers for upload process.
+    const {
+      dataHandler,
+      getFilePath,
+      getFileSize,
+      getHash,
+      complete,
+      cleanup,
+      getWritePromise
+    } = options.useTempFiles
+      ? tempFileHandler(options, field, filename) // Upload into temporary file.
+      : memHandler(options, field, filename);     // Upload into RAM.
+
+    const writePromise = options.useTempFiles
+      ? getWritePromise().catch(err => {
+        req.unpipe(busboy);
+        req.resume();
+        cleanup();
+        next(err);
+      }) : getWritePromise();
+
+    // Define upload timer.
+    const uploadTimer = new UploadTimer(options.uploadTimeout, () => {
+      file.removeAllListeners('data');
+      file.resume();
+      // After destroy an error event will be emitted and file clean up will be done.
+      file.destroy(new Error(`Upload timeout ${field}->${filename}, bytes:${getFileSize()}`));
+    });
+
+    file.on('limit', () => {
+      debugLog(options, `Size limit reached for ${field}->${filename}, bytes:${getFileSize()}`);
+      // Reset upload timer in case of file limit reached.
+      uploadTimer.clear();
+      // Run a user defined limit handler if it has been set.
+      if (isFunc(options.limitHandler)) return options.limitHandler(req, res, next);
+      // Close connection with 413 code and do cleanup if abortOnLimit set(default: false).
+      if (options.abortOnLimit) {
+        debugLog(options, `Aborting upload because of size limit ${field}->${filename}.`);
+        !isFunc(options.limitHandler) ? closeConnection(413, options.responseOnLimit) : '';
+        cleanup();
+      }
+    });
+
+    file.on('data', (data) => {
+      uploadTimer.set(); // Refresh upload timer each time new data chunk came.
+      dataHandler(data); // Handle new piece of data.
+    });
+
+    file.on('end', () => {
+      const size = getFileSize();
+      // Debug logging for file upload ending.
+      debugLog(options, `Upload finished ${field}->${filename}, bytes:${size}`);
+      // Reset upload timer in case of end event.
+      uploadTimer.clear();
+      // See https://github.com/richardgirges/express-fileupload/issues/191
+      // Do not add file instance to the req.files if original name and size are empty.
+      // Empty name and zero size indicates empty file field in the posted form.
+      if (!name && size === 0) {
+        if (options.useTempFiles) {
+          cleanup();
+          debugLog(options, `Removing the empty file ${field}->${filename}`);
+        }
+        return debugLog(options, `Don't add file instance if original name and size are empty`);
+      }
+      req.files = buildFields(req.files, field, fileFactory({
+        buffer: complete(),
+        name: filename,
+        tempFilePath: getFilePath(),
+        hash: getHash(),
+        size,
+        encoding,
+        truncated: file.truncated,
+        mimetype: mime
+      }, options));
+
+      if (!req[waitFlushProperty]) {
+        req[waitFlushProperty] = [];
+      }
+      req[waitFlushProperty].push(writePromise);
+    });
+
+    file.on('error', (err) => {
+      uploadTimer.clear(); // Reset upload timer in case of errors.
+      debugLog(options, err);
+      cleanup();
+      next();
+    });
+
+    // Debug logging for a new file upload.
+    debugLog(options, `New upload started ${field}->${filename}, bytes:${getFileSize()}`);
+    // Set new upload timeout for a new file.
+    uploadTimer.set();
+  });
+
+  busboy.on('finish', () => {
+    debugLog(options, `Busboy finished parsing request.`);
+    if (options.parseNested) {
+      req.body = processNested(req.body);
+      req.files = processNested(req.files);
+    }
+
+    if (!req[waitFlushProperty]) return next();
+    Promise.all(req[waitFlushProperty])
+      .then(() => {
+        delete req[waitFlushProperty];
+        next();
+      });
+  });
+
+  busboy.on('error', (err) => {
+    debugLog(options, `Busboy error`);
+    next(err);
+  });
+
+  req.pipe(busboy);
+};

+ 35 - 0
backend/node_modules/express-fileupload/lib/processNested.js

@@ -0,0 +1,35 @@
+const { isSafeFromPollution } = require("./utilities");
+
+module.exports = function(data){
+  if (!data || data.length < 1) return Object.create(null);
+
+  let d = Object.create(null),
+    keys = Object.keys(data);
+
+  for (let i = 0; i < keys.length; i++) {
+    let key = keys[i],
+      value = data[key],
+      current = d,
+      keyParts = key
+        .replace(new RegExp(/\[/g), '.')
+        .replace(new RegExp(/\]/g), '')
+        .split('.');
+
+    for (let index = 0; index < keyParts.length; index++){
+      let k = keyParts[index];
+
+      // Ensure we don't allow prototype pollution
+      if (!isSafeFromPollution(current, k)) {
+        continue;
+      }
+
+      if (index >= keyParts.length - 1){
+        current[k] = value;
+      } else {
+        if (!current[k]) current[k] = !isNaN(keyParts[index + 1]) ? [] : Object.create(null);
+        current = current[k];
+      }
+    }
+  }
+  return d;
+};

+ 64 - 0
backend/node_modules/express-fileupload/lib/tempFileHandler.js

@@ -0,0 +1,64 @@
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const {
+  debugLog,
+  checkAndMakeDir,
+  getTempFilename,
+  deleteFile
+} = require('./utilities');
+
+module.exports = (options, fieldname, filename) => {
+  const dir = path.normalize(options.tempFileDir);
+  const tempFilePath = path.join(dir, getTempFilename());
+  checkAndMakeDir({ createParentPath: true }, tempFilePath);
+
+  debugLog(options, `Temporary file path is ${tempFilePath}`);
+ 
+  const hash = crypto.createHash('md5');
+  let fileSize = 0;
+  let completed = false;
+
+  debugLog(options, `Opening write stream for ${fieldname}->${filename}...`);
+  const writeStream = fs.createWriteStream(tempFilePath);
+  const writePromise = new Promise((resolve, reject) => {
+    writeStream.on('finish', () => resolve());
+    writeStream.on('error', (err) => {
+      debugLog(options, `Error write temp file: ${err}`);
+      reject(err);
+    });
+  });
+
+  return {
+    dataHandler: (data) => {
+      if (completed === true) {
+        debugLog(options, `Error: got ${fieldname}->${filename} data chunk for completed upload!`);
+        return;
+      }
+      writeStream.write(data);
+      hash.update(data);
+      fileSize += data.length;
+      debugLog(options, `Uploading ${fieldname}->${filename}, bytes:${fileSize}...`);
+    },
+    getFilePath: () => tempFilePath,
+    getFileSize: () => fileSize,
+    getHash: () => hash.digest('hex'),
+    complete: () => {
+      completed = true;
+      debugLog(options, `Upload ${fieldname}->${filename} completed, bytes:${fileSize}.`);
+      if (writeStream !== false) writeStream.end();
+      // Return empty buff since data was uploaded into a temp file.
+      return Buffer.concat([]);
+    },
+    cleanup: () => {
+      completed = true;
+      debugLog(options, `Cleaning up temporary file ${tempFilePath}...`);
+      writeStream.end();
+      deleteFile(tempFilePath, err => (err 
+        ? debugLog(options, `Cleaning up temporary file ${tempFilePath} failed: ${err}`)
+        : debugLog(options, `Cleaning up temporary file ${tempFilePath} done.`)
+      ));
+    },
+    getWritePromise: () => writePromise
+  };
+};

+ 26 - 0
backend/node_modules/express-fileupload/lib/uploadtimer.js

@@ -0,0 +1,26 @@
+class UploadTimer {
+  /**
+   * @constructor
+   * @param {number} timeout - timer timeout in msecs. 
+   * @param {Function} callback - callback to run when timeout reached.
+   */
+  constructor(timeout = 0, callback = () => {}) {
+    this.timeout = timeout;
+    this.callback = callback;
+    this.timer = null;
+  }
+
+  clear() {
+    clearTimeout(this.timer);
+  }
+
+  set() {
+    // Do not start a timer if zero timeout or it hasn't been set. 
+    if (!this.timeout) return false;
+    this.clear();
+    this.timer = setTimeout(this.callback, this.timeout);
+    return true;
+  }
+}
+
+module.exports = UploadTimer;

+ 311 - 0
backend/node_modules/express-fileupload/lib/utilities.js

@@ -0,0 +1,311 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const { Readable } = require('stream');
+
+// Parameters for safe file name parsing.
+const SAFE_FILE_NAME_REGEX = /[^\w-]/g;
+const MAX_EXTENSION_LENGTH = 3;
+
+// Parameters to generate unique temporary file names:
+const TEMP_COUNTER_MAX = 65536;
+const TEMP_PREFIX = 'tmp';
+let tempCounter = 0;
+
+/**
+ * Logs message to console if debug option set to true.
+ * @param {Object} options - options object.
+ * @param {string} msg - message to log.
+ * @returns {boolean} - false if debug is off.
+ */
+const debugLog = (options, msg) => {
+  const opts = options || {};
+  if (!opts.debug) return false;
+  console.log(`Express-file-upload: ${msg}`); // eslint-disable-line
+  return true;
+};
+
+/**
+ * Generates unique temporary file name. e.g. tmp-5000-156788789789.
+ * @param {string} prefix - a prefix for generated unique file name.
+ * @returns {string}
+ */
+const getTempFilename = (prefix = TEMP_PREFIX) => {
+  tempCounter = tempCounter >= TEMP_COUNTER_MAX ? 1 : tempCounter + 1;
+  return `${prefix}-${tempCounter}-${Date.now()}`;
+};
+
+/**
+ * isFunc: Checks if argument is a function.
+ * @returns {boolean} - Returns true if argument is a function.
+ */
+const isFunc = func => func && func.constructor && func.call && func.apply ? true: false;
+
+/**
+ * Set errorFunc to the same value as successFunc for callback mode.
+ * @returns {Function}
+ */
+const errorFunc = (resolve, reject) => isFunc(reject) ? reject : resolve;
+
+/**
+ * Return a callback function for promise resole/reject args.
+ * Ensures that callback is called only once.
+ * @returns {Function}
+ */
+const promiseCallback = (resolve, reject) => {
+  let hasFired = false;
+  return (err) => {
+    if (hasFired) {
+      return;
+    }
+
+    hasFired = true;
+    return err ? errorFunc(resolve, reject)(err) : resolve();
+  };
+};
+
+/**
+ * Builds instance options from arguments objects(can't be arrow function).
+ * @returns {Object} - result options.
+ */
+const buildOptions = function() {
+  const result = {};
+  [...arguments].forEach(options => {
+    if (!options || typeof options !== 'object') return;
+    Object.keys(options).forEach(i => result[i] = options[i]);
+  });
+  return result;
+};
+
+// The default prototypes for both objects and arrays.
+// Used by isSafeFromPollution
+const OBJECT_PROTOTYPE_KEYS = Object.getOwnPropertyNames(Object.prototype);
+const ARRAY_PROTOTYPE_KEYS = Object.getOwnPropertyNames(Array.prototype);
+
+/**
+ * Determines whether a key insertion into an object could result in a prototype pollution
+ * @param {Object} base - The object whose insertion we are checking
+ * @param {string} key - The key that will be inserted
+ */
+const isSafeFromPollution = (base, key) => {
+  // We perform an instanceof check instead of Array.isArray as the former is more
+  // permissive for cases in which the object as an Array prototype but was not constructed
+  // via an Array constructor or literal.
+  const TOUCHES_ARRAY_PROTOTYPE = (base instanceof Array) && ARRAY_PROTOTYPE_KEYS.includes(key);
+  const TOUCHES_OBJECT_PROTOTYPE = OBJECT_PROTOTYPE_KEYS.includes(key);
+
+  return !TOUCHES_ARRAY_PROTOTYPE && !TOUCHES_OBJECT_PROTOTYPE;
+};
+
+/**
+ * Builds request fields (using to build req.body and req.files)
+ * @param {Object} instance - request object.
+ * @param {string} field - field name.
+ * @param {any} value - field value.
+ * @returns {Object}
+ */
+const buildFields = (instance, field, value) => {
+  // Do nothing if value is not set.
+  if (value === null || value === undefined) return instance;
+  instance = instance || Object.create(null);
+
+  if (!isSafeFromPollution(instance, field)) {
+    return instance;
+  }
+  // Non-array fields
+  if (!instance[field]) {
+    instance[field] = value;
+    return instance;
+  }
+  // Array fields
+  if (instance[field] instanceof Array) {
+    instance[field].push(value);
+  } else {
+    instance[field] = [instance[field], value];
+  }
+  return instance;
+};
+
+/**
+ * Creates a folder for file specified in the path variable
+ * @param {Object} fileUploadOptions
+ * @param {string} filePath
+ * @returns {boolean}
+ */
+const checkAndMakeDir = (fileUploadOptions, filePath) => {
+  // Check upload options were set.
+  if (!fileUploadOptions) return false;
+  if (!fileUploadOptions.createParentPath) return false;
+  // Check whether folder for the file exists.
+  if (!filePath) return false;
+  const parentPath = path.dirname(filePath);
+  // Create folder if it doesn't exist.
+  if (!fs.existsSync(parentPath)) fs.mkdirSync(parentPath, { recursive: true });
+  // Checks folder again and return a results.
+  return fs.existsSync(parentPath);
+};
+
+/**
+ * Deletes a file.
+ * @param {string} file - Path to the file to delete.
+ * @param {Function} callback
+ */
+const deleteFile = (file, callback) => fs.unlink(file, callback);
+
+/**
+ * Copy file via streams
+ * @param {string} src - Path to the source file
+ * @param {string} dst - Path to the destination file.
+ */
+const copyFile = (src, dst, callback) => {
+  // cbCalled flag and runCb helps to run cb only once.
+  let cbCalled = false;
+  let runCb = (err) => {
+    if (cbCalled) return;
+    cbCalled = true;
+    callback(err);
+  };
+  // Create read stream
+  let readable = fs.createReadStream(src);
+  readable.on('error', runCb);
+  // Create write stream
+  let writable = fs.createWriteStream(dst);
+  writable.on('error', (err)=>{
+    readable.destroy();
+    runCb(err);
+  });
+  writable.on('close', () => runCb());
+  // Copy file via piping streams.
+  readable.pipe(writable);
+};
+
+/**
+ * moveFile: moves the file from src to dst.
+ * Firstly trying to rename the file if no luck copying it to dst and then deleteing src.
+ * @param {string} src - Path to the source file
+ * @param {string} dst - Path to the destination file.
+ * @param {Function} callback - A callback function.
+ */
+const moveFile = (src, dst, callback) => fs.rename(src, dst, err => (err
+  ? copyFile(src, dst, err => err ? callback(err) : deleteFile(src, callback))
+  : callback()
+));
+
+/**
+ * Save buffer data to a file.
+ * @param {Buffer} buffer - buffer to save to a file.
+ * @param {string} filePath - path to a file.
+ */
+const saveBufferToFile = (buffer, filePath, callback) => {
+  if (!Buffer.isBuffer(buffer)) {
+    return callback(new Error('buffer variable should be type of Buffer!'));
+  }
+  // Setup readable stream from buffer.
+  let streamData = buffer;
+  let readStream = Readable();
+  readStream._read = () => {
+    readStream.push(streamData);
+    streamData = null;
+  };
+  // Setup file system writable stream.
+  let fstream = fs.createWriteStream(filePath);
+  // console.log("Calling saveBuffer");
+  fstream.on('error', err => {
+    // console.log("err cb")
+    callback(err);
+  });
+  fstream.on('close', () => {
+    // console.log("close cb");
+    callback();
+  });
+  // Copy file via piping streams.
+  readStream.pipe(fstream);
+};
+
+/**
+ * Decodes uriEncoded file names.
+ * @param fileName {String} - file name to decode.
+ * @returns {String}
+ */
+const uriDecodeFileName = (opts, fileName) => {
+  return opts.uriDecodeFileNames ? decodeURIComponent(fileName) : fileName;
+};
+
+/**
+ * Parses filename and extension and returns object {name, extension}.
+ * @param {boolean|integer} preserveExtension - true/false or number of characters for extension.
+ * @param {string} fileName - file name to parse.
+ * @returns {Object} - { name, extension }.
+ */
+const parseFileNameExtension = (preserveExtension, fileName) => {
+  const preserveExtensionLength = parseInt(preserveExtension);
+  const result = {name: fileName, extension: ''};
+  if (!preserveExtension && preserveExtensionLength !== 0) return result;
+  // Define maximum extension length
+  const maxExtLength = isNaN(preserveExtensionLength)
+    ? MAX_EXTENSION_LENGTH
+    : Math.abs(preserveExtensionLength);
+
+  const nameParts = fileName.split('.');
+  if (nameParts.length < 2) return result;
+
+  let extension = nameParts.pop();
+  if (
+    extension.length > maxExtLength &&
+    maxExtLength > 0
+  ) {
+    nameParts[nameParts.length - 1] +=
+      '.' +
+      extension.substr(0, extension.length - maxExtLength);
+    extension = extension.substr(-maxExtLength);
+  }
+
+  result.extension = maxExtLength ? extension : '';
+  result.name = nameParts.join('.');
+  return result;
+};
+
+/**
+ * Parse file name and extension.
+ * @param {Object} opts - middleware options.
+ * @param {string} fileName - Uploaded file name.
+ * @returns {string}
+ */
+const parseFileName = (opts, fileName) => {
+  // Check fileName argument
+  if (!fileName || typeof fileName !== 'string') return getTempFilename();
+  // Cut off file name if it's lenght more then 255.
+  let parsedName = fileName.length <= 255 ? fileName : fileName.substr(0, 255);
+  // Decode file name if uriDecodeFileNames option set true.
+  parsedName = uriDecodeFileName(opts, parsedName);
+  // Stop parsing file name if safeFileNames options hasn't been set.
+  if (!opts.safeFileNames) return parsedName;
+  // Set regular expression for the file name.
+  const nameRegex = typeof opts.safeFileNames === 'object' && opts.safeFileNames instanceof RegExp
+    ? opts.safeFileNames
+    : SAFE_FILE_NAME_REGEX;
+  // Parse file name extension.
+  let {name, extension} = parseFileNameExtension(opts.preserveExtension, parsedName);
+  if (extension.length) extension = '.' + extension.replace(nameRegex, '');
+
+  return name.replace(nameRegex, '').concat(extension);
+};
+
+module.exports = {
+  isFunc,
+  debugLog,
+  copyFile, // For testing purpose.
+  moveFile,
+  errorFunc,
+  deleteFile, // For testing purpose.
+  buildFields,
+  buildOptions,
+  parseFileName,
+  getTempFilename,
+  promiseCallback,
+  checkAndMakeDir,
+  saveBufferToFile,
+  uriDecodeFileName,
+  isSafeFromPollution
+};

+ 78 - 0
backend/node_modules/express-fileupload/test/fileFactory.spec.js

@@ -0,0 +1,78 @@
+'use strict';
+
+const fs = require('fs');
+const md5 = require('md5');
+const path = require('path');
+const assert = require('assert');
+const server = require('./server');
+const {isFunc} = require('../lib/utilities');
+const fileFactory = require('../lib/fileFactory');
+
+const mockFileName = 'basketball.png';
+const mockFile = path.join(server.fileDir, mockFileName);
+const mockBuffer = fs.readFileSync(mockFile);
+const mockMd5 = md5(mockBuffer);
+
+const mockFileOpts = {
+  name: mockFileName,
+  buffer: mockBuffer,
+  encoding: 'utf-8',
+  mimetype: 'image/png',
+  hash: mockMd5,
+  tempFilePath: mockFile
+};
+
+describe('Test of the fileFactory factory', function() {
+  beforeEach(() => server.clearUploadsDir());
+
+  it('return a file object', () => assert.ok(fileFactory(mockFileOpts)));
+
+  describe('Properties', function() {
+    it('contains the name property', () => {
+      assert.equal(fileFactory(mockFileOpts).name, mockFileName);
+    });
+    it('contains the data property', () => assert.ok(fileFactory(mockFileOpts).data));
+    it('contains the encoding property', () => {
+      assert.equal(fileFactory(mockFileOpts).encoding, 'utf-8');
+    });
+    it('contains the mimetype property', () => {
+      assert.equal(fileFactory(mockFileOpts).mimetype, 'image/png');
+    });
+    it('contains the md5 property', () => assert.equal(fileFactory(mockFileOpts).md5, mockMd5));
+    it('contains the mv method', () => assert.equal(isFunc(fileFactory(mockFileOpts).mv), true));
+  });
+
+  describe('File object behavior for in memory upload', function() {
+    const file = fileFactory(mockFileOpts);
+    it('move the file to the specified folder', (done) => {
+      file.mv(path.join(server.uploadDir, mockFileName), (err) => {
+        assert.ifError(err);
+        done();
+      });
+    });
+    it('reject the mv if the destination does not exists', (done) => {
+      file.mv(path.join(server.uploadDir, 'unknown', mockFileName), (err) => {
+        assert.ok(err);
+        done();
+      });
+    });
+  });
+
+  describe('File object behavior for upload into temporary file', function() {
+    const file = fileFactory(mockFileOpts, { useTempFiles: true });
+    it('move the file to the specified folder', (done) => {
+      file.mv(path.join(server.uploadDir, mockFileName), (err) => {
+        assert.ifError(err);
+        // Place back moved file.
+        fs.renameSync(path.join(server.uploadDir, mockFileName), mockFile);
+        done();
+      });
+    });
+    it('reject the mv if the destination does not exists', (done) => {
+      file.mv(path.join(server.uploadDir, 'unknown', mockFileName), (err) => {
+        assert.ok(err);
+        done();
+      });
+    });
+  });
+});

+ 95 - 0
backend/node_modules/express-fileupload/test/fileLimitUploads.spec.js

@@ -0,0 +1,95 @@
+'use strict';
+
+const path = require('path');
+const request = require('supertest');
+const assert = require('assert');
+const server = require('./server');
+const clearUploadsDir = server.clearUploadsDir;
+const fileDir = server.fileDir;
+
+describe('Test Single File Upload With File Size Limit', function() {
+  let app, limitHandlerRun;
+
+  beforeEach(function() {
+    clearUploadsDir();
+  });
+
+  describe('abort connection on limit reached', function() {
+    before(function() {
+      app = server.setup({
+        limits: {fileSize: 200 * 1024}, // set 200kb upload limit
+        abortOnLimit: true
+      });
+    });
+
+    it(`upload 'basketball.png' (~154kb) with 200kb size limit`, function(done) {
+      let filePath = path.join(fileDir, 'basketball.png');
+
+      request(app)
+        .post('/upload/single/truncated')
+        .attach('testFile', filePath)
+        .expect(200)
+        .end(done);
+    });
+
+    it(`fail when uploading 'car.png' (~269kb) with 200kb size limit`, function(done) {
+      let filePath = path.join(fileDir, 'car.png');
+
+      request(app)
+        .post('/upload/single/truncated')
+        .attach('testFile', filePath)
+        .expect(413)
+        .end(done);
+    });
+  });
+
+  describe('Run limitHandler on limit reached.', function(){
+    before(function() {
+      app = server.setup({
+        limits: {fileSize: 200 * 1024},     // set 200kb upload limit
+        limitHandler: (req, res) => { // set limit handler
+          res.writeHead(500, { Connection: 'close', 'Content-Type': 'application/json'});
+          res.end(JSON.stringify({response: 'Limit reached!'}));
+          limitHandlerRun = true;
+        }
+      });
+    });
+
+    it(`Run limit handler when uploading 'car.png' (~269kb) with 200kb size limit`, function(done) {
+      let filePath = path.join(fileDir, 'car.png');
+      limitHandlerRun = false;
+
+      request(app)
+        .post('/upload/single/truncated')
+        .attach('testFile', filePath)
+        .expect(500, {response: 'Limit reached!'})
+        .end(function(err){
+          if (err) return done(err);
+          if (!limitHandlerRun) return done('handler did not run');
+          done();
+        });
+    });
+
+  });
+
+  describe('pass truncated file to the next handler', function() {
+    before(function() {
+      app = server.setup({
+        limits: {fileSize: 200 * 1024} // set 200kb upload limit
+      });
+    });
+
+    it(`fail when uploading 'car.png' (~269kb) with 200kb size limit`, function(done) {
+      let filePath = path.join(fileDir, 'car.png');
+
+      request(app)
+        .post('/upload/single/truncated')
+        .attach('testFile', filePath)
+        .expect(400)
+        .end(function(err, res) {
+          assert.ok(res.error.text === 'File too big');
+          done();
+        });
+    });
+  });
+});

+ 85 - 0
backend/node_modules/express-fileupload/test/multipartFields.spec.js

@@ -0,0 +1,85 @@
+'use strict';
+
+const request = require('supertest');
+const server = require('./server');
+const app = server.setup();
+
+let mockUser = {
+  firstName: 'Joe',
+  lastName: 'Schmo',
+  email: 'joe@mailinator.com'
+};
+
+let mockCars = [
+  'rsx',
+  'tsx',
+  'civic',
+  'integra'
+];
+
+describe('Test Multipart Form Single Field Submissions', function() {
+  it('submit multipart user data with POST', function(done) {
+    request(app)
+      .post('/fields/user')
+      .field('firstName', mockUser.firstName)
+      .field('lastName', mockUser.lastName)
+      .field('email', mockUser.email)
+      .expect('Content-Type', /json/)
+      .expect(200, {
+        firstName: mockUser.firstName,
+        lastName: mockUser.lastName,
+        email: mockUser.email
+      }, done);
+  });
+
+  it('submit multipart user data with PUT', function(done) {
+    request(app)
+      .post('/fields/user')
+      .field('firstName', mockUser.firstName)
+      .field('lastName', mockUser.lastName)
+      .field('email', mockUser.email)
+      .expect('Content-Type', /json/)
+      .expect(200, {
+        firstName: mockUser.firstName,
+        lastName: mockUser.lastName,
+        email: mockUser.email
+      }, done);
+  });
+
+  it('fail when user data submitted without multipart', function(done) {
+    request(app)
+      .post('/fields/user')
+      .send(mockUser)
+      .expect(400)
+      .end(done);
+  });
+
+  it('fail when user data not submitted', function(done) {
+    request(app)
+      .post('/fields/user')
+      .expect(400)
+      .end(done);
+  });
+});
+
+describe('Test Multipart Form Array Field Submissions', function() {
+  it('submit array of data with POST', function(done) {
+    let req = request(app).post('/fields/array');
+
+    for (let i = 0; i < mockCars.length; i++) {
+      req.field('testField', mockCars[i]);
+    }
+
+    req
+      .expect(200)
+      .end(function(err, res) {
+        if (err) {
+          return done(err);
+        }
+
+        let responseMatchesRequest = res.body.join(',') === mockCars.join(',');
+
+        done(responseMatchesRequest ? null : 'Data was returned as expected.');
+      });
+  });
+});

+ 451 - 0
backend/node_modules/express-fileupload/test/multipartUploads.spec.js

@@ -0,0 +1,451 @@
+'use strict';
+
+const fs = require('fs');
+const md5 = require('md5');
+const path = require('path');
+const request = require('supertest');
+const server = require('./server');
+
+const fileDir = server.fileDir;
+const tempDir = server.tempDir;
+const uploadDir = server.uploadDir;
+const clearTempDir = server.clearTempDir;
+const clearUploadsDir = server.clearUploadsDir;
+
+const mockFiles = ['car.png', 'tree.png', 'basketball.png', 'emptyfile.txt'];
+
+const mockUser = {
+  firstName: 'Joe',
+  lastName: 'Schmo',
+  email: 'joe@mailinator.com'
+};
+
+// Reset response body.uploadDir/uploadPath for testing.
+const resetBodyUploadData = (res) => {
+  res.body.uploadDir = '';
+  res.body.uploadPath = '';
+};
+
+const genUploadResult = (fileName, filePath) => {
+  const fileStat = fs.statSync(filePath);
+  const fileBuffer = fs.readFileSync(filePath);
+  return {
+    name: fileName,
+    md5: md5(fileBuffer),
+    size: fileStat.size,
+    uploadDir: '',
+    uploadPath: ''
+  };
+};
+
+describe('Test Directory Cleaning Method', function() {
+  it('emptied "uploads" directory', function(done) {
+    clearUploadsDir();
+    const filesFound = fs.readdirSync(uploadDir).length;
+    done(filesFound ? `Directory not empty. Found ${filesFound} files.` : null);
+  });
+});
+
+describe('Test Single File Upload', function() {
+  const app = server.setup();
+
+  mockFiles.forEach((fileName) => {
+    const filePath = path.join(fileDir, fileName);
+    const uploadedFilePath = path.join(uploadDir, fileName);
+    const result = genUploadResult(fileName, filePath);
+
+    it(`upload ${fileName} with POST`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single')
+        .attach('testFile', filePath)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });
+
+    it(`upload ${fileName} with PUT`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single')
+        .attach('testFile', filePath)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });    
+  });
+
+  it('fail when no files were attached', function(done) {
+    request(app)
+      .post('/upload/single')
+      .expect(400)
+      .end(done);
+  });
+
+  it('fail when using GET', function(done) {
+    request(app)
+      .get('/upload/single')
+      .attach('testFile', path.join(fileDir, mockFiles[0]))
+      .expect(400)
+      .end(done);
+  });
+
+  it('fail when using HEAD', function(done) {
+    request(app)
+      .head('/upload/single')
+      .attach('testFile', path.join(fileDir, mockFiles[0]))
+      .expect(400)
+      .end(done);
+  });
+});
+
+describe('Test Single File Upload w/ .mv()', function() {
+  const app = server.setup();
+
+  mockFiles.forEach((fileName) => {
+    const filePath = path.join(fileDir, fileName);
+    const uploadedFilePath = path.join(uploadDir, fileName);
+    const result = genUploadResult(fileName, filePath);
+
+    it(`upload ${fileName} with POST w/ .mv()`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single')
+        .attach('testFile', filePath)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });
+
+    it(`upload ${fileName} with PUT w/ .mv()`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single')
+        .attach('testFile', filePath)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });
+  });
+});
+
+describe('Test Single File Upload with useTempFiles option.', function() {
+  const app = server.setup({ useTempFiles: true, tempFileDir: tempDir });
+
+  mockFiles.forEach((fileName) => {
+    const filePath = path.join(fileDir, fileName);
+    const uploadedFilePath = path.join(uploadDir, fileName);
+    const result = genUploadResult(fileName, filePath);
+    
+    it(`upload ${fileName} with POST`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single')
+        .attach('testFile', filePath)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });
+
+    it(`upload ${fileName} with PUT`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single')
+        .attach('testFile', filePath)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });    
+  });
+
+  it('fail when no files were attached', function(done) {
+    request(app)
+      .post('/upload/single')
+      .expect(400)
+      .end(done);
+  });
+
+  it('fail when using GET', function(done) {
+    request(app)
+      .get('/upload/single')
+      .attach('testFile', path.join(fileDir, mockFiles[0]))
+      .expect(400)
+      .end(done);
+  });
+
+  it('fail when using HEAD', function(done) {
+    request(app)
+      .head('/upload/single')
+      .attach('testFile', path.join(fileDir, mockFiles[0]))
+      .expect(400)
+      .end(done);
+  });
+});
+
+describe('Test Single File Upload with useTempFiles option and empty tempFileDir.', function() {
+  const app = server.setup({ useTempFiles: true, tempFileDir: '' });
+
+  mockFiles.forEach((fileName) => {
+    const filePath = path.join(fileDir, fileName);
+    const uploadedFilePath = path.join(uploadDir, fileName);
+    const result = genUploadResult(fileName, filePath);
+
+    it(`upload ${fileName} with POST`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single')
+        .attach('testFile', filePath)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });    
+  });
+});
+
+describe('Test Single File Upload w/ .mv() Promise', function() {
+  const app = server.setup();
+
+  mockFiles.forEach((fileName) => {
+    const filePath = path.join(fileDir, fileName);
+    const uploadedFilePath = path.join(uploadDir, fileName);
+    const result = genUploadResult(fileName, filePath);
+    
+    it(`upload ${fileName} with POST w/ .mv() Promise`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single/promise')
+        .attach('testFile', filePath)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });
+
+    it(`upload ${fileName} with PUT w/ .mv() Promise`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single/promise')
+        .attach('testFile', filePath)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });    
+  });
+
+  it('fail when no files were attached', function(done) {
+    request(app)
+      .post('/upload/single')
+      .expect(400)
+      .end(done);
+  });
+
+  it('fail when using GET', function(done) {
+    request(app)
+      .get('/upload/single')
+      .attach('testFile', path.join(fileDir, mockFiles[0]))
+      .expect(400)
+      .end(done);
+  });
+
+  it('fail when using HEAD', function(done) {
+    request(app)
+      .head('/upload/single')
+      .attach('testFile', path.join(fileDir, mockFiles[0]))
+      .expect(400)
+      .end(done);
+  });
+});
+
+describe('Test Single File Upload w/ .mv() Promise and useTempFiles set to true', function() {
+  const app = server.setup({ useTempFiles: true, tempFileDir: tempDir });
+
+  mockFiles.forEach((fileName) => {
+    const filePath = path.join(fileDir, fileName);
+    const uploadedFilePath = path.join(uploadDir, fileName);
+    const result = genUploadResult(fileName, filePath);
+    
+    it(`upload ${fileName} with POST w/ .mv() Promise`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single/promise')
+        .attach('testFile', filePath)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });
+
+    it(`upload ${fileName} with PUT w/ .mv() Promise`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single/promise')
+        .attach('testFile', filePath)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });    
+  });
+
+  it('fail when no files were attached', (done) => {
+    request(app)
+      .post('/upload/single')
+      .expect(400)
+      .end(done);
+  });
+
+  it('fail when using GET', (done) => {
+    request(app)
+      .get('/upload/single')
+      .attach('testFile', path.join(fileDir, mockFiles[0]))
+      .expect(400)
+      .end(done);
+  });
+
+  it('fail when using HEAD', (done) => {
+    request(app)
+      .head('/upload/single')
+      .attach('testFile', path.join(fileDir, mockFiles[0]))
+      .expect(400)
+      .end(done);
+  });
+});
+
+describe('Test Multi-File Upload', function() {
+  const app = server.setup();
+
+  it('upload multiple files with POST', (done) => {
+    clearUploadsDir();
+    const req = request(app).post('/upload/multiple');
+    const expectedResult = [];
+    const expectedResultSorted = [];
+    const uploadedFilesPath = [];
+    mockFiles.forEach((fileName, index) => {
+      const filePath = path.join(fileDir, fileName);
+      req.attach(`testFile${index + 1}`, filePath);
+      uploadedFilesPath.push(path.join(uploadDir, fileName));
+      expectedResult.push(genUploadResult(fileName, filePath));
+    });
+
+    req
+      .expect((res) => {
+        res.body.forEach((fileInfo) => {
+          fileInfo.uploadDir = '';
+          fileInfo.uploadPath = '';
+          const index = mockFiles.indexOf(fileInfo.name);
+          expectedResultSorted.push(expectedResult[index]);
+        });
+      })
+      .expect(200, expectedResultSorted)
+      .end((err) => {
+        if (err) return done(err);
+        fs.stat(uploadedFilesPath[0], (err) => {
+          if (err) return done(err);
+          fs.stat(uploadedFilesPath[1], (err) => {
+            if (err) return done(err);
+            fs.stat(uploadedFilesPath[2], done);
+          });
+        });
+      });
+  });
+});
+
+describe('Test File Array Upload', function() {
+  const app = server.setup();
+
+  it('upload array of files with POST', (done) => {
+    clearUploadsDir();
+    const req = request(app).post('/upload/array');
+    const expectedResult = [];
+    const expectedResultSorted = [];
+    const uploadedFilesPath = [];
+    mockFiles.forEach((fileName) => {
+      const filePath = path.join(fileDir, fileName);
+      uploadedFilesPath.push(path.join(uploadDir, fileName));
+      expectedResult.push(genUploadResult(fileName, filePath));
+      req.attach('testFiles', filePath);
+    });
+
+    req
+      .expect((res)=>{
+        res.body.forEach((fileInfo) => {
+          fileInfo.uploadDir = '';
+          fileInfo.uploadPath = '';
+          const index = mockFiles.indexOf(fileInfo.name);
+          expectedResultSorted.push(expectedResult[index]);
+        });
+      })
+      .expect(200, expectedResultSorted)
+      .end((err) => {
+        if (err) return done(err);
+        uploadedFilesPath.forEach((uploadedFilePath) => {
+          fs.statSync(uploadedFilePath);
+        });
+        done();
+      });
+  });
+});
+
+describe('Test Upload With Fields', function() {
+  const app = server.setup();
+  mockFiles.forEach((fileName) => {
+    const filePath = path.join(fileDir, fileName);
+    const uploadedFilePath = path.join(uploadDir, fileName);
+    // Expected results
+    const result = genUploadResult(fileName, filePath);
+    result.firstName = mockUser.firstName;
+    result.lastName = mockUser.lastName;
+    result.email = mockUser.email;
+
+    it(`upload ${fileName} and submit fields at the same time with POST`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .post('/upload/single/withfields')
+        .attach('testFile', filePath)
+        .field('firstName', mockUser.firstName)
+        .field('lastName', mockUser.lastName)
+        .field('email', mockUser.email)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });
+
+    it(`upload ${fileName} and submit fields at the same time with PUT`, function(done) {
+      clearUploadsDir();
+      request(app)
+        .put('/upload/single/withfields')
+        .attach('testFile', filePath)
+        .field('firstName', mockUser.firstName)
+        .field('lastName', mockUser.lastName)
+        .field('email', mockUser.email)
+        .expect(resetBodyUploadData)
+        .expect(200, result, err => (err ? done(err) : fs.stat(uploadedFilePath, done)));
+    });    
+  });
+});
+
+describe('Test Aborting/Canceling during upload', function() {
+  this.timeout(4000); // Set timeout for async tests.
+  const uploadTimeout = 1000;
+
+  const app = server.setup({
+    useTempFiles: true,
+    tempFileDir: tempDir,
+    debug: true,
+    uploadTimeout
+  });
+
+  clearTempDir();
+  clearUploadsDir();
+  mockFiles.forEach((fileName) => {
+    const filePath = path.join(fileDir, fileName);
+
+    it(`Delete temp file if ${fileName} upload was aborted`, (done) => {
+      const req = request(app)
+        .post('/upload/single')
+        .attach('testFile', filePath)
+        .on('progress', (e) => {
+          const progress = (e.loaded * 100) / e.total;
+          // Aborting request, use req.req since it is original superagent request.
+          if (progress > 50) req.req.abort();
+        })
+        .end((err) => {
+          if (!err) return done(`Connection hasn't been aborted!`);
+          if (err.code !== 'ECONNRESET') return done(err);
+          // err.code === 'ECONNRESET' that means upload has been aborted.
+          // Checking temp directory after upload timeout.
+          setTimeout(() => {
+            fs.readdir(tempDir, (err, files) => {
+              if (err) return done(err);
+              return files.length ? done(`Temporary directory contains files!`) : done();
+            });
+          }, uploadTimeout * 2);
+        });
+    });
+  });
+});

+ 219 - 0
backend/node_modules/express-fileupload/test/options.spec.js

@@ -0,0 +1,219 @@
+const fs = require('fs');
+const path = require('path');
+const request = require('supertest');
+const server = require('./server');
+const clearUploadsDir = server.clearUploadsDir;
+const fileDir = server.fileDir;
+const uploadDir = server.uploadDir;
+
+describe('File Upload Options Tests', function() {
+  afterEach(function(done) {
+    clearUploadsDir();
+    done();
+  });
+
+  /**
+   * Upload the file for testing and verify the expected filename.
+   * @param {object} options The expressFileUpload options.
+   * @param {string} actualFileNameToUpload The name of the file to upload.
+   * @param {string} expectedFileNameOnFileSystem The name of the file after upload.
+   * @param {function} done The mocha continuation function.
+   */
+  function executeFileUploadTestWalk(options,
+    actualFileNameToUpload,
+    expectedFileNameOnFileSystem,
+    done) {
+    request(server.setup(options))
+      .post('/upload/single')
+      .attach('testFile', path.join(fileDir, actualFileNameToUpload))
+      .expect(200)
+      .end(function(err) {
+        if (err) {
+          return done(err);
+        }
+
+        const uploadedFilePath = path.join(uploadDir, expectedFileNameOnFileSystem);
+
+        fs.stat(uploadedFilePath, done);
+      });
+  }
+
+  describe('Testing [safeFileNames] option to ensure:', function() {
+    it('Does nothing to your filename when disabled.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: false};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'my$Invalid#fileName.png123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Is disabled by default.',
+      function(done) {
+        const fileUploadOptions = null;
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'my$Invalid#fileName.png123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Strips away all non-alphanumeric characters (excluding hyphens/underscores) when enabled.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileNamepng123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Accepts a regex for stripping (decidedly) "invalid" characters from filename.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: /[$#]/g};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileName.png123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+  });
+
+  describe('Testing [preserveExtension] option to ensure:', function() {
+    it('Does not preserve the extension of your filename when disabled.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true, preserveExtension: false};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileNamepng123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Is disabled by default.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileNamepng123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Shortens your extension to the default(3) when enabled, if the extension found is larger.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true, preserveExtension: true};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileNamepng.123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Leaves your extension alone when enabled, if the extension found is <= default(3) length',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true, preserveExtension: true};
+        const actualFileName = 'car.png';
+        const expectedFileName = 'car.png';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Can be configured for an extension length > default(3).',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true, preserveExtension: 7};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileName.png123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Can be configured for an extension length < default(3).',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true, preserveExtension: 2};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileNamepng1.23';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Will use the absolute value of your extension length when negative.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true, preserveExtension: -5};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileNamep.ng123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Will leave no extension when the extension length == 0.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true, preserveExtension: 0};
+        const actualFileName = 'car.png';
+        const expectedFileName = 'car';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Will accept numbers as strings, if they can be resolved with parseInt.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true, preserveExtension: '3'};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileNamepng.123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Will be evaluated for truthy-ness if it cannot be parsed as an int.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true, preserveExtension: 'not-a-#-but-truthy'};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileNamepng.123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Will ignore any decimal amount when evaluating for extension length.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true, preserveExtension: 4.98};
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileNamepn.g123';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+
+    it('Only considers the last dotted part as the extension.',
+      function(done) {
+        const fileUploadOptions = {safeFileNames: true, preserveExtension: true};
+        const actualFileName = 'basket.ball.bp';
+        const expectedFileName = 'basketball.bp';
+
+        executeFileUploadTestWalk(fileUploadOptions, actualFileName, expectedFileName, done);
+      });
+  });
+
+  describe('Testing [parseNested] option to ensure:', function() {
+    it('When [parseNested] is enabled result are nested', function(done){
+      const app = server.setup({parseNested: true});
+      request(app)
+        .post('/fields/nested')
+        .field('name', 'John')
+        .field('hobbies[0]', 'Cinema')
+        .field('hobbies[1]', 'Bike')
+        .expect('Content-Type', /json/)
+        .expect(200, {
+          name: 'John',
+          hobbies: ['Cinema', 'Bike']
+        }, done);
+    });
+
+    it('When [parseNested] is disabled are flattened', function(done){
+      const app = server.setup({parseNested: false});
+      request(app)
+        .post('/fields/flattened')
+        .field('name', 'John')
+        .field('hobbies[0]', 'Cinema')
+        .field('hobbies[1]', 'Bike')
+        .expect('Content-Type', /json/)
+        .expect(200, {
+          name: 'John',
+          'hobbies[0]': 'Cinema',
+          'hobbies[1]': 'Bike'
+        }, done);
+    });
+  });
+});

+ 59 - 0
backend/node_modules/express-fileupload/test/processNested.spec.js

@@ -0,0 +1,59 @@
+'use strict';
+
+const assert = require('assert');
+const processNested = require('../lib/processNested');
+
+describe('Test Convert Flatten object to Nested object', function() {
+  it('With no nested data', () => {
+    const data = {
+        'firstname': 'John',
+        'lastname': 'Doe',
+        'age': 22
+      },
+      excerpt = { firstname: 'John', lastname: 'Doe', age: 22 },
+      processed = processNested(data);
+
+    assert.deepEqual(processed, excerpt);
+  });
+
+  it('With nested data', () => {
+    const data = {
+        'firstname': 'John',
+        'lastname': 'Doe',
+        'age': 22,
+        'hobbies[0]': 'Cinema',
+        'hobbies[1]': 'Bike',
+        'address[line]': '78  Lynch Street',
+        'address[city]': 'Milwaukee',
+        'friends[0][name]': 'Jane',
+        'friends[0][lastname]': 'Doe',
+        'friends[1][name]': 'Joe',
+        'friends[1][lastname]': 'Doe'
+      },
+      excerpt = {
+        firstname: 'John',
+        lastname: 'Doe',
+        age: 22,
+        hobbies: [ 'Cinema', 'Bike' ],
+        address: { line: '78  Lynch Street', city: 'Milwaukee' },
+        friends: [
+          { name: 'Jane', lastname: 'Doe' },
+          { name: 'Joe', lastname: 'Doe' }
+        ]
+      },
+      processed = processNested(data);
+
+    assert.deepEqual(processed, excerpt);
+  });
+
+  it('Do not allow prototype pollution', () => {
+    const pollutionOb1 = JSON.parse(`{"__proto__.POLLUTED1": "FOOBAR"}`);
+    const pollutionOb2 = JSON.parse(`{"constructor.prototype.POLLUTED2": "FOOBAR"}`);
+
+    processNested(pollutionOb1);
+    processNested(pollutionOb2);
+
+    assert.equal(global.POLLUTED1, undefined);
+    assert.equal(global.POLLUTED2, undefined);
+  });
+});

+ 275 - 0
backend/node_modules/express-fileupload/test/server.js

@@ -0,0 +1,275 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const rimraf = require('rimraf');
+
+const fileDir = path.join(__dirname, 'files');
+const tempDir = path.join(__dirname, 'temp');
+const uploadDir = path.join(__dirname, 'uploads');
+
+const clearDir = (dir) => {
+  try {
+    if (fs.existsSync(dir)) rimraf.sync(dir);
+    fs.mkdirSync(dir, { recursive: true });
+  } catch (err) {
+    // 
+  }
+};
+
+const clearUploadsDir = () => clearDir(uploadDir);
+const clearTempDir = () => clearDir(tempDir);
+
+const getUploadedFileData = (file) => ({
+  md5: file.md5,
+  name: file.name,
+  size: file.size,
+  uploadPath: path.join(uploadDir, file.name),
+  uploadDir: uploadDir
+});
+
+const setup = (fileUploadOptions) => {
+  const express = require('express');
+  const expressFileupload = require('../lib/index');
+
+  const app = express();
+
+  app.use(expressFileupload(fileUploadOptions || {}));
+
+  app.all('/upload/single', (req, res) => {
+    if (!req.files) {
+      return res.status(400).send('No files were uploaded.');
+    }
+
+    const testFile = req.files.testFile;
+    const fileData = getUploadedFileData(testFile);
+
+    testFile.mv(fileData.uploadPath, (err) => {
+      if (err) {
+        console.log('ERR', err); // eslint-disable-line
+        return res.status(500).send(err);
+      }
+      res.json(fileData);
+    });
+  });
+
+  app.all('/upload/single/promise', (req, res) => {
+    if (!req.files) {
+      return res.status(400).send('No files were uploaded.');
+    }
+
+    const testFile = req.files.testFile;
+    const fileData = getUploadedFileData(testFile);
+
+    testFile
+      .mv(fileData.uploadPath)
+      .then(() => {
+        res.json(fileData);
+      })
+      .catch(err => {
+        res.status(500).send(err);
+      });
+  });
+
+  app.all('/upload/single/withfields', (req, res) => {
+    if (!req.files) {
+      return res.status(400).send('No files were uploaded.');
+    }
+
+    if (!req.body) {
+      return res.status(400).send('No request body found');
+    }
+    
+    const fields = ['firstName', 'lastName', 'email'];
+    for (let i = 0; i < fields.length; i += 1) {
+      if (!req.body[fields[i]] || !req.body[fields[i]].trim()) {
+        return res.status(400).send(`Invalid field: ${fields[i]}`);
+      }
+    }
+
+    const testFile = req.files.testFile;
+    const fileData = getUploadedFileData(testFile);
+    fields.forEach((field) => { fileData[field] = req.body[field]; });
+
+    testFile.mv(fileData.uploadPath, (err) => {
+      if (err) {
+        return res.status(500).send(err);
+      }
+      res.json(fileData);
+    });
+  });
+
+  app.all('/upload/single/truncated', (req, res) => {
+    if (!req.files) {
+      return res.status(400).send('No files were uploaded.');
+    }
+
+    // status 400 to differentiate from ending the request in the on limit
+    return req.files.testFile.truncated
+      ? res.status(400).send(`File too big`)
+      : res.status(200).send('Upload succeed');
+  });
+
+  app.all('/upload/multiple', function(req, res) {
+    if (!req.files) {
+      return res.status(400).send('No files were uploaded.');
+    }
+
+    const fileNames = ['testFile1', 'testFile2', 'testFile3'];
+    
+    const testFiles = fileNames.map(file => req.files[file]);
+    for (let i = 0; i < testFiles.length; i += 1) {
+      if (!testFiles[i]) {
+        return res.status(400).send(`${fileNames[i]} was not uploaded!`);
+      }
+    }
+    
+    const filesData = testFiles.map(file => getUploadedFileData(file));
+
+    testFiles[0].mv(filesData[0].uploadPath, (err) => {
+      if (err) {
+        return res.status(500).send(err);
+      }
+
+      testFiles[1].mv(filesData[1].uploadPath, (err) => {
+        if (err) {
+          return res.status(500).send(err);
+        }
+
+        testFiles[2].mv(filesData[2].uploadPath, (err) => {
+          if (err) {
+            return res.status(500).send(err);
+          }
+
+          res.json(filesData);
+        });
+      });
+    });
+  });
+
+  app.all('/upload/array', function(req, res) {
+    if (!req.files) {
+      return res.status(400).send('No files were uploaded.');
+    }
+
+    const testFiles = req.files.testFiles;
+
+    if (!testFiles) {
+      return res.status(400).send('No files were uploaded');
+    }
+
+    if (!Array.isArray(testFiles)) {
+      return res.status(400).send('Files were not uploaded as an array');
+    }
+
+    if (!testFiles.length) {
+      return res.status(400).send('Files array is empty');
+    }
+
+    const filesData = testFiles.map(file => getUploadedFileData(file));
+
+    let uploadCount = 0;
+    for (let i = 0; i < testFiles.length; i += 1) {
+
+      testFiles[i].mv(filesData[i].uploadPath, (err) => {
+        if (err) {
+          return res.status(500).send(err);
+        }
+
+        uploadCount += 1;
+        if (uploadCount === testFiles.length) {
+          res.json(filesData);
+        }
+      });
+    }
+  });
+
+  app.all('/fields/user', function(req, res) {
+    if (!req.body) {
+      return res.status(400).send('No request body found');
+    }
+
+    const fields = ['firstName', 'lastName', 'email'];
+    for (let i = 0; i < fields.length; i += 1) {
+      if (!req.body[fields[i]] || !req.body[fields[i]].trim()) {
+        return res.status(400).send(`Invalid field: ${fields[i]}`);
+      }
+    }
+
+    res.json({
+      firstName: req.body.firstName,
+      lastName: req.body.lastName,
+      email: req.body.email
+    });
+  });
+
+  app.all('/fields/nested', function(req, res) {
+    if (!req.body) {
+      return res.status(400).send('No request body found');
+    }
+
+    if (!req.body.name || !req.body.name.trim()) {
+      return res.status(400).send('Invalid name');
+    }
+
+    if (!req.body.hobbies || !req.body.hobbies.length == 2) {
+      return res.status(400).send('Invalid hobbies');
+    }
+
+    res.json({
+      name: req.body.name,
+      hobbies: req.body.hobbies
+    });
+  });
+
+  app.all('/fields/flattened', function(req, res) {
+    if (!req.body) {
+      return res.status(400).send('No request body found');
+    }
+
+    if (!req.body.name || !req.body.name.trim()) {
+      return res.status(400).send('Invalid name');
+    }
+
+    if (!req.body['hobbies[0]'] || !req.body['hobbies[0]'].trim()) {
+      return res.status(400).send('Invalid hobbies[0]');
+    }
+
+    if (!req.body['hobbies[1]'] || !req.body['hobbies[1]'].trim()) {
+      return res.status(400).send('Invalid hobbies[1]');
+    }
+
+    res.json({
+      name: req.body.name,
+      'hobbies[0]': req.body['hobbies[0]'],
+      'hobbies[1]': req.body['hobbies[1]']
+    });
+  });
+
+  app.all('/fields/array', function(req, res) {
+    if (!req.body) {
+      return res.status(400).send('No request body found');
+    }
+
+    if (!req.body.testField) {
+      return res.status(400).send('Invalid field');
+    }
+
+    if (!Array.isArray(req.body.testField)) {
+      return res.status(400).send('Field is not an array');
+    }
+
+    res.json(req.body.testField);
+  });
+
+  return app;
+};
+
+module.exports = {
+  setup,
+  fileDir,
+  tempDir,
+  uploadDir,
+  clearTempDir,
+  clearUploadsDir
+};

+ 132 - 0
backend/node_modules/express-fileupload/test/tempFile.spec.js

@@ -0,0 +1,132 @@
+const fs = require('fs');
+const md5 = require('md5');
+const path = require('path');
+const request = require('supertest');
+const server = require('./server');
+const clearUploadsDir =
+  server.clearUploadsDir;
+const fileDir =
+  server.fileDir;
+const uploadDir =
+  server.uploadDir;
+describe('File Upload Options Tests', function() {
+  afterEach(function(done) {
+    clearUploadsDir();
+    done();
+  });
+  /**
+   * Upload the file for testing and verify the expected filename.
+   * @param {object} options The expressFileUpload options.
+   * @param {string} actualFileNameToUpload The name of the file to upload.
+   * @param {string} expectedFileNameOnFileSystem The name of the file after upload.
+   * @param {function} done The mocha continuation function.
+   */
+  function executeFileUploadTestWalk(
+    options,
+    actualFileNameToUpload,
+    expectedFileNameOnFileSystem,
+    done
+  ) {
+
+    let filePath = path.join(fileDir, actualFileNameToUpload);
+    let fileBuffer = fs.readFileSync(filePath);
+    let fileHash = md5(fileBuffer);
+    let fileStat = fs.statSync(filePath);
+    let uploadedFilePath = path.join(uploadDir, expectedFileNameOnFileSystem);
+
+    request(
+      server.setup(options)
+    )
+      .post('/upload/single')
+      .attach('testFile', filePath)
+      .expect((res)=>{
+        res.body.uploadDir = '';
+        res.body.uploadPath = '';
+      })
+      .expect(200, {
+        name: expectedFileNameOnFileSystem,
+        md5: fileHash,
+        size: fileStat.size,
+        uploadDir: '',
+        uploadPath: ''
+      })
+      .end(function(err) {
+        if (err) {
+          return done(err);
+        }
+        
+        fs.stat(uploadedFilePath, done);
+      });
+  }
+  describe('Testing [safeFileNames with useTempFiles] option to ensure:', function() {
+    it('Does nothing to your filename when disabled.', function(done) {
+      const fileUploadOptions = {
+        safeFileNames: false,
+        useTempFiles: true,
+        tempFileDir: '/tmp/'
+      };
+      const actualFileName =
+        'my$Invalid#fileName.png123';
+      const expectedFileName =
+        'my$Invalid#fileName.png123';
+      executeFileUploadTestWalk(
+        fileUploadOptions,
+        actualFileName,
+        expectedFileName,
+        done
+      );
+    });
+    it('Is disabled by default.', function(done) {
+      const fileUploadOptions = {
+        useTempFiles: true,
+        tempFileDir: '/tmp/'
+      };
+      const actualFileName =
+        'my$Invalid#fileName.png123';
+      const expectedFileName =
+        'my$Invalid#fileName.png123';
+      executeFileUploadTestWalk(
+        fileUploadOptions,
+        actualFileName,
+        expectedFileName,
+        done
+      );
+    });
+
+    it(
+      'Strips away all non-alphanumeric characters (excluding hyphens/underscores) when enabled.', 
+      function(done) {
+        const fileUploadOptions = {
+          safeFileNames: true,
+          useTempFiles: true,
+          tempFileDir: '/tmp/'
+        };
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileNamepng123';
+        executeFileUploadTestWalk(
+          fileUploadOptions,
+          actualFileName,
+          expectedFileName,
+          done
+        );
+      });
+
+    it(
+      'Accepts a regex for stripping (decidedly) "invalid" characters from filename.', 
+      function(done) {
+        const fileUploadOptions = {
+          safeFileNames: /[$#]/g,
+          useTempFiles: true,
+          tempFileDir: '/tmp/'
+        };
+        const actualFileName = 'my$Invalid#fileName.png123';
+        const expectedFileName = 'myInvalidfileName.png123';
+        executeFileUploadTestWalk(
+          fileUploadOptions,
+          actualFileName,
+          expectedFileName,
+          done
+        );
+      });
+  });
+});

+ 28 - 0
backend/node_modules/express-fileupload/test/uploadtimer.spec.js

@@ -0,0 +1,28 @@
+'use strict';
+
+const assert = require('assert');
+const UploadTimer = require('../lib/uploadtimer');
+
+describe('Test UploadTimer class', () => {
+
+  it('It runs a callback function after specified timeout.', (done) => {
+    const uploadTimer = new UploadTimer(1000, done);
+    uploadTimer.set();
+  });
+
+  it('set method returns true if timeout specified.', () => {
+    const uploadTimer = new UploadTimer(1000);
+    assert.equal(uploadTimer.set(), true);
+  });
+
+  it('set method returns false if timeout has not specified.', () => {
+    const uploadTimer = new UploadTimer();
+    assert.equal(uploadTimer.set(), false);
+  });
+
+  it('set method returns false if zero timeout has specified.', () => {
+    const uploadTimer = new UploadTimer(0);
+    assert.equal(uploadTimer.set(), false);
+  });
+
+});

+ 468 - 0
backend/node_modules/express-fileupload/test/utilities.spec.js

@@ -0,0 +1,468 @@
+'use strict';
+
+const assert = require('assert');
+const path = require('path');
+const fs = require('fs');
+const md5 = require('md5');
+const server = require('./server');
+const fileDir = server.fileDir;
+const uploadDir = server.uploadDir;
+const {
+  debugLog,
+  isFunc,
+  errorFunc,
+  getTempFilename,
+  buildOptions,
+  buildFields,
+  checkAndMakeDir,
+  deleteFile,
+  copyFile,
+  saveBufferToFile,
+  parseFileName,
+  uriDecodeFileName,
+  isSafeFromPollution
+} = require('../lib/utilities');
+
+const mockFile = 'basketball.png';
+const mockBuffer = fs.readFileSync(path.join(fileDir, mockFile));
+const mockHash = md5(mockBuffer);
+
+
+describe('Test of the utilities functions', function() {
+  beforeEach(function() {
+    server.clearUploadsDir();
+  });
+  //debugLog tests
+  describe('Test debugLog function', () => {
+
+    let testMessage = 'Test message';
+
+    it('debugLog returns false if no options passed', () => {
+      assert.equal(debugLog(null, testMessage), false);
+    });
+
+    it('debugLog returns false if option debug is false', () => {
+      assert.equal(debugLog({debug: false}, testMessage), false);
+    });
+
+    it('debugLog returns true if option debug is true', () => {
+      assert.equal(debugLog({debug: true}, testMessage), true);
+    });
+
+  });
+  //isFunc tests
+  describe('Test isFunc function', () => {
+
+    it('isFunc returns true if function passed', () => assert.equal(isFunc(()=>{}), true));
+
+    it('isFunc returns false if null passed', function() {
+      assert.equal(isFunc(null), false);
+    });
+
+    it('isFunc returns false if undefined passed', function() {
+      assert.equal(isFunc(undefined), false);
+    });
+
+    it('isFunc returns false if object passed', function() {
+      assert.equal(isFunc({}), false);
+    });
+
+    it('isFunc returns false if array passed', function() {
+      assert.equal(isFunc([]), false);
+    });
+  });
+  //errorFunc tests
+  describe('Test errorFunc function', () => {
+
+    const resolve = () => 'success';
+    const reject = () => 'error';
+
+    it('errorFunc returns resolve if reject function has not been passed', () => {
+      let result = errorFunc(resolve);
+      assert.equal(result(), 'success');
+    });
+
+    it('errorFunc returns reject if reject function has been passed', () => {
+      let result = errorFunc(resolve, reject);
+      assert.equal(result(), 'error');
+    });
+
+  });
+  //getTempFilename tests
+  describe('Test getTempFilename function', () => {
+
+    const nameRegexp = /tmp-\d{1,5}-\d{1,}/;
+
+    it('getTempFilename result matches regexp /tmp-d{1,5}-d{1,}/', () => {
+
+      let errCounter = 0;
+      let tempName = '';
+      for (var i = 0; i < 65537; i++) {
+        tempName = getTempFilename();
+        if (!nameRegexp.test(tempName)) errCounter ++;
+      }
+
+      assert.equal(errCounter, 0);
+    });
+
+    it('getTempFilename current and previous results are not equal', () => {
+
+      let errCounter = 0;
+      let tempName = '';
+      let previousName = '';
+      for (var i = 0; i < 65537; i++) {
+        previousName = tempName;
+        tempName = getTempFilename();
+        if (previousName === tempName) errCounter ++;
+      }
+
+      assert.equal(errCounter, 0);
+    });
+
+  });
+  //parseFileName
+  describe('Test parseFileName function', () => {
+
+    it('Does nothing to your filename when disabled.', () => {
+      const opts = {safeFileNames: false};
+      const name = 'my$Invalid#fileName.png123';
+      const expected = 'my$Invalid#fileName.png123';
+      let result = parseFileName(opts, name);
+      assert.equal(result, expected);
+    });
+
+    it('Cuts of file name length if it more then 255 chars.', () => {
+      const name = 'a'.repeat(300);
+      const result = parseFileName({}, name);
+      assert.equal(result.length, 255);
+    });
+
+    it(
+      'Strips away all non-alphanumeric characters (excluding hyphens/underscores) when enabled.',
+      () => {
+        const opts = {safeFileNames: true};
+        const name = 'my$Invalid#fileName.png123';
+        const expected = 'myInvalidfileNamepng123';
+        let result = parseFileName(opts, name);
+        assert.equal(result, expected);
+      });
+
+    it(
+      'Strips away all non-alphanumeric chars when preserveExtension: true for a name without dots',
+      () => {
+        const opts = {safeFileNames: true, preserveExtension: true};
+        const name = 'my$Invalid#fileName';
+        const expected = 'myInvalidfileName';
+        let result = parseFileName(opts, name);
+        assert.equal(result, expected);
+      });
+
+    it('Accepts a regex for stripping (decidedly) "invalid" characters from filename.', () => {
+      const opts = {safeFileNames: /[$#]/g};
+      const name = 'my$Invalid#fileName.png123';
+      const expected = 'myInvalidfileName.png123';
+      let result = parseFileName(opts, name);
+      assert.equal(result, expected);
+    });
+
+    it(
+      'Returns correct filename if name contains dots characters and preserveExtension: true.',
+      () => {
+        const opts = {safeFileNames: true, preserveExtension: true};
+        const name = 'basket.ball.png';
+        const expected = 'basketball.png';
+        let result = parseFileName(opts, name);
+        assert.equal(result, expected);
+      });
+
+    it('Returns a temporary file name if name argument is empty.', () => {
+      const opts = {safeFileNames: false};
+      const result = parseFileName(opts);
+      assert.equal(typeof result, 'string');
+    });
+
+  });
+  //buildOptions tests
+  describe('Test buildOptions function', () => {
+
+    const source = { option1: '1', option2: '2' };
+    const sourceAddon = { option3: '3'};
+    const expected = { option1: '1', option2: '2' };
+    const expectedAddon = { option1: '1', option2: '2', option3: '3'};
+
+    it('buildOptions returns and equal object to the object which was paased', () => {
+      let result = buildOptions(source);
+      assert.deepStrictEqual(result, source);
+    });
+
+    it('buildOptions doesnt add non object or null arguments to the result', () => {
+      let result = buildOptions(source, 2, '3', null);
+      assert.deepStrictEqual(result, expected);
+    });
+
+    it('buildOptions adds value to the result from the several source argumets', () => {
+      let result = buildOptions(source, sourceAddon);
+      assert.deepStrictEqual(result, expectedAddon);
+    });
+
+  });
+  //buildFields tests
+  describe('Test buildOptions function', () => {
+
+    it('buildFields does nothing if null value has been passed', () => {
+      let fields = null;
+      fields = buildFields(fields, 'test', null);
+      assert.equal(fields, null);
+    });
+
+  });
+  //checkAndMakeDir tests
+  describe('Test checkAndMakeDir function', () => {
+    //
+    it('checkAndMakeDir returns false if upload options object was not set', () => {
+      assert.equal(checkAndMakeDir(), false);
+    });
+    //
+    it('checkAndMakeDir returns false if upload option createParentPath was not set', () => {
+      assert.equal(checkAndMakeDir({}), false);
+    });
+    //
+    it('checkAndMakeDir returns false if filePath was not set', () => {
+      assert.equal(checkAndMakeDir({createParentPath: true}), false);
+    });
+    //
+    it('checkAndMakeDir return true if path to the file already exists', ()=>{
+      let dir = path.join(uploadDir, 'testfile');
+      assert.equal(checkAndMakeDir({createParentPath: true}, dir), true);
+    });
+    //
+    it('checkAndMakeDir creates a dir if path to the file not exists', ()=>{
+      let dir = path.join(uploadDir, 'testfolder', 'testfile');
+      assert.equal(checkAndMakeDir({createParentPath: true}, dir), true);
+    });
+    //
+    it('checkAndMakeDir creates a dir recursively if path to the file not exists', ()=>{
+      let dir = path.join(uploadDir, 'testfolder', 'testsubfolder', 'testfile');
+      assert.equal(checkAndMakeDir({createParentPath: true}, dir), true);
+    });
+  });
+  //saveBufferToFile tests
+  describe('Test saveBufferToFile function', function(){
+    beforeEach(function() {
+      server.clearUploadsDir();
+    });
+
+    it('Save buffer to a file', function(done) {
+      let filePath = path.join(uploadDir, mockFile);
+      saveBufferToFile(mockBuffer, filePath, function(err){
+        if (err) {
+          return done(err);
+        }
+        fs.stat(filePath, done);
+      });
+    });
+
+    it('Failed if not a buffer passed', function(done) {
+      let filePath = path.join(uploadDir, mockFile);
+      saveBufferToFile(undefined, filePath, function(err){
+        if (err) {
+          return done();
+        }
+      });
+    });
+
+    it('Failed if wrong path passed', function(done) {
+      let filePath = '';
+      saveBufferToFile(mockFile, filePath, function(err){
+        if (err) {
+          return done();
+        }
+      });
+    });
+  });
+
+  describe('Test deleteFile function', function(){
+    beforeEach(function() {
+      server.clearUploadsDir();
+    });
+
+    it('Failed if nonexistent file passed', function(done){
+      let filePath = path.join(uploadDir, getTempFilename());
+
+      deleteFile(filePath, function(err){
+        if (err) {
+          return done();
+        }
+      });
+    });
+
+    it('Delete a file', function(done){
+      let srcPath = path.join(fileDir, mockFile);
+      let dstPath = path.join(uploadDir, getTempFilename());
+
+      //copy a file
+      copyFile(srcPath, dstPath, function(err){
+        if (err) {
+          return done(err);
+        }
+        fs.stat(dstPath, (err)=>{
+          if (err){
+            return done(err);
+          }
+          // delete a file
+          deleteFile(dstPath, function(err){
+            if (err) {
+              return done(err);
+            }
+
+            fs.stat(dstPath, (err)=>{
+              if (err){
+                return done();
+              }
+
+              //error if a file still exist
+              done(err);
+            });
+          });
+        });
+      });
+    });
+
+  });
+
+  describe('Test copyFile function', function(){
+    beforeEach(function() {
+      server.clearUploadsDir();
+    });
+
+    it('Copy a file and check a hash', function(done) {
+      let srcPath = path.join(fileDir, mockFile);
+      let dstPath = path.join(uploadDir, mockFile);
+
+      copyFile(srcPath, dstPath, function(err){
+        if (err) {
+          return done(err);
+        }
+        fs.stat(dstPath, (err)=>{
+          if (err){
+            return done(err);
+          }
+          //Match source and destination files hash.
+          let fileBuffer = fs.readFileSync(dstPath);
+          let fileHash = md5(fileBuffer);
+          return (fileHash === mockHash) ? done() : done(err);
+        });
+      });
+    });
+
+    it('Failed if wrong source file path passed', function(done){
+      let srcPath = path.join(fileDir, 'unknown');
+      let dstPath = path.join(uploadDir, mockFile);
+
+      copyFile(srcPath, dstPath, function(err){
+        if (err) {
+          return done();
+        }
+      });
+    });
+
+    it('Failed if wrong destination file path passed', function(done){
+      let srcPath = path.join(fileDir, 'unknown');
+      let dstPath = path.join('unknown', 'unknown');
+
+      copyFile(srcPath, dstPath, function(err){
+        if (err) {
+          return done();
+        }
+      });
+    });
+  });
+
+  describe('Test uriDecodeFileName function', function() {
+    const testData = [
+      { enc: 'test%22filename', dec: 'test"filename' },
+      { enc: 'test%60filename', dec: 'test`filename' },
+      { enc: '%3Fx%3Dtest%22filename', dec: '?x=test"filename'}
+    ];
+
+    // Test decoding if uriDecodeFileNames: true.
+    testData.forEach((testName) => {
+      const opts = { uriDecodeFileNames: true };
+      it(`Return ${testName.dec} for input ${testName.enc} if uriDecodeFileNames: true`, () => {
+        assert.equal(uriDecodeFileName(opts, testName.enc), testName.dec);
+      });
+    });
+
+    // Test decoding if uriDecodeFileNames: false.
+    testData.forEach((testName) => {
+      const opts = { uriDecodeFileNames: false };
+      it(`Return ${testName.enc} for input ${testName.enc} if uriDecodeFileNames: false`, () => {
+        assert.equal(uriDecodeFileName(opts, testName.enc), testName.enc);
+      });
+    });
+  });
+
+  describe('Test for no prototype pollution in buildFields', function() {
+    const prototypeFields = [
+      { name: '__proto__', data: {} },
+      { name: 'constructor', data: {} },
+      { name: 'toString', data: {} }
+    ];
+
+    const nonPrototypeFields = [
+      { name: 'a', data: {} },
+      { name: 'b', data: {} }
+    ];
+
+    let fieldObject = undefined;
+    [...prototypeFields, ...nonPrototypeFields].forEach((field) => {
+      fieldObject = buildFields(fieldObject, field.name, field.data);
+    });
+
+    it(`Has ${nonPrototypeFields.length} keys`, () => {
+      assert.equal(Object.keys(fieldObject).length, nonPrototypeFields.length);
+    });
+
+    it(`Has null as its prototype`, () => {
+      assert.equal(Object.getPrototypeOf(fieldObject), null);
+    });
+
+    prototypeFields.forEach((field) => {
+      it(`${field.name} property is not an array`, () => {
+        // Note, Array.isArray is an insufficient test due to it returning false
+        // for Objects with an array prototype.
+        assert.equal(fieldObject[field.name] instanceof Array, false);
+      });
+    });
+  });
+
+  describe('Test for correct detection of prototype pollution', function() {
+    const validInsertions = [
+      { base: {}, key: 'a' },
+      { base: { a: 1 }, key: 'a' },
+      { base: { __proto__: { a: 1 } }, key: 'a' },
+      { base: [1], key: 0 },
+      { base: { __proto__: [1] }, key: 0 }
+    ];
+
+    const invalidInsertions = [
+      { base: {}, key: '__proto__' },
+      { base: {}, key: 'constructor' },
+      { base: [1], key: '__proto__' },
+      { base: [1], key: 'length' },
+      { base: { __proto__: [1] }, key: 'length' }
+    ];
+
+    validInsertions.forEach((insertion) => {
+      it(`Key ${insertion.key} should be valid for ${JSON.stringify(insertion.base)}`, () => {
+        assert.equal(isSafeFromPollution(insertion.base, insertion.key), true);
+      });
+    });
+
+    invalidInsertions.forEach((insertion) => {
+      it(`Key ${insertion.key} should not be valid for ${JSON.stringify(insertion.base)}`, () => {
+        assert.equal(isSafeFromPollution(insertion.base, insertion.key), false);
+      });
+    });
+  });
+});

+ 157 - 0
backend/node_modules/express/node_modules/body-parser/index.js

@@ -0,0 +1,157 @@
+/*!
+ * body-parser
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var deprecate = require('depd')('body-parser')
+
+/**
+ * Cache of loaded parsers.
+ * @private
+ */
+
+var parsers = Object.create(null)
+
+/**
+ * @typedef Parsers
+ * @type {function}
+ * @property {function} json
+ * @property {function} raw
+ * @property {function} text
+ * @property {function} urlencoded
+ */
+
+/**
+ * Module exports.
+ * @type {Parsers}
+ */
+
+exports = module.exports = deprecate.function(bodyParser,
+  'bodyParser: use individual json/urlencoded middlewares')
+
+/**
+ * JSON parser.
+ * @public
+ */
+
+Object.defineProperty(exports, 'json', {
+  configurable: true,
+  enumerable: true,
+  get: createParserGetter('json')
+})
+
+/**
+ * Raw parser.
+ * @public
+ */
+
+Object.defineProperty(exports, 'raw', {
+  configurable: true,
+  enumerable: true,
+  get: createParserGetter('raw')
+})
+
+/**
+ * Text parser.
+ * @public
+ */
+
+Object.defineProperty(exports, 'text', {
+  configurable: true,
+  enumerable: true,
+  get: createParserGetter('text')
+})
+
+/**
+ * URL-encoded parser.
+ * @public
+ */
+
+Object.defineProperty(exports, 'urlencoded', {
+  configurable: true,
+  enumerable: true,
+  get: createParserGetter('urlencoded')
+})
+
+/**
+ * Create a middleware to parse json and urlencoded bodies.
+ *
+ * @param {object} [options]
+ * @return {function}
+ * @deprecated
+ * @public
+ */
+
+function bodyParser (options) {
+  var opts = {}
+
+  // exclude type option
+  if (options) {
+    for (var prop in options) {
+      if (prop !== 'type') {
+        opts[prop] = options[prop]
+      }
+    }
+  }
+
+  var _urlencoded = exports.urlencoded(opts)
+  var _json = exports.json(opts)
+
+  return function bodyParser (req, res, next) {
+    _json(req, res, function (err) {
+      if (err) return next(err)
+      _urlencoded(req, res, next)
+    })
+  }
+}
+
+/**
+ * Create a getter for loading a parser.
+ * @private
+ */
+
+function createParserGetter (name) {
+  return function get () {
+    return loadParser(name)
+  }
+}
+
+/**
+ * Load a parser module.
+ * @private
+ */
+
+function loadParser (parserName) {
+  var parser = parsers[parserName]
+
+  if (parser !== undefined) {
+    return parser
+  }
+
+  // this uses a switch for static require analysis
+  switch (parserName) {
+    case 'json':
+      parser = require('./lib/types/json')
+      break
+    case 'raw':
+      parser = require('./lib/types/raw')
+      break
+    case 'text':
+      parser = require('./lib/types/text')
+      break
+    case 'urlencoded':
+      parser = require('./lib/types/urlencoded')
+      break
+  }
+
+  // store to prevent invoking require()
+  return (parsers[parserName] = parser)
+}

+ 181 - 0
backend/node_modules/express/node_modules/body-parser/lib/read.js

@@ -0,0 +1,181 @@
+/*!
+ * body-parser
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var createError = require('http-errors')
+var getBody = require('raw-body')
+var iconv = require('iconv-lite')
+var onFinished = require('on-finished')
+var zlib = require('zlib')
+
+/**
+ * Module exports.
+ */
+
+module.exports = read
+
+/**
+ * Read a request into a buffer and parse.
+ *
+ * @param {object} req
+ * @param {object} res
+ * @param {function} next
+ * @param {function} parse
+ * @param {function} debug
+ * @param {object} options
+ * @private
+ */
+
+function read (req, res, next, parse, debug, options) {
+  var length
+  var opts = options
+  var stream
+
+  // flag as parsed
+  req._body = true
+
+  // read options
+  var encoding = opts.encoding !== null
+    ? opts.encoding
+    : null
+  var verify = opts.verify
+
+  try {
+    // get the content stream
+    stream = contentstream(req, debug, opts.inflate)
+    length = stream.length
+    stream.length = undefined
+  } catch (err) {
+    return next(err)
+  }
+
+  // set raw-body options
+  opts.length = length
+  opts.encoding = verify
+    ? null
+    : encoding
+
+  // assert charset is supported
+  if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) {
+    return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
+      charset: encoding.toLowerCase(),
+      type: 'charset.unsupported'
+    }))
+  }
+
+  // read body
+  debug('read body')
+  getBody(stream, opts, function (error, body) {
+    if (error) {
+      var _error
+
+      if (error.type === 'encoding.unsupported') {
+        // echo back charset
+        _error = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
+          charset: encoding.toLowerCase(),
+          type: 'charset.unsupported'
+        })
+      } else {
+        // set status code on error
+        _error = createError(400, error)
+      }
+
+      // read off entire request
+      stream.resume()
+      onFinished(req, function onfinished () {
+        next(createError(400, _error))
+      })
+      return
+    }
+
+    // verify
+    if (verify) {
+      try {
+        debug('verify body')
+        verify(req, res, body, encoding)
+      } catch (err) {
+        next(createError(403, err, {
+          body: body,
+          type: err.type || 'entity.verify.failed'
+        }))
+        return
+      }
+    }
+
+    // parse
+    var str = body
+    try {
+      debug('parse body')
+      str = typeof body !== 'string' && encoding !== null
+        ? iconv.decode(body, encoding)
+        : body
+      req.body = parse(str)
+    } catch (err) {
+      next(createError(400, err, {
+        body: str,
+        type: err.type || 'entity.parse.failed'
+      }))
+      return
+    }
+
+    next()
+  })
+}
+
+/**
+ * Get the content stream of the request.
+ *
+ * @param {object} req
+ * @param {function} debug
+ * @param {boolean} [inflate=true]
+ * @return {object}
+ * @api private
+ */
+
+function contentstream (req, debug, inflate) {
+  var encoding = (req.headers['content-encoding'] || 'identity').toLowerCase()
+  var length = req.headers['content-length']
+  var stream
+
+  debug('content-encoding "%s"', encoding)
+
+  if (inflate === false && encoding !== 'identity') {
+    throw createError(415, 'content encoding unsupported', {
+      encoding: encoding,
+      type: 'encoding.unsupported'
+    })
+  }
+
+  switch (encoding) {
+    case 'deflate':
+      stream = zlib.createInflate()
+      debug('inflate body')
+      req.pipe(stream)
+      break
+    case 'gzip':
+      stream = zlib.createGunzip()
+      debug('gunzip body')
+      req.pipe(stream)
+      break
+    case 'identity':
+      stream = req
+      stream.length = length
+      break
+    default:
+      throw createError(415, 'unsupported content encoding "' + encoding + '"', {
+        encoding: encoding,
+        type: 'encoding.unsupported'
+      })
+  }
+
+  return stream
+}

+ 230 - 0
backend/node_modules/express/node_modules/body-parser/lib/types/json.js

@@ -0,0 +1,230 @@
+/*!
+ * body-parser
+ * Copyright(c) 2014 Jonathan Ong
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var bytes = require('bytes')
+var contentType = require('content-type')
+var createError = require('http-errors')
+var debug = require('debug')('body-parser:json')
+var read = require('../read')
+var typeis = require('type-is')
+
+/**
+ * Module exports.
+ */
+
+module.exports = json
+
+/**
+ * RegExp to match the first non-space in a string.
+ *
+ * Allowed whitespace is defined in RFC 7159:
+ *
+ *    ws = *(
+ *            %x20 /              ; Space
+ *            %x09 /              ; Horizontal tab
+ *            %x0A /              ; Line feed or New line
+ *            %x0D )              ; Carriage return
+ */
+
+var FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*(.)/ // eslint-disable-line no-control-regex
+
+/**
+ * Create a middleware to parse JSON bodies.
+ *
+ * @param {object} [options]
+ * @return {function}
+ * @public
+ */
+
+function json (options) {
+  var opts = options || {}
+
+  var limit = typeof opts.limit !== 'number'
+    ? bytes.parse(opts.limit || '100kb')
+    : opts.limit
+  var inflate = opts.inflate !== false
+  var reviver = opts.reviver
+  var strict = opts.strict !== false
+  var type = opts.type || 'application/json'
+  var verify = opts.verify || false
+
+  if (verify !== false && typeof verify !== 'function') {
+    throw new TypeError('option verify must be function')
+  }
+
+  // create the appropriate type checking function
+  var shouldParse = typeof type !== 'function'
+    ? typeChecker(type)
+    : type
+
+  function parse (body) {
+    if (body.length === 0) {
+      // special-case empty json body, as it's a common client-side mistake
+      // TODO: maybe make this configurable or part of "strict" option
+      return {}
+    }
+
+    if (strict) {
+      var first = firstchar(body)
+
+      if (first !== '{' && first !== '[') {
+        debug('strict violation')
+        throw createStrictSyntaxError(body, first)
+      }
+    }
+
+    try {
+      debug('parse json')
+      return JSON.parse(body, reviver)
+    } catch (e) {
+      throw normalizeJsonSyntaxError(e, {
+        message: e.message,
+        stack: e.stack
+      })
+    }
+  }
+
+  return function jsonParser (req, res, next) {
+    if (req._body) {
+      debug('body already parsed')
+      next()
+      return
+    }
+
+    req.body = req.body || {}
+
+    // skip requests without bodies
+    if (!typeis.hasBody(req)) {
+      debug('skip empty body')
+      next()
+      return
+    }
+
+    debug('content-type %j', req.headers['content-type'])
+
+    // determine if request should be parsed
+    if (!shouldParse(req)) {
+      debug('skip parsing')
+      next()
+      return
+    }
+
+    // assert charset per RFC 7159 sec 8.1
+    var charset = getCharset(req) || 'utf-8'
+    if (charset.substr(0, 4) !== 'utf-') {
+      debug('invalid charset')
+      next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
+        charset: charset,
+        type: 'charset.unsupported'
+      }))
+      return
+    }
+
+    // read
+    read(req, res, next, parse, debug, {
+      encoding: charset,
+      inflate: inflate,
+      limit: limit,
+      verify: verify
+    })
+  }
+}
+
+/**
+ * Create strict violation syntax error matching native error.
+ *
+ * @param {string} str
+ * @param {string} char
+ * @return {Error}
+ * @private
+ */
+
+function createStrictSyntaxError (str, char) {
+  var index = str.indexOf(char)
+  var partial = str.substring(0, index) + '#'
+
+  try {
+    JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation')
+  } catch (e) {
+    return normalizeJsonSyntaxError(e, {
+      message: e.message.replace('#', char),
+      stack: e.stack
+    })
+  }
+}
+
+/**
+ * Get the first non-whitespace character in a string.
+ *
+ * @param {string} str
+ * @return {function}
+ * @private
+ */
+
+function firstchar (str) {
+  return FIRST_CHAR_REGEXP.exec(str)[1]
+}
+
+/**
+ * Get the charset of a request.
+ *
+ * @param {object} req
+ * @api private
+ */
+
+function getCharset (req) {
+  try {
+    return (contentType.parse(req).parameters.charset || '').toLowerCase()
+  } catch (e) {
+    return undefined
+  }
+}
+
+/**
+ * Normalize a SyntaxError for JSON.parse.
+ *
+ * @param {SyntaxError} error
+ * @param {object} obj
+ * @return {SyntaxError}
+ */
+
+function normalizeJsonSyntaxError (error, obj) {
+  var keys = Object.getOwnPropertyNames(error)
+
+  for (var i = 0; i < keys.length; i++) {
+    var key = keys[i]
+    if (key !== 'stack' && key !== 'message') {
+      delete error[key]
+    }
+  }
+
+  // replace stack before message for Node.js 0.10 and below
+  error.stack = obj.stack.replace(error.message, obj.message)
+  error.message = obj.message
+
+  return error
+}
+
+/**
+ * Get the simple type checker.
+ *
+ * @param {string} type
+ * @return {function}
+ */
+
+function typeChecker (type) {
+  return function checkType (req) {
+    return Boolean(typeis(req, type))
+  }
+}

+ 101 - 0
backend/node_modules/express/node_modules/body-parser/lib/types/raw.js

@@ -0,0 +1,101 @@
+/*!
+ * body-parser
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ */
+
+var bytes = require('bytes')
+var debug = require('debug')('body-parser:raw')
+var read = require('../read')
+var typeis = require('type-is')
+
+/**
+ * Module exports.
+ */
+
+module.exports = raw
+
+/**
+ * Create a middleware to parse raw bodies.
+ *
+ * @param {object} [options]
+ * @return {function}
+ * @api public
+ */
+
+function raw (options) {
+  var opts = options || {}
+
+  var inflate = opts.inflate !== false
+  var limit = typeof opts.limit !== 'number'
+    ? bytes.parse(opts.limit || '100kb')
+    : opts.limit
+  var type = opts.type || 'application/octet-stream'
+  var verify = opts.verify || false
+
+  if (verify !== false && typeof verify !== 'function') {
+    throw new TypeError('option verify must be function')
+  }
+
+  // create the appropriate type checking function
+  var shouldParse = typeof type !== 'function'
+    ? typeChecker(type)
+    : type
+
+  function parse (buf) {
+    return buf
+  }
+
+  return function rawParser (req, res, next) {
+    if (req._body) {
+      debug('body already parsed')
+      next()
+      return
+    }
+
+    req.body = req.body || {}
+
+    // skip requests without bodies
+    if (!typeis.hasBody(req)) {
+      debug('skip empty body')
+      next()
+      return
+    }
+
+    debug('content-type %j', req.headers['content-type'])
+
+    // determine if request should be parsed
+    if (!shouldParse(req)) {
+      debug('skip parsing')
+      next()
+      return
+    }
+
+    // read
+    read(req, res, next, parse, debug, {
+      encoding: null,
+      inflate: inflate,
+      limit: limit,
+      verify: verify
+    })
+  }
+}
+
+/**
+ * Get the simple type checker.
+ *
+ * @param {string} type
+ * @return {function}
+ */
+
+function typeChecker (type) {
+  return function checkType (req) {
+    return Boolean(typeis(req, type))
+  }
+}

+ 121 - 0
backend/node_modules/express/node_modules/body-parser/lib/types/text.js

@@ -0,0 +1,121 @@
+/*!
+ * body-parser
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ */
+
+var bytes = require('bytes')
+var contentType = require('content-type')
+var debug = require('debug')('body-parser:text')
+var read = require('../read')
+var typeis = require('type-is')
+
+/**
+ * Module exports.
+ */
+
+module.exports = text
+
+/**
+ * Create a middleware to parse text bodies.
+ *
+ * @param {object} [options]
+ * @return {function}
+ * @api public
+ */
+
+function text (options) {
+  var opts = options || {}
+
+  var defaultCharset = opts.defaultCharset || 'utf-8'
+  var inflate = opts.inflate !== false
+  var limit = typeof opts.limit !== 'number'
+    ? bytes.parse(opts.limit || '100kb')
+    : opts.limit
+  var type = opts.type || 'text/plain'
+  var verify = opts.verify || false
+
+  if (verify !== false && typeof verify !== 'function') {
+    throw new TypeError('option verify must be function')
+  }
+
+  // create the appropriate type checking function
+  var shouldParse = typeof type !== 'function'
+    ? typeChecker(type)
+    : type
+
+  function parse (buf) {
+    return buf
+  }
+
+  return function textParser (req, res, next) {
+    if (req._body) {
+      debug('body already parsed')
+      next()
+      return
+    }
+
+    req.body = req.body || {}
+
+    // skip requests without bodies
+    if (!typeis.hasBody(req)) {
+      debug('skip empty body')
+      next()
+      return
+    }
+
+    debug('content-type %j', req.headers['content-type'])
+
+    // determine if request should be parsed
+    if (!shouldParse(req)) {
+      debug('skip parsing')
+      next()
+      return
+    }
+
+    // get charset
+    var charset = getCharset(req) || defaultCharset
+
+    // read
+    read(req, res, next, parse, debug, {
+      encoding: charset,
+      inflate: inflate,
+      limit: limit,
+      verify: verify
+    })
+  }
+}
+
+/**
+ * Get the charset of a request.
+ *
+ * @param {object} req
+ * @api private
+ */
+
+function getCharset (req) {
+  try {
+    return (contentType.parse(req).parameters.charset || '').toLowerCase()
+  } catch (e) {
+    return undefined
+  }
+}
+
+/**
+ * Get the simple type checker.
+ *
+ * @param {string} type
+ * @return {function}
+ */
+
+function typeChecker (type) {
+  return function checkType (req) {
+    return Boolean(typeis(req, type))
+  }
+}

+ 284 - 0
backend/node_modules/express/node_modules/body-parser/lib/types/urlencoded.js

@@ -0,0 +1,284 @@
+/*!
+ * body-parser
+ * Copyright(c) 2014 Jonathan Ong
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var bytes = require('bytes')
+var contentType = require('content-type')
+var createError = require('http-errors')
+var debug = require('debug')('body-parser:urlencoded')
+var deprecate = require('depd')('body-parser')
+var read = require('../read')
+var typeis = require('type-is')
+
+/**
+ * Module exports.
+ */
+
+module.exports = urlencoded
+
+/**
+ * Cache of parser modules.
+ */
+
+var parsers = Object.create(null)
+
+/**
+ * Create a middleware to parse urlencoded bodies.
+ *
+ * @param {object} [options]
+ * @return {function}
+ * @public
+ */
+
+function urlencoded (options) {
+  var opts = options || {}
+
+  // notice because option default will flip in next major
+  if (opts.extended === undefined) {
+    deprecate('undefined extended: provide extended option')
+  }
+
+  var extended = opts.extended !== false
+  var inflate = opts.inflate !== false
+  var limit = typeof opts.limit !== 'number'
+    ? bytes.parse(opts.limit || '100kb')
+    : opts.limit
+  var type = opts.type || 'application/x-www-form-urlencoded'
+  var verify = opts.verify || false
+
+  if (verify !== false && typeof verify !== 'function') {
+    throw new TypeError('option verify must be function')
+  }
+
+  // create the appropriate query parser
+  var queryparse = extended
+    ? extendedparser(opts)
+    : simpleparser(opts)
+
+  // create the appropriate type checking function
+  var shouldParse = typeof type !== 'function'
+    ? typeChecker(type)
+    : type
+
+  function parse (body) {
+    return body.length
+      ? queryparse(body)
+      : {}
+  }
+
+  return function urlencodedParser (req, res, next) {
+    if (req._body) {
+      debug('body already parsed')
+      next()
+      return
+    }
+
+    req.body = req.body || {}
+
+    // skip requests without bodies
+    if (!typeis.hasBody(req)) {
+      debug('skip empty body')
+      next()
+      return
+    }
+
+    debug('content-type %j', req.headers['content-type'])
+
+    // determine if request should be parsed
+    if (!shouldParse(req)) {
+      debug('skip parsing')
+      next()
+      return
+    }
+
+    // assert charset
+    var charset = getCharset(req) || 'utf-8'
+    if (charset !== 'utf-8') {
+      debug('invalid charset')
+      next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
+        charset: charset,
+        type: 'charset.unsupported'
+      }))
+      return
+    }
+
+    // read
+    read(req, res, next, parse, debug, {
+      debug: debug,
+      encoding: charset,
+      inflate: inflate,
+      limit: limit,
+      verify: verify
+    })
+  }
+}
+
+/**
+ * Get the extended query parser.
+ *
+ * @param {object} options
+ */
+
+function extendedparser (options) {
+  var parameterLimit = options.parameterLimit !== undefined
+    ? options.parameterLimit
+    : 1000
+  var parse = parser('qs')
+
+  if (isNaN(parameterLimit) || parameterLimit < 1) {
+    throw new TypeError('option parameterLimit must be a positive number')
+  }
+
+  if (isFinite(parameterLimit)) {
+    parameterLimit = parameterLimit | 0
+  }
+
+  return function queryparse (body) {
+    var paramCount = parameterCount(body, parameterLimit)
+
+    if (paramCount === undefined) {
+      debug('too many parameters')
+      throw createError(413, 'too many parameters', {
+        type: 'parameters.too.many'
+      })
+    }
+
+    var arrayLimit = Math.max(100, paramCount)
+
+    debug('parse extended urlencoding')
+    return parse(body, {
+      allowPrototypes: true,
+      arrayLimit: arrayLimit,
+      depth: Infinity,
+      parameterLimit: parameterLimit
+    })
+  }
+}
+
+/**
+ * Get the charset of a request.
+ *
+ * @param {object} req
+ * @api private
+ */
+
+function getCharset (req) {
+  try {
+    return (contentType.parse(req).parameters.charset || '').toLowerCase()
+  } catch (e) {
+    return undefined
+  }
+}
+
+/**
+ * Count the number of parameters, stopping once limit reached
+ *
+ * @param {string} body
+ * @param {number} limit
+ * @api private
+ */
+
+function parameterCount (body, limit) {
+  var count = 0
+  var index = 0
+
+  while ((index = body.indexOf('&', index)) !== -1) {
+    count++
+    index++
+
+    if (count === limit) {
+      return undefined
+    }
+  }
+
+  return count
+}
+
+/**
+ * Get parser for module name dynamically.
+ *
+ * @param {string} name
+ * @return {function}
+ * @api private
+ */
+
+function parser (name) {
+  var mod = parsers[name]
+
+  if (mod !== undefined) {
+    return mod.parse
+  }
+
+  // this uses a switch for static require analysis
+  switch (name) {
+    case 'qs':
+      mod = require('qs')
+      break
+    case 'querystring':
+      mod = require('querystring')
+      break
+  }
+
+  // store to prevent invoking require()
+  parsers[name] = mod
+
+  return mod.parse
+}
+
+/**
+ * Get the simple query parser.
+ *
+ * @param {object} options
+ */
+
+function simpleparser (options) {
+  var parameterLimit = options.parameterLimit !== undefined
+    ? options.parameterLimit
+    : 1000
+  var parse = parser('querystring')
+
+  if (isNaN(parameterLimit) || parameterLimit < 1) {
+    throw new TypeError('option parameterLimit must be a positive number')
+  }
+
+  if (isFinite(parameterLimit)) {
+    parameterLimit = parameterLimit | 0
+  }
+
+  return function queryparse (body) {
+    var paramCount = parameterCount(body, parameterLimit)
+
+    if (paramCount === undefined) {
+      debug('too many parameters')
+      throw createError(413, 'too many parameters', {
+        type: 'parameters.too.many'
+      })
+    }
+
+    debug('parse urlencoding')
+    return parse(body, undefined, undefined, { maxKeys: parameterLimit })
+  }
+}
+
+/**
+ * Get the simple type checker.
+ *
+ * @param {string} type
+ * @return {function}
+ */
+
+function typeChecker (type) {
+  return function checkType (req) {
+    return Boolean(typeis(req, type))
+  }
+}

+ 166 - 0
backend/node_modules/express/node_modules/bytes/index.js

@@ -0,0 +1,166 @@
+/*!
+ * bytes
+ * Copyright(c) 2012-2014 TJ Holowaychuk
+ * Copyright(c) 2015 Jed Watson
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = bytes;
+module.exports.format = format;
+module.exports.parse = parse;
+
+/**
+ * Module variables.
+ * @private
+ */
+
+var formatThousandsRegExp = /\B(?=(\d{3})+(?!\d))/g;
+
+var formatDecimalsRegExp = /(?:\.0*|(\.[^0]+)0+)$/;
+
+var map = {
+  b:  1,
+  kb: 1 << 10,
+  mb: 1 << 20,
+  gb: 1 << 30,
+  tb: Math.pow(1024, 4),
+  pb: Math.pow(1024, 5),
+};
+
+var parseRegExp = /^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i;
+
+/**
+ * Convert the given value in bytes into a string or parse to string to an integer in bytes.
+ *
+ * @param {string|number} value
+ * @param {{
+ *  case: [string],
+ *  decimalPlaces: [number]
+ *  fixedDecimals: [boolean]
+ *  thousandsSeparator: [string]
+ *  unitSeparator: [string]
+ *  }} [options] bytes options.
+ *
+ * @returns {string|number|null}
+ */
+
+function bytes(value, options) {
+  if (typeof value === 'string') {
+    return parse(value);
+  }
+
+  if (typeof value === 'number') {
+    return format(value, options);
+  }
+
+  return null;
+}
+
+/**
+ * Format the given value in bytes into a string.
+ *
+ * If the value is negative, it is kept as such. If it is a float,
+ * it is rounded.
+ *
+ * @param {number} value
+ * @param {object} [options]
+ * @param {number} [options.decimalPlaces=2]
+ * @param {number} [options.fixedDecimals=false]
+ * @param {string} [options.thousandsSeparator=]
+ * @param {string} [options.unit=]
+ * @param {string} [options.unitSeparator=]
+ *
+ * @returns {string|null}
+ * @public
+ */
+
+function format(value, options) {
+  if (!Number.isFinite(value)) {
+    return null;
+  }
+
+  var mag = Math.abs(value);
+  var thousandsSeparator = (options && options.thousandsSeparator) || '';
+  var unitSeparator = (options && options.unitSeparator) || '';
+  var decimalPlaces = (options && options.decimalPlaces !== undefined) ? options.decimalPlaces : 2;
+  var fixedDecimals = Boolean(options && options.fixedDecimals);
+  var unit = (options && options.unit) || '';
+
+  if (!unit || !map[unit.toLowerCase()]) {
+    if (mag >= map.pb) {
+      unit = 'PB';
+    } else if (mag >= map.tb) {
+      unit = 'TB';
+    } else if (mag >= map.gb) {
+      unit = 'GB';
+    } else if (mag >= map.mb) {
+      unit = 'MB';
+    } else if (mag >= map.kb) {
+      unit = 'KB';
+    } else {
+      unit = 'B';
+    }
+  }
+
+  var val = value / map[unit.toLowerCase()];
+  var str = val.toFixed(decimalPlaces);
+
+  if (!fixedDecimals) {
+    str = str.replace(formatDecimalsRegExp, '$1');
+  }
+
+  if (thousandsSeparator) {
+    str = str.split('.').map(function (s, i) {
+      return i === 0
+        ? s.replace(formatThousandsRegExp, thousandsSeparator)
+        : s
+    }).join('.');
+  }
+
+  return str + unitSeparator + unit;
+}
+
+/**
+ * Parse the string value into an integer in bytes.
+ *
+ * If no unit is given, it is assumed the value is in bytes.
+ *
+ * @param {number|string} val
+ *
+ * @returns {number|null}
+ * @public
+ */
+
+function parse(val) {
+  if (typeof val === 'number' && !isNaN(val)) {
+    return val;
+  }
+
+  if (typeof val !== 'string') {
+    return null;
+  }
+
+  // Test if the string passed is valid
+  var results = parseRegExp.exec(val);
+  var floatValue;
+  var unit = 'b';
+
+  if (!results) {
+    // Nothing could be extracted from the given string
+    floatValue = parseInt(val, 10);
+    unit = 'b'
+  } else {
+    // Retrieve the value and the unit
+    floatValue = parseFloat(results[1]);
+    unit = results[4].toLowerCase();
+  }
+
+  return Math.floor(map[unit] * floatValue);
+}

+ 286 - 0
backend/node_modules/express/node_modules/raw-body/index.js

@@ -0,0 +1,286 @@
+/*!
+ * raw-body
+ * Copyright(c) 2013-2014 Jonathan Ong
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var bytes = require('bytes')
+var createError = require('http-errors')
+var iconv = require('iconv-lite')
+var unpipe = require('unpipe')
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = getRawBody
+
+/**
+ * Module variables.
+ * @private
+ */
+
+var ICONV_ENCODING_MESSAGE_REGEXP = /^Encoding not recognized: /
+
+/**
+ * Get the decoder for a given encoding.
+ *
+ * @param {string} encoding
+ * @private
+ */
+
+function getDecoder (encoding) {
+  if (!encoding) return null
+
+  try {
+    return iconv.getDecoder(encoding)
+  } catch (e) {
+    // error getting decoder
+    if (!ICONV_ENCODING_MESSAGE_REGEXP.test(e.message)) throw e
+
+    // the encoding was not found
+    throw createError(415, 'specified encoding unsupported', {
+      encoding: encoding,
+      type: 'encoding.unsupported'
+    })
+  }
+}
+
+/**
+ * Get the raw body of a stream (typically HTTP).
+ *
+ * @param {object} stream
+ * @param {object|string|function} [options]
+ * @param {function} [callback]
+ * @public
+ */
+
+function getRawBody (stream, options, callback) {
+  var done = callback
+  var opts = options || {}
+
+  if (options === true || typeof options === 'string') {
+    // short cut for encoding
+    opts = {
+      encoding: options
+    }
+  }
+
+  if (typeof options === 'function') {
+    done = options
+    opts = {}
+  }
+
+  // validate callback is a function, if provided
+  if (done !== undefined && typeof done !== 'function') {
+    throw new TypeError('argument callback must be a function')
+  }
+
+  // require the callback without promises
+  if (!done && !global.Promise) {
+    throw new TypeError('argument callback is required')
+  }
+
+  // get encoding
+  var encoding = opts.encoding !== true
+    ? opts.encoding
+    : 'utf-8'
+
+  // convert the limit to an integer
+  var limit = bytes.parse(opts.limit)
+
+  // convert the expected length to an integer
+  var length = opts.length != null && !isNaN(opts.length)
+    ? parseInt(opts.length, 10)
+    : null
+
+  if (done) {
+    // classic callback style
+    return readStream(stream, encoding, length, limit, done)
+  }
+
+  return new Promise(function executor (resolve, reject) {
+    readStream(stream, encoding, length, limit, function onRead (err, buf) {
+      if (err) return reject(err)
+      resolve(buf)
+    })
+  })
+}
+
+/**
+ * Halt a stream.
+ *
+ * @param {Object} stream
+ * @private
+ */
+
+function halt (stream) {
+  // unpipe everything from the stream
+  unpipe(stream)
+
+  // pause stream
+  if (typeof stream.pause === 'function') {
+    stream.pause()
+  }
+}
+
+/**
+ * Read the data from the stream.
+ *
+ * @param {object} stream
+ * @param {string} encoding
+ * @param {number} length
+ * @param {number} limit
+ * @param {function} callback
+ * @public
+ */
+
+function readStream (stream, encoding, length, limit, callback) {
+  var complete = false
+  var sync = true
+
+  // check the length and limit options.
+  // note: we intentionally leave the stream paused,
+  // so users should handle the stream themselves.
+  if (limit !== null && length !== null && length > limit) {
+    return done(createError(413, 'request entity too large', {
+      expected: length,
+      length: length,
+      limit: limit,
+      type: 'entity.too.large'
+    }))
+  }
+
+  // streams1: assert request encoding is buffer.
+  // streams2+: assert the stream encoding is buffer.
+  //   stream._decoder: streams1
+  //   state.encoding: streams2
+  //   state.decoder: streams2, specifically < 0.10.6
+  var state = stream._readableState
+  if (stream._decoder || (state && (state.encoding || state.decoder))) {
+    // developer error
+    return done(createError(500, 'stream encoding should not be set', {
+      type: 'stream.encoding.set'
+    }))
+  }
+
+  var received = 0
+  var decoder
+
+  try {
+    decoder = getDecoder(encoding)
+  } catch (err) {
+    return done(err)
+  }
+
+  var buffer = decoder
+    ? ''
+    : []
+
+  // attach listeners
+  stream.on('aborted', onAborted)
+  stream.on('close', cleanup)
+  stream.on('data', onData)
+  stream.on('end', onEnd)
+  stream.on('error', onEnd)
+
+  // mark sync section complete
+  sync = false
+
+  function done () {
+    var args = new Array(arguments.length)
+
+    // copy arguments
+    for (var i = 0; i < args.length; i++) {
+      args[i] = arguments[i]
+    }
+
+    // mark complete
+    complete = true
+
+    if (sync) {
+      process.nextTick(invokeCallback)
+    } else {
+      invokeCallback()
+    }
+
+    function invokeCallback () {
+      cleanup()
+
+      if (args[0]) {
+        // halt the stream on error
+        halt(stream)
+      }
+
+      callback.apply(null, args)
+    }
+  }
+
+  function onAborted () {
+    if (complete) return
+
+    done(createError(400, 'request aborted', {
+      code: 'ECONNABORTED',
+      expected: length,
+      length: length,
+      received: received,
+      type: 'request.aborted'
+    }))
+  }
+
+  function onData (chunk) {
+    if (complete) return
+
+    received += chunk.length
+
+    if (limit !== null && received > limit) {
+      done(createError(413, 'request entity too large', {
+        limit: limit,
+        received: received,
+        type: 'entity.too.large'
+      }))
+    } else if (decoder) {
+      buffer += decoder.write(chunk)
+    } else {
+      buffer.push(chunk)
+    }
+  }
+
+  function onEnd (err) {
+    if (complete) return
+    if (err) return done(err)
+
+    if (length !== null && received !== length) {
+      done(createError(400, 'request size did not match content length', {
+        expected: length,
+        length: length,
+        received: received,
+        type: 'request.size.invalid'
+      }))
+    } else {
+      var string = decoder
+        ? buffer + (decoder.end() || '')
+        : Buffer.concat(buffer)
+      done(null, string)
+    }
+  }
+
+  function cleanup () {
+    buffer = null
+
+    stream.removeListener('aborted', onAborted)
+    stream.removeListener('data', onData)
+    stream.removeListener('end', onEnd)
+    stream.removeListener('error', onEnd)
+    stream.removeListener('close', cleanup)
+  }
+}

+ 52 - 0
backend/node_modules/function-bind/implementation.js

@@ -0,0 +1,52 @@
+'use strict';
+
+/* eslint no-invalid-this: 1 */
+
+var ERROR_MESSAGE = 'Function.prototype.bind called on incompatible ';
+var slice = Array.prototype.slice;
+var toStr = Object.prototype.toString;
+var funcType = '[object Function]';
+
+module.exports = function bind(that) {
+    var target = this;
+    if (typeof target !== 'function' || toStr.call(target) !== funcType) {
+        throw new TypeError(ERROR_MESSAGE + target);
+    }
+    var args = slice.call(arguments, 1);
+
+    var bound;
+    var binder = function () {
+        if (this instanceof bound) {
+            var result = target.apply(
+                this,
+                args.concat(slice.call(arguments))
+            );
+            if (Object(result) === result) {
+                return result;
+            }
+            return this;
+        } else {
+            return target.apply(
+                that,
+                args.concat(slice.call(arguments))
+            );
+        }
+    };
+
+    var boundLength = Math.max(0, target.length - args.length);
+    var boundArgs = [];
+    for (var i = 0; i < boundLength; i++) {
+        boundArgs.push('$' + i);
+    }
+
+    bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder);
+
+    if (target.prototype) {
+        var Empty = function Empty() {};
+        Empty.prototype = target.prototype;
+        bound.prototype = new Empty();
+        Empty.prototype = null;
+    }
+
+    return bound;
+};

+ 5 - 0
backend/node_modules/function-bind/index.js

@@ -0,0 +1,5 @@
+'use strict';
+
+var implementation = require('./implementation');
+
+module.exports = Function.prototype.bind || implementation;

+ 252 - 0
backend/node_modules/function-bind/test/index.js

@@ -0,0 +1,252 @@
+// jscs:disable requireUseStrict
+
+var test = require('tape');
+
+var functionBind = require('../implementation');
+var getCurrentContext = function () { return this; };
+
+test('functionBind is a function', function (t) {
+    t.equal(typeof functionBind, 'function');
+    t.end();
+});
+
+test('non-functions', function (t) {
+    var nonFunctions = [true, false, [], {}, 42, 'foo', NaN, /a/g];
+    t.plan(nonFunctions.length);
+    for (var i = 0; i < nonFunctions.length; ++i) {
+        try { functionBind.call(nonFunctions[i]); } catch (ex) {
+            t.ok(ex instanceof TypeError, 'throws when given ' + String(nonFunctions[i]));
+        }
+    }
+    t.end();
+});
+
+test('without a context', function (t) {
+    t.test('binds properly', function (st) {
+        var args, context;
+        var namespace = {
+            func: functionBind.call(function () {
+                args = Array.prototype.slice.call(arguments);
+                context = this;
+            })
+        };
+        namespace.func(1, 2, 3);
+        st.deepEqual(args, [1, 2, 3]);
+        st.equal(context, getCurrentContext.call());
+        st.end();
+    });
+
+    t.test('binds properly, and still supplies bound arguments', function (st) {
+        var args, context;
+        var namespace = {
+            func: functionBind.call(function () {
+                args = Array.prototype.slice.call(arguments);
+                context = this;
+            }, undefined, 1, 2, 3)
+        };
+        namespace.func(4, 5, 6);
+        st.deepEqual(args, [1, 2, 3, 4, 5, 6]);
+        st.equal(context, getCurrentContext.call());
+        st.end();
+    });
+
+    t.test('returns properly', function (st) {
+        var args;
+        var namespace = {
+            func: functionBind.call(function () {
+                args = Array.prototype.slice.call(arguments);
+                return this;
+            }, null)
+        };
+        var context = namespace.func(1, 2, 3);
+        st.equal(context, getCurrentContext.call(), 'returned context is namespaced context');
+        st.deepEqual(args, [1, 2, 3], 'passed arguments are correct');
+        st.end();
+    });
+
+    t.test('returns properly with bound arguments', function (st) {
+        var args;
+        var namespace = {
+            func: functionBind.call(function () {
+                args = Array.prototype.slice.call(arguments);
+                return this;
+            }, null, 1, 2, 3)
+        };
+        var context = namespace.func(4, 5, 6);
+        st.equal(context, getCurrentContext.call(), 'returned context is namespaced context');
+        st.deepEqual(args, [1, 2, 3, 4, 5, 6], 'passed arguments are correct');
+        st.end();
+    });
+
+    t.test('called as a constructor', function (st) {
+        var thunkify = function (value) {
+            return function () { return value; };
+        };
+        st.test('returns object value', function (sst) {
+            var expectedReturnValue = [1, 2, 3];
+            var Constructor = functionBind.call(thunkify(expectedReturnValue), null);
+            var result = new Constructor();
+            sst.equal(result, expectedReturnValue);
+            sst.end();
+        });
+
+        st.test('does not return primitive value', function (sst) {
+            var Constructor = functionBind.call(thunkify(42), null);
+            var result = new Constructor();
+            sst.notEqual(result, 42);
+            sst.end();
+        });
+
+        st.test('object from bound constructor is instance of original and bound constructor', function (sst) {
+            var A = function (x) {
+                this.name = x || 'A';
+            };
+            var B = functionBind.call(A, null, 'B');
+
+            var result = new B();
+            sst.ok(result instanceof B, 'result is instance of bound constructor');
+            sst.ok(result instanceof A, 'result is instance of original constructor');
+            sst.end();
+        });
+
+        st.end();
+    });
+
+    t.end();
+});
+
+test('with a context', function (t) {
+    t.test('with no bound arguments', function (st) {
+        var args, context;
+        var boundContext = {};
+        var namespace = {
+            func: functionBind.call(function () {
+                args = Array.prototype.slice.call(arguments);
+                context = this;
+            }, boundContext)
+        };
+        namespace.func(1, 2, 3);
+        st.equal(context, boundContext, 'binds a context properly');
+        st.deepEqual(args, [1, 2, 3], 'supplies passed arguments');
+        st.end();
+    });
+
+    t.test('with bound arguments', function (st) {
+        var args, context;
+        var boundContext = {};
+        var namespace = {
+            func: functionBind.call(function () {
+                args = Array.prototype.slice.call(arguments);
+                context = this;
+            }, boundContext, 1, 2, 3)
+        };
+        namespace.func(4, 5, 6);
+        st.equal(context, boundContext, 'binds a context properly');
+        st.deepEqual(args, [1, 2, 3, 4, 5, 6], 'supplies bound and passed arguments');
+        st.end();
+    });
+
+    t.test('returns properly', function (st) {
+        var boundContext = {};
+        var args;
+        var namespace = {
+            func: functionBind.call(function () {
+                args = Array.prototype.slice.call(arguments);
+                return this;
+            }, boundContext)
+        };
+        var context = namespace.func(1, 2, 3);
+        st.equal(context, boundContext, 'returned context is bound context');
+        st.notEqual(context, getCurrentContext.call(), 'returned context is not lexical context');
+        st.deepEqual(args, [1, 2, 3], 'passed arguments are correct');
+        st.end();
+    });
+
+    t.test('returns properly with bound arguments', function (st) {
+        var boundContext = {};
+        var args;
+        var namespace = {
+            func: functionBind.call(function () {
+                args = Array.prototype.slice.call(arguments);
+                return this;
+            }, boundContext, 1, 2, 3)
+        };
+        var context = namespace.func(4, 5, 6);
+        st.equal(context, boundContext, 'returned context is bound context');
+        st.notEqual(context, getCurrentContext.call(), 'returned context is not lexical context');
+        st.deepEqual(args, [1, 2, 3, 4, 5, 6], 'passed arguments are correct');
+        st.end();
+    });
+
+    t.test('passes the correct arguments when called as a constructor', function (st) {
+        var expected = { name: 'Correct' };
+        var namespace = {
+            Func: functionBind.call(function (arg) {
+                return arg;
+            }, { name: 'Incorrect' })
+        };
+        var returned = new namespace.Func(expected);
+        st.equal(returned, expected, 'returns the right arg when called as a constructor');
+        st.end();
+    });
+
+    t.test('has the new instance\'s context when called as a constructor', function (st) {
+        var actualContext;
+        var expectedContext = { foo: 'bar' };
+        var namespace = {
+            Func: functionBind.call(function () {
+                actualContext = this;
+            }, expectedContext)
+        };
+        var result = new namespace.Func();
+        st.equal(result instanceof namespace.Func, true);
+        st.notEqual(actualContext, expectedContext);
+        st.end();
+    });
+
+    t.end();
+});
+
+test('bound function length', function (t) {
+    t.test('sets a correct length without thisArg', function (st) {
+        var subject = functionBind.call(function (a, b, c) { return a + b + c; });
+        st.equal(subject.length, 3);
+        st.equal(subject(1, 2, 3), 6);
+        st.end();
+    });
+
+    t.test('sets a correct length with thisArg', function (st) {
+        var subject = functionBind.call(function (a, b, c) { return a + b + c; }, {});
+        st.equal(subject.length, 3);
+        st.equal(subject(1, 2, 3), 6);
+        st.end();
+    });
+
+    t.test('sets a correct length without thisArg and first argument', function (st) {
+        var subject = functionBind.call(function (a, b, c) { return a + b + c; }, undefined, 1);
+        st.equal(subject.length, 2);
+        st.equal(subject(2, 3), 6);
+        st.end();
+    });
+
+    t.test('sets a correct length with thisArg and first argument', function (st) {
+        var subject = functionBind.call(function (a, b, c) { return a + b + c; }, {}, 1);
+        st.equal(subject.length, 2);
+        st.equal(subject(2, 3), 6);
+        st.end();
+    });
+
+    t.test('sets a correct length without thisArg and too many arguments', function (st) {
+        var subject = functionBind.call(function (a, b, c) { return a + b + c; }, undefined, 1, 2, 3, 4);
+        st.equal(subject.length, 0);
+        st.equal(subject(), 6);
+        st.end();
+    });
+
+    t.test('sets a correct length with thisArg and too many arguments', function (st) {
+        var subject = functionBind.call(function (a, b, c) { return a + b + c; }, {}, 1, 2, 3, 4);
+        st.equal(subject.length, 0);
+        st.equal(subject(), 6);
+        st.end();
+    });
+});

+ 334 - 0
backend/node_modules/get-intrinsic/index.js

@@ -0,0 +1,334 @@
+'use strict';
+
+var undefined;
+
+var $SyntaxError = SyntaxError;
+var $Function = Function;
+var $TypeError = TypeError;
+
+// eslint-disable-next-line consistent-return
+var getEvalledConstructor = function (expressionSyntax) {
+	try {
+		return $Function('"use strict"; return (' + expressionSyntax + ').constructor;')();
+	} catch (e) {}
+};
+
+var $gOPD = Object.getOwnPropertyDescriptor;
+if ($gOPD) {
+	try {
+		$gOPD({}, '');
+	} catch (e) {
+		$gOPD = null; // this is IE 8, which has a broken gOPD
+	}
+}
+
+var throwTypeError = function () {
+	throw new $TypeError();
+};
+var ThrowTypeError = $gOPD
+	? (function () {
+		try {
+			// eslint-disable-next-line no-unused-expressions, no-caller, no-restricted-properties
+			arguments.callee; // IE 8 does not throw here
+			return throwTypeError;
+		} catch (calleeThrows) {
+			try {
+				// IE 8 throws on Object.getOwnPropertyDescriptor(arguments, '')
+				return $gOPD(arguments, 'callee').get;
+			} catch (gOPDthrows) {
+				return throwTypeError;
+			}
+		}
+	}())
+	: throwTypeError;
+
+var hasSymbols = require('has-symbols')();
+
+var getProto = Object.getPrototypeOf || function (x) { return x.__proto__; }; // eslint-disable-line no-proto
+
+var needsEval = {};
+
+var TypedArray = typeof Uint8Array === 'undefined' ? undefined : getProto(Uint8Array);
+
+var INTRINSICS = {
+	'%AggregateError%': typeof AggregateError === 'undefined' ? undefined : AggregateError,
+	'%Array%': Array,
+	'%ArrayBuffer%': typeof ArrayBuffer === 'undefined' ? undefined : ArrayBuffer,
+	'%ArrayIteratorPrototype%': hasSymbols ? getProto([][Symbol.iterator]()) : undefined,
+	'%AsyncFromSyncIteratorPrototype%': undefined,
+	'%AsyncFunction%': needsEval,
+	'%AsyncGenerator%': needsEval,
+	'%AsyncGeneratorFunction%': needsEval,
+	'%AsyncIteratorPrototype%': needsEval,
+	'%Atomics%': typeof Atomics === 'undefined' ? undefined : Atomics,
+	'%BigInt%': typeof BigInt === 'undefined' ? undefined : BigInt,
+	'%Boolean%': Boolean,
+	'%DataView%': typeof DataView === 'undefined' ? undefined : DataView,
+	'%Date%': Date,
+	'%decodeURI%': decodeURI,
+	'%decodeURIComponent%': decodeURIComponent,
+	'%encodeURI%': encodeURI,
+	'%encodeURIComponent%': encodeURIComponent,
+	'%Error%': Error,
+	'%eval%': eval, // eslint-disable-line no-eval
+	'%EvalError%': EvalError,
+	'%Float32Array%': typeof Float32Array === 'undefined' ? undefined : Float32Array,
+	'%Float64Array%': typeof Float64Array === 'undefined' ? undefined : Float64Array,
+	'%FinalizationRegistry%': typeof FinalizationRegistry === 'undefined' ? undefined : FinalizationRegistry,
+	'%Function%': $Function,
+	'%GeneratorFunction%': needsEval,
+	'%Int8Array%': typeof Int8Array === 'undefined' ? undefined : Int8Array,
+	'%Int16Array%': typeof Int16Array === 'undefined' ? undefined : Int16Array,
+	'%Int32Array%': typeof Int32Array === 'undefined' ? undefined : Int32Array,
+	'%isFinite%': isFinite,
+	'%isNaN%': isNaN,
+	'%IteratorPrototype%': hasSymbols ? getProto(getProto([][Symbol.iterator]())) : undefined,
+	'%JSON%': typeof JSON === 'object' ? JSON : undefined,
+	'%Map%': typeof Map === 'undefined' ? undefined : Map,
+	'%MapIteratorPrototype%': typeof Map === 'undefined' || !hasSymbols ? undefined : getProto(new Map()[Symbol.iterator]()),
+	'%Math%': Math,
+	'%Number%': Number,
+	'%Object%': Object,
+	'%parseFloat%': parseFloat,
+	'%parseInt%': parseInt,
+	'%Promise%': typeof Promise === 'undefined' ? undefined : Promise,
+	'%Proxy%': typeof Proxy === 'undefined' ? undefined : Proxy,
+	'%RangeError%': RangeError,
+	'%ReferenceError%': ReferenceError,
+	'%Reflect%': typeof Reflect === 'undefined' ? undefined : Reflect,
+	'%RegExp%': RegExp,
+	'%Set%': typeof Set === 'undefined' ? undefined : Set,
+	'%SetIteratorPrototype%': typeof Set === 'undefined' || !hasSymbols ? undefined : getProto(new Set()[Symbol.iterator]()),
+	'%SharedArrayBuffer%': typeof SharedArrayBuffer === 'undefined' ? undefined : SharedArrayBuffer,
+	'%String%': String,
+	'%StringIteratorPrototype%': hasSymbols ? getProto(''[Symbol.iterator]()) : undefined,
+	'%Symbol%': hasSymbols ? Symbol : undefined,
+	'%SyntaxError%': $SyntaxError,
+	'%ThrowTypeError%': ThrowTypeError,
+	'%TypedArray%': TypedArray,
+	'%TypeError%': $TypeError,
+	'%Uint8Array%': typeof Uint8Array === 'undefined' ? undefined : Uint8Array,
+	'%Uint8ClampedArray%': typeof Uint8ClampedArray === 'undefined' ? undefined : Uint8ClampedArray,
+	'%Uint16Array%': typeof Uint16Array === 'undefined' ? undefined : Uint16Array,
+	'%Uint32Array%': typeof Uint32Array === 'undefined' ? undefined : Uint32Array,
+	'%URIError%': URIError,
+	'%WeakMap%': typeof WeakMap === 'undefined' ? undefined : WeakMap,
+	'%WeakRef%': typeof WeakRef === 'undefined' ? undefined : WeakRef,
+	'%WeakSet%': typeof WeakSet === 'undefined' ? undefined : WeakSet
+};
+
+var doEval = function doEval(name) {
+	var value;
+	if (name === '%AsyncFunction%') {
+		value = getEvalledConstructor('async function () {}');
+	} else if (name === '%GeneratorFunction%') {
+		value = getEvalledConstructor('function* () {}');
+	} else if (name === '%AsyncGeneratorFunction%') {
+		value = getEvalledConstructor('async function* () {}');
+	} else if (name === '%AsyncGenerator%') {
+		var fn = doEval('%AsyncGeneratorFunction%');
+		if (fn) {
+			value = fn.prototype;
+		}
+	} else if (name === '%AsyncIteratorPrototype%') {
+		var gen = doEval('%AsyncGenerator%');
+		if (gen) {
+			value = getProto(gen.prototype);
+		}
+	}
+
+	INTRINSICS[name] = value;
+
+	return value;
+};
+
+var LEGACY_ALIASES = {
+	'%ArrayBufferPrototype%': ['ArrayBuffer', 'prototype'],
+	'%ArrayPrototype%': ['Array', 'prototype'],
+	'%ArrayProto_entries%': ['Array', 'prototype', 'entries'],
+	'%ArrayProto_forEach%': ['Array', 'prototype', 'forEach'],
+	'%ArrayProto_keys%': ['Array', 'prototype', 'keys'],
+	'%ArrayProto_values%': ['Array', 'prototype', 'values'],
+	'%AsyncFunctionPrototype%': ['AsyncFunction', 'prototype'],
+	'%AsyncGenerator%': ['AsyncGeneratorFunction', 'prototype'],
+	'%AsyncGeneratorPrototype%': ['AsyncGeneratorFunction', 'prototype', 'prototype'],
+	'%BooleanPrototype%': ['Boolean', 'prototype'],
+	'%DataViewPrototype%': ['DataView', 'prototype'],
+	'%DatePrototype%': ['Date', 'prototype'],
+	'%ErrorPrototype%': ['Error', 'prototype'],
+	'%EvalErrorPrototype%': ['EvalError', 'prototype'],
+	'%Float32ArrayPrototype%': ['Float32Array', 'prototype'],
+	'%Float64ArrayPrototype%': ['Float64Array', 'prototype'],
+	'%FunctionPrototype%': ['Function', 'prototype'],
+	'%Generator%': ['GeneratorFunction', 'prototype'],
+	'%GeneratorPrototype%': ['GeneratorFunction', 'prototype', 'prototype'],
+	'%Int8ArrayPrototype%': ['Int8Array', 'prototype'],
+	'%Int16ArrayPrototype%': ['Int16Array', 'prototype'],
+	'%Int32ArrayPrototype%': ['Int32Array', 'prototype'],
+	'%JSONParse%': ['JSON', 'parse'],
+	'%JSONStringify%': ['JSON', 'stringify'],
+	'%MapPrototype%': ['Map', 'prototype'],
+	'%NumberPrototype%': ['Number', 'prototype'],
+	'%ObjectPrototype%': ['Object', 'prototype'],
+	'%ObjProto_toString%': ['Object', 'prototype', 'toString'],
+	'%ObjProto_valueOf%': ['Object', 'prototype', 'valueOf'],
+	'%PromisePrototype%': ['Promise', 'prototype'],
+	'%PromiseProto_then%': ['Promise', 'prototype', 'then'],
+	'%Promise_all%': ['Promise', 'all'],
+	'%Promise_reject%': ['Promise', 'reject'],
+	'%Promise_resolve%': ['Promise', 'resolve'],
+	'%RangeErrorPrototype%': ['RangeError', 'prototype'],
+	'%ReferenceErrorPrototype%': ['ReferenceError', 'prototype'],
+	'%RegExpPrototype%': ['RegExp', 'prototype'],
+	'%SetPrototype%': ['Set', 'prototype'],
+	'%SharedArrayBufferPrototype%': ['SharedArrayBuffer', 'prototype'],
+	'%StringPrototype%': ['String', 'prototype'],
+	'%SymbolPrototype%': ['Symbol', 'prototype'],
+	'%SyntaxErrorPrototype%': ['SyntaxError', 'prototype'],
+	'%TypedArrayPrototype%': ['TypedArray', 'prototype'],
+	'%TypeErrorPrototype%': ['TypeError', 'prototype'],
+	'%Uint8ArrayPrototype%': ['Uint8Array', 'prototype'],
+	'%Uint8ClampedArrayPrototype%': ['Uint8ClampedArray', 'prototype'],
+	'%Uint16ArrayPrototype%': ['Uint16Array', 'prototype'],
+	'%Uint32ArrayPrototype%': ['Uint32Array', 'prototype'],
+	'%URIErrorPrototype%': ['URIError', 'prototype'],
+	'%WeakMapPrototype%': ['WeakMap', 'prototype'],
+	'%WeakSetPrototype%': ['WeakSet', 'prototype']
+};
+
+var bind = require('function-bind');
+var hasOwn = require('has');
+var $concat = bind.call(Function.call, Array.prototype.concat);
+var $spliceApply = bind.call(Function.apply, Array.prototype.splice);
+var $replace = bind.call(Function.call, String.prototype.replace);
+var $strSlice = bind.call(Function.call, String.prototype.slice);
+var $exec = bind.call(Function.call, RegExp.prototype.exec);
+
+/* adapted from https://github.com/lodash/lodash/blob/4.17.15/dist/lodash.js#L6735-L6744 */
+var rePropName = /[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g;
+var reEscapeChar = /\\(\\)?/g; /** Used to match backslashes in property paths. */
+var stringToPath = function stringToPath(string) {
+	var first = $strSlice(string, 0, 1);
+	var last = $strSlice(string, -1);
+	if (first === '%' && last !== '%') {
+		throw new $SyntaxError('invalid intrinsic syntax, expected closing `%`');
+	} else if (last === '%' && first !== '%') {
+		throw new $SyntaxError('invalid intrinsic syntax, expected opening `%`');
+	}
+	var result = [];
+	$replace(string, rePropName, function (match, number, quote, subString) {
+		result[result.length] = quote ? $replace(subString, reEscapeChar, '$1') : number || match;
+	});
+	return result;
+};
+/* end adaptation */
+
+var getBaseIntrinsic = function getBaseIntrinsic(name, allowMissing) {
+	var intrinsicName = name;
+	var alias;
+	if (hasOwn(LEGACY_ALIASES, intrinsicName)) {
+		alias = LEGACY_ALIASES[intrinsicName];
+		intrinsicName = '%' + alias[0] + '%';
+	}
+
+	if (hasOwn(INTRINSICS, intrinsicName)) {
+		var value = INTRINSICS[intrinsicName];
+		if (value === needsEval) {
+			value = doEval(intrinsicName);
+		}
+		if (typeof value === 'undefined' && !allowMissing) {
+			throw new $TypeError('intrinsic ' + name + ' exists, but is not available. Please file an issue!');
+		}
+
+		return {
+			alias: alias,
+			name: intrinsicName,
+			value: value
+		};
+	}
+
+	throw new $SyntaxError('intrinsic ' + name + ' does not exist!');
+};
+
+module.exports = function GetIntrinsic(name, allowMissing) {
+	if (typeof name !== 'string' || name.length === 0) {
+		throw new $TypeError('intrinsic name must be a non-empty string');
+	}
+	if (arguments.length > 1 && typeof allowMissing !== 'boolean') {
+		throw new $TypeError('"allowMissing" argument must be a boolean');
+	}
+
+	if ($exec(/^%?[^%]*%?$/g, name) === null) {
+		throw new $SyntaxError('`%` may not be present anywhere but at the beginning and end of the intrinsic name');
+	}
+	var parts = stringToPath(name);
+	var intrinsicBaseName = parts.length > 0 ? parts[0] : '';
+
+	var intrinsic = getBaseIntrinsic('%' + intrinsicBaseName + '%', allowMissing);
+	var intrinsicRealName = intrinsic.name;
+	var value = intrinsic.value;
+	var skipFurtherCaching = false;
+
+	var alias = intrinsic.alias;
+	if (alias) {
+		intrinsicBaseName = alias[0];
+		$spliceApply(parts, $concat([0, 1], alias));
+	}
+
+	for (var i = 1, isOwn = true; i < parts.length; i += 1) {
+		var part = parts[i];
+		var first = $strSlice(part, 0, 1);
+		var last = $strSlice(part, -1);
+		if (
+			(
+				(first === '"' || first === "'" || first === '`')
+				|| (last === '"' || last === "'" || last === '`')
+			)
+			&& first !== last
+		) {
+			throw new $SyntaxError('property names with quotes must have matching quotes');
+		}
+		if (part === 'constructor' || !isOwn) {
+			skipFurtherCaching = true;
+		}
+
+		intrinsicBaseName += '.' + part;
+		intrinsicRealName = '%' + intrinsicBaseName + '%';
+
+		if (hasOwn(INTRINSICS, intrinsicRealName)) {
+			value = INTRINSICS[intrinsicRealName];
+		} else if (value != null) {
+			if (!(part in value)) {
+				if (!allowMissing) {
+					throw new $TypeError('base intrinsic for ' + name + ' exists, but the property is not available.');
+				}
+				return void undefined;
+			}
+			if ($gOPD && (i + 1) >= parts.length) {
+				var desc = $gOPD(value, part);
+				isOwn = !!desc;
+
+				// By convention, when a data property is converted to an accessor
+				// property to emulate a data property that does not suffer from
+				// the override mistake, that accessor's getter is marked with
+				// an `originalValue` property. Here, when we detect this, we
+				// uphold the illusion by pretending to see that original data
+				// property, i.e., returning the value rather than the getter
+				// itself.
+				if (isOwn && 'get' in desc && !('originalValue' in desc.get)) {
+					value = desc.get;
+				} else {
+					value = value[part];
+				}
+			} else {
+				isOwn = hasOwn(value, part);
+				value = value[part];
+			}
+
+			if (isOwn && !skipFurtherCaching) {
+				INTRINSICS[intrinsicRealName] = value;
+			}
+		}
+	}
+	return value;
+};

+ 274 - 0
backend/node_modules/get-intrinsic/test/GetIntrinsic.js

@@ -0,0 +1,274 @@
+'use strict';
+
+var GetIntrinsic = require('../');
+
+var test = require('tape');
+var forEach = require('for-each');
+var debug = require('object-inspect');
+var generatorFns = require('make-generator-function')();
+var asyncFns = require('make-async-function').list();
+var asyncGenFns = require('make-async-generator-function')();
+var mockProperty = require('mock-property');
+
+var callBound = require('call-bind/callBound');
+var v = require('es-value-fixtures');
+var $gOPD = require('es-abstract/helpers/getOwnPropertyDescriptor');
+var DefinePropertyOrThrow = require('es-abstract/2021/DefinePropertyOrThrow');
+
+var $isProto = callBound('%Object.prototype.isPrototypeOf%');
+
+test('export', function (t) {
+	t.equal(typeof GetIntrinsic, 'function', 'it is a function');
+	t.equal(GetIntrinsic.length, 2, 'function has length of 2');
+
+	t.end();
+});
+
+test('throws', function (t) {
+	t['throws'](
+		function () { GetIntrinsic('not an intrinsic'); },
+		SyntaxError,
+		'nonexistent intrinsic throws a syntax error'
+	);
+
+	t['throws'](
+		function () { GetIntrinsic(''); },
+		TypeError,
+		'empty string intrinsic throws a type error'
+	);
+
+	t['throws'](
+		function () { GetIntrinsic('.'); },
+		SyntaxError,
+		'"just a dot" intrinsic throws a syntax error'
+	);
+
+	t['throws'](
+		function () { GetIntrinsic('%String'); },
+		SyntaxError,
+		'Leading % without trailing % throws a syntax error'
+	);
+
+	t['throws'](
+		function () { GetIntrinsic('String%'); },
+		SyntaxError,
+		'Trailing % without leading % throws a syntax error'
+	);
+
+	t['throws'](
+		function () { GetIntrinsic("String['prototype]"); },
+		SyntaxError,
+		'Dynamic property access is disallowed for intrinsics (unterminated string)'
+	);
+
+	t['throws'](
+		function () { GetIntrinsic('%Proxy.prototype.undefined%'); },
+		TypeError,
+		"Throws when middle part doesn't exist (%Proxy.prototype.undefined%)"
+	);
+
+	t['throws'](
+		function () { GetIntrinsic('%Array.prototype%garbage%'); },
+		SyntaxError,
+		'Throws with extra percent signs'
+	);
+
+	t['throws'](
+		function () { GetIntrinsic('%Array.prototype%push%'); },
+		SyntaxError,
+		'Throws with extra percent signs, even on an existing intrinsic'
+	);
+
+	forEach(v.nonStrings, function (nonString) {
+		t['throws'](
+			function () { GetIntrinsic(nonString); },
+			TypeError,
+			debug(nonString) + ' is not a String'
+		);
+	});
+
+	forEach(v.nonBooleans, function (nonBoolean) {
+		t['throws'](
+			function () { GetIntrinsic('%', nonBoolean); },
+			TypeError,
+			debug(nonBoolean) + ' is not a Boolean'
+		);
+	});
+
+	forEach([
+		'toString',
+		'propertyIsEnumerable',
+		'hasOwnProperty'
+	], function (objectProtoMember) {
+		t['throws'](
+			function () { GetIntrinsic(objectProtoMember); },
+			SyntaxError,
+			debug(objectProtoMember) + ' is not an intrinsic'
+		);
+	});
+
+	t.end();
+});
+
+test('base intrinsics', function (t) {
+	t.equal(GetIntrinsic('%Object%'), Object, '%Object% yields Object');
+	t.equal(GetIntrinsic('Object'), Object, 'Object yields Object');
+	t.equal(GetIntrinsic('%Array%'), Array, '%Array% yields Array');
+	t.equal(GetIntrinsic('Array'), Array, 'Array yields Array');
+
+	t.end();
+});
+
+test('dotted paths', function (t) {
+	t.equal(GetIntrinsic('%Object.prototype.toString%'), Object.prototype.toString, '%Object.prototype.toString% yields Object.prototype.toString');
+	t.equal(GetIntrinsic('Object.prototype.toString'), Object.prototype.toString, 'Object.prototype.toString yields Object.prototype.toString');
+	t.equal(GetIntrinsic('%Array.prototype.push%'), Array.prototype.push, '%Array.prototype.push% yields Array.prototype.push');
+	t.equal(GetIntrinsic('Array.prototype.push'), Array.prototype.push, 'Array.prototype.push yields Array.prototype.push');
+
+	test('underscore paths are aliases for dotted paths', { skip: !Object.isFrozen || Object.isFrozen(Object.prototype) }, function (st) {
+		var original = GetIntrinsic('%ObjProto_toString%');
+
+		forEach([
+			'%Object.prototype.toString%',
+			'Object.prototype.toString',
+			'%ObjectPrototype.toString%',
+			'ObjectPrototype.toString',
+			'%ObjProto_toString%',
+			'ObjProto_toString'
+		], function (name) {
+			DefinePropertyOrThrow(Object.prototype, 'toString', {
+				'[[Value]]': function toString() {
+					return original.apply(this, arguments);
+				}
+			});
+			st.equal(GetIntrinsic(name), original, name + ' yields original Object.prototype.toString');
+		});
+
+		DefinePropertyOrThrow(Object.prototype, 'toString', { '[[Value]]': original });
+		st.end();
+	});
+
+	test('dotted paths cache', { skip: !Object.isFrozen || Object.isFrozen(Object.prototype) }, function (st) {
+		var original = GetIntrinsic('%Object.prototype.propertyIsEnumerable%');
+
+		forEach([
+			'%Object.prototype.propertyIsEnumerable%',
+			'Object.prototype.propertyIsEnumerable',
+			'%ObjectPrototype.propertyIsEnumerable%',
+			'ObjectPrototype.propertyIsEnumerable'
+		], function (name) {
+			var restore = mockProperty(Object.prototype, 'propertyIsEnumerable', {
+				value: function propertyIsEnumerable() {
+					return original.apply(this, arguments);
+				}
+			});
+			st.equal(GetIntrinsic(name), original, name + ' yields cached Object.prototype.propertyIsEnumerable');
+
+			restore();
+		});
+
+		st.end();
+	});
+
+	test('dotted path reports correct error', function (st) {
+		st['throws'](function () {
+			GetIntrinsic('%NonExistentIntrinsic.prototype.property%');
+		}, /%NonExistentIntrinsic%/, 'The base intrinsic of %NonExistentIntrinsic.prototype.property% is %NonExistentIntrinsic%');
+
+		st['throws'](function () {
+			GetIntrinsic('%NonExistentIntrinsicPrototype.property%');
+		}, /%NonExistentIntrinsicPrototype%/, 'The base intrinsic of %NonExistentIntrinsicPrototype.property% is %NonExistentIntrinsicPrototype%');
+
+		st.end();
+	});
+
+	t.end();
+});
+
+test('accessors', { skip: !$gOPD || typeof Map !== 'function' }, function (t) {
+	var actual = $gOPD(Map.prototype, 'size');
+	t.ok(actual, 'Map.prototype.size has a descriptor');
+	t.equal(typeof actual.get, 'function', 'Map.prototype.size has a getter function');
+	t.equal(GetIntrinsic('%Map.prototype.size%'), actual.get, '%Map.prototype.size% yields the getter for it');
+	t.equal(GetIntrinsic('Map.prototype.size'), actual.get, 'Map.prototype.size yields the getter for it');
+
+	t.end();
+});
+
+test('generator functions', { skip: !generatorFns.length }, function (t) {
+	var $GeneratorFunction = GetIntrinsic('%GeneratorFunction%');
+	var $GeneratorFunctionPrototype = GetIntrinsic('%Generator%');
+	var $GeneratorPrototype = GetIntrinsic('%GeneratorPrototype%');
+
+	forEach(generatorFns, function (genFn) {
+		var fnName = genFn.name;
+		fnName = fnName ? "'" + fnName + "'" : 'genFn';
+
+		t.ok(genFn instanceof $GeneratorFunction, fnName + ' instanceof %GeneratorFunction%');
+		t.ok($isProto($GeneratorFunctionPrototype, genFn), '%Generator% is prototype of ' + fnName);
+		t.ok($isProto($GeneratorPrototype, genFn.prototype), '%GeneratorPrototype% is prototype of ' + fnName + '.prototype');
+	});
+
+	t.end();
+});
+
+test('async functions', { skip: !asyncFns.length }, function (t) {
+	var $AsyncFunction = GetIntrinsic('%AsyncFunction%');
+	var $AsyncFunctionPrototype = GetIntrinsic('%AsyncFunctionPrototype%');
+
+	forEach(asyncFns, function (asyncFn) {
+		var fnName = asyncFn.name;
+		fnName = fnName ? "'" + fnName + "'" : 'asyncFn';
+
+		t.ok(asyncFn instanceof $AsyncFunction, fnName + ' instanceof %AsyncFunction%');
+		t.ok($isProto($AsyncFunctionPrototype, asyncFn), '%AsyncFunctionPrototype% is prototype of ' + fnName);
+	});
+
+	t.end();
+});
+
+test('async generator functions', { skip: asyncGenFns.length === 0 }, function (t) {
+	var $AsyncGeneratorFunction = GetIntrinsic('%AsyncGeneratorFunction%');
+	var $AsyncGeneratorFunctionPrototype = GetIntrinsic('%AsyncGenerator%');
+	var $AsyncGeneratorPrototype = GetIntrinsic('%AsyncGeneratorPrototype%');
+
+	forEach(asyncGenFns, function (asyncGenFn) {
+		var fnName = asyncGenFn.name;
+		fnName = fnName ? "'" + fnName + "'" : 'asyncGenFn';
+
+		t.ok(asyncGenFn instanceof $AsyncGeneratorFunction, fnName + ' instanceof %AsyncGeneratorFunction%');
+		t.ok($isProto($AsyncGeneratorFunctionPrototype, asyncGenFn), '%AsyncGenerator% is prototype of ' + fnName);
+		t.ok($isProto($AsyncGeneratorPrototype, asyncGenFn.prototype), '%AsyncGeneratorPrototype% is prototype of ' + fnName + '.prototype');
+	});
+
+	t.end();
+});
+
+test('%ThrowTypeError%', function (t) {
+	var $ThrowTypeError = GetIntrinsic('%ThrowTypeError%');
+
+	t.equal(typeof $ThrowTypeError, 'function', 'is a function');
+	t['throws'](
+		$ThrowTypeError,
+		TypeError,
+		'%ThrowTypeError% throws a TypeError'
+	);
+
+	t.end();
+});
+
+test('allowMissing', { skip: asyncGenFns.length > 0 }, function (t) {
+	t['throws'](
+		function () { GetIntrinsic('%AsyncGeneratorPrototype%'); },
+		TypeError,
+		'throws when missing'
+	);
+
+	t.equal(
+		GetIntrinsic('%AsyncGeneratorPrototype%', true),
+		undefined,
+		'does not throw when allowMissing'
+	);
+
+	t.end();
+});

+ 0 - 0
backend/node_modules/has-symbols/index.js


Some files were not shown because too many files changed in this diff