@jsenv/server 15.5.2 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +79 -86
  2. package/dist/html/4xx.html +196 -0
  3. package/dist/html/500.html +17 -0
  4. package/dist/html/route_inspector.html +2435 -0
  5. package/dist/js/formidable.js +17 -0
  6. package/dist/js/vendors.js +2001 -0
  7. package/dist/js/ws.js +6863 -0
  8. package/dist/jsenv_server.js +8829 -0
  9. package/package.json +20 -9
  10. package/src/content_negotiation/pick_content_version.js +63 -0
  11. package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
  12. package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
  13. package/src/interfacing_with_node/from_node_request.js +455 -0
  14. package/src/interfacing_with_node/get_observable_value_type.js +43 -0
  15. package/src/interfacing_with_node/observable.js +91 -75
  16. package/src/interfacing_with_node/observable_from.js +34 -0
  17. package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
  18. package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
  19. package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
  20. package/src/interfacing_with_node/observable_from_promise.js +13 -0
  21. package/src/interfacing_with_node/write_node_response.js +80 -35
  22. package/src/internal/colorizeResponseStatus.js +2 -0
  23. package/src/internal/headers_composition.js +42 -15
  24. package/src/internal/multiple-header.js +27 -12
  25. package/src/internal/object_composition.js +2 -2
  26. package/src/internal/response_composition.js +16 -3
  27. package/src/main.js +15 -11
  28. package/src/progressive_response.js +38 -0
  29. package/src/replace_placeholder_in_html.js +12 -0
  30. package/src/request_body_handling.js +22 -0
  31. package/src/router/router.js +878 -0
  32. package/src/server_events.js +431 -0
  33. package/src/service_controller.js +79 -73
  34. package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
  35. package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
  36. package/src/services/cors/jsenv_service_cors.js +10 -18
  37. package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
  38. package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
  39. package/src/services/error_handler/client/500.html +17 -0
  40. package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
  41. package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
  42. package/src/services/open_file/jsenv_service_open_file.js +37 -0
  43. package/src/services/route_inspector/client/code_block.js +215 -0
  44. package/src/services/route_inspector/client/route_inspector.html +1363 -0
  45. package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
  46. package/src/start_server.js +575 -607
  47. package/src/web_socket_response.js +53 -0
  48. package/src/from_fetch_response.js +0 -15
  49. package/src/interfacing_with_node/body.js +0 -81
  50. package/src/interfacing_with_node/request_factory.js +0 -125
  51. package/src/readRequestBody.js +0 -32
  52. package/src/server_timing/timing_measure.js +0 -26
  53. package/src/service_composition/path_to_regexp.js +0 -436
  54. package/src/service_composition/routing.js +0 -31
  55. package/src/sse/sse_room.js +0 -313
@@ -0,0 +1,2001 @@
1
+ import { PassThrough, Transform, Stream } from "node:stream";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import fsPromises from "node:fs/promises";
5
+ import { EventEmitter } from "node:events";
6
+ import { StringDecoder } from "node:string_decoder";
7
+ import require$$0 from "node:domain";
8
+ import fs from "node:fs";
9
+ import require$$1, { createHash } from "node:crypto";
10
+
11
+ /* eslint-disable no-underscore-dangle */
12
+
13
+
14
+ class PersistentFile extends EventEmitter {
15
+ constructor({ filepath, newFilename, originalFilename, mimetype, hashAlgorithm }) {
16
+ super();
17
+
18
+ this.lastModifiedDate = null;
19
+ Object.assign(this, { filepath, newFilename, originalFilename, mimetype, hashAlgorithm });
20
+
21
+ this.size = 0;
22
+ this._writeStream = null;
23
+
24
+ if (typeof this.hashAlgorithm === 'string') {
25
+ this.hash = require$$1.createHash(this.hashAlgorithm);
26
+ } else {
27
+ this.hash = null;
28
+ }
29
+ }
30
+
31
+ open() {
32
+ this._writeStream = fs.createWriteStream(this.filepath);
33
+ this._writeStream.on('error', (err) => {
34
+ this.emit('error', err);
35
+ });
36
+ }
37
+
38
+ toJSON() {
39
+ const json = {
40
+ size: this.size,
41
+ filepath: this.filepath,
42
+ newFilename: this.newFilename,
43
+ mimetype: this.mimetype,
44
+ mtime: this.lastModifiedDate,
45
+ length: this.length,
46
+ originalFilename: this.originalFilename,
47
+ };
48
+ if (this.hash && this.hash !== '') {
49
+ json.hash = this.hash;
50
+ }
51
+ return json;
52
+ }
53
+
54
+ toString() {
55
+ return `PersistentFile: ${this.newFilename}, Original: ${this.originalFilename}, Path: ${this.filepath}`;
56
+ }
57
+
58
+ write(buffer, cb) {
59
+ if (this.hash) {
60
+ this.hash.update(buffer);
61
+ }
62
+
63
+ if (this._writeStream.closed) {
64
+ cb();
65
+ return;
66
+ }
67
+
68
+ this._writeStream.write(buffer, () => {
69
+ this.lastModifiedDate = new Date();
70
+ this.size += buffer.length;
71
+ this.emit('progress', this.size);
72
+ cb();
73
+ });
74
+ }
75
+
76
+ end(cb) {
77
+ if (this.hash) {
78
+ this.hash = this.hash.digest('hex');
79
+ }
80
+ this._writeStream.end(() => {
81
+ this.emit('end');
82
+ cb();
83
+ });
84
+ }
85
+
86
+ destroy() {
87
+ this._writeStream.destroy();
88
+ const filepath = this.filepath;
89
+ setTimeout(function () {
90
+ fs.unlink(filepath, () => {});
91
+ }, 1);
92
+ }
93
+ }
94
+
95
+ /* eslint-disable no-underscore-dangle */
96
+
97
+
98
+ class VolatileFile extends EventEmitter {
99
+ constructor({ filepath, newFilename, originalFilename, mimetype, hashAlgorithm, createFileWriteStream }) {
100
+ super();
101
+
102
+ this.lastModifiedDate = null;
103
+ Object.assign(this, { filepath, newFilename, originalFilename, mimetype, hashAlgorithm, createFileWriteStream });
104
+
105
+ this.size = 0;
106
+ this._writeStream = null;
107
+
108
+ if (typeof this.hashAlgorithm === 'string') {
109
+ this.hash = createHash(this.hashAlgorithm);
110
+ } else {
111
+ this.hash = null;
112
+ }
113
+ }
114
+
115
+ open() {
116
+ this._writeStream = this.createFileWriteStream(this);
117
+ this._writeStream.on('error', (err) => {
118
+ this.emit('error', err);
119
+ });
120
+ }
121
+
122
+ destroy() {
123
+ this._writeStream.destroy();
124
+ }
125
+
126
+ toJSON() {
127
+ const json = {
128
+ size: this.size,
129
+ newFilename: this.newFilename,
130
+ length: this.length,
131
+ originalFilename: this.originalFilename,
132
+ mimetype: this.mimetype,
133
+ };
134
+ if (this.hash && this.hash !== '') {
135
+ json.hash = this.hash;
136
+ }
137
+ return json;
138
+ }
139
+
140
+ toString() {
141
+ return `VolatileFile: ${this.originalFilename}`;
142
+ }
143
+
144
+ write(buffer, cb) {
145
+ if (this.hash) {
146
+ this.hash.update(buffer);
147
+ }
148
+
149
+ if (this._writeStream.closed || this._writeStream.destroyed) {
150
+ cb();
151
+ return;
152
+ }
153
+
154
+ this._writeStream.write(buffer, () => {
155
+ this.size += buffer.length;
156
+ this.emit('progress', this.size);
157
+ cb();
158
+ });
159
+ }
160
+
161
+ end(cb) {
162
+ if (this.hash) {
163
+ this.hash = this.hash.digest('hex');
164
+ }
165
+ this._writeStream.end(() => {
166
+ this.emit('end');
167
+ cb();
168
+ });
169
+ }
170
+ }
171
+
172
+ for(var r=256,n=[];r--;)n[r]=(r+256).toString(16).substring(1);function hexoid(r){r=r||16;var t="",o=0;return function(){if(!t||256===o){for(t="",o=(1+r)/2|0;o--;)t+=n[256*Math.random()|0];t=t.substring(o=0,r-2);}return t+n[o++]}}
173
+
174
+ function getDefaultExportFromCjs$1 (x) {
175
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
176
+ }
177
+
178
+ var once = {exports: {}};
179
+
180
+ var wrappy_1$1;
181
+ var hasRequiredWrappy$1;
182
+
183
+ function requireWrappy$1 () {
184
+ if (hasRequiredWrappy$1) return wrappy_1$1;
185
+ hasRequiredWrappy$1 = 1;
186
+ // Returns a wrapper function that returns a wrapped callback
187
+ // The wrapper function should do some stuff, and return a
188
+ // presumably different callback function.
189
+ // This makes sure that own properties are retained, so that
190
+ // decorations and such are not lost along the way.
191
+ wrappy_1$1 = wrappy;
192
+ function wrappy (fn, cb) {
193
+ if (fn && cb) return wrappy(fn)(cb)
194
+
195
+ if (typeof fn !== 'function')
196
+ throw new TypeError('need wrapper function')
197
+
198
+ Object.keys(fn).forEach(function (k) {
199
+ wrapper[k] = fn[k];
200
+ });
201
+
202
+ return wrapper
203
+
204
+ function wrapper() {
205
+ var args = new Array(arguments.length);
206
+ for (var i = 0; i < args.length; i++) {
207
+ args[i] = arguments[i];
208
+ }
209
+ var ret = fn.apply(this, args);
210
+ var cb = args[args.length-1];
211
+ if (typeof ret === 'function' && ret !== cb) {
212
+ Object.keys(cb).forEach(function (k) {
213
+ ret[k] = cb[k];
214
+ });
215
+ }
216
+ return ret
217
+ }
218
+ }
219
+ return wrappy_1$1;
220
+ }
221
+
222
+ var hasRequiredOnce;
223
+
224
+ function requireOnce () {
225
+ if (hasRequiredOnce) return once.exports;
226
+ hasRequiredOnce = 1;
227
+ var wrappy = requireWrappy$1();
228
+ once.exports = wrappy(once$1);
229
+ once.exports.strict = wrappy(onceStrict);
230
+
231
+ once$1.proto = once$1(function () {
232
+ Object.defineProperty(Function.prototype, 'once', {
233
+ value: function () {
234
+ return once$1(this)
235
+ },
236
+ configurable: true
237
+ });
238
+
239
+ Object.defineProperty(Function.prototype, 'onceStrict', {
240
+ value: function () {
241
+ return onceStrict(this)
242
+ },
243
+ configurable: true
244
+ });
245
+ });
246
+
247
+ function once$1 (fn) {
248
+ var f = function () {
249
+ if (f.called) return f.value
250
+ f.called = true;
251
+ return f.value = fn.apply(this, arguments)
252
+ };
253
+ f.called = false;
254
+ return f
255
+ }
256
+
257
+ function onceStrict (fn) {
258
+ var f = function () {
259
+ if (f.called)
260
+ throw new Error(f.onceError)
261
+ f.called = true;
262
+ return f.value = fn.apply(this, arguments)
263
+ };
264
+ var name = fn.name || 'Function wrapped with `once`';
265
+ f.onceError = name + " shouldn't be called more than once";
266
+ f.called = false;
267
+ return f
268
+ }
269
+ return once.exports;
270
+ }
271
+
272
+ var onceExports = requireOnce();
273
+ var __jsenv_default_import__$1 = /*@__PURE__*/getDefaultExportFromCjs$1(onceExports);
274
+
275
+ onceExports.strict;
276
+
277
+ function getDefaultExportFromCjs (x) {
278
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
279
+ }
280
+
281
+ var wrappy_1;
282
+ var hasRequiredWrappy;
283
+
284
+ function requireWrappy () {
285
+ if (hasRequiredWrappy) return wrappy_1;
286
+ hasRequiredWrappy = 1;
287
+ // Returns a wrapper function that returns a wrapped callback
288
+ // The wrapper function should do some stuff, and return a
289
+ // presumably different callback function.
290
+ // This makes sure that own properties are retained, so that
291
+ // decorations and such are not lost along the way.
292
+ wrappy_1 = wrappy;
293
+ function wrappy (fn, cb) {
294
+ if (fn && cb) return wrappy(fn)(cb)
295
+
296
+ if (typeof fn !== 'function')
297
+ throw new TypeError('need wrapper function')
298
+
299
+ Object.keys(fn).forEach(function (k) {
300
+ wrapper[k] = fn[k];
301
+ });
302
+
303
+ return wrapper
304
+
305
+ function wrapper() {
306
+ var args = new Array(arguments.length);
307
+ for (var i = 0; i < args.length; i++) {
308
+ args[i] = arguments[i];
309
+ }
310
+ var ret = fn.apply(this, args);
311
+ var cb = args[args.length-1];
312
+ if (typeof ret === 'function' && ret !== cb) {
313
+ Object.keys(cb).forEach(function (k) {
314
+ ret[k] = cb[k];
315
+ });
316
+ }
317
+ return ret
318
+ }
319
+ }
320
+ return wrappy_1;
321
+ }
322
+
323
+ var raw;
324
+ var hasRequiredRaw;
325
+
326
+ function requireRaw () {
327
+ if (hasRequiredRaw) return raw;
328
+ hasRequiredRaw = 1;
329
+
330
+ var domain; // The domain module is executed on demand
331
+ var hasSetImmediate = typeof setImmediate === "function";
332
+
333
+ // Use the fastest means possible to execute a task in its own turn, with
334
+ // priority over other events including network IO events in Node.js.
335
+ //
336
+ // An exception thrown by a task will permanently interrupt the processing of
337
+ // subsequent tasks. The higher level `asap` function ensures that if an
338
+ // exception is thrown by a task, that the task queue will continue flushing as
339
+ // soon as possible, but if you use `rawAsap` directly, you are responsible to
340
+ // either ensure that no exceptions are thrown from your task, or to manually
341
+ // call `rawAsap.requestFlush` if an exception is thrown.
342
+ raw = rawAsap;
343
+ function rawAsap(task) {
344
+ if (!queue.length) {
345
+ requestFlush();
346
+ flushing = true;
347
+ }
348
+ // Avoids a function call
349
+ queue[queue.length] = task;
350
+ }
351
+
352
+ var queue = [];
353
+ // Once a flush has been requested, no further calls to `requestFlush` are
354
+ // necessary until the next `flush` completes.
355
+ var flushing = false;
356
+ // The position of the next task to execute in the task queue. This is
357
+ // preserved between calls to `flush` so that it can be resumed if
358
+ // a task throws an exception.
359
+ var index = 0;
360
+ // If a task schedules additional tasks recursively, the task queue can grow
361
+ // unbounded. To prevent memory excaustion, the task queue will periodically
362
+ // truncate already-completed tasks.
363
+ var capacity = 1024;
364
+
365
+ // The flush function processes all tasks that have been scheduled with
366
+ // `rawAsap` unless and until one of those tasks throws an exception.
367
+ // If a task throws an exception, `flush` ensures that its state will remain
368
+ // consistent and will resume where it left off when called again.
369
+ // However, `flush` does not make any arrangements to be called again if an
370
+ // exception is thrown.
371
+ function flush() {
372
+ while (index < queue.length) {
373
+ var currentIndex = index;
374
+ // Advance the index before calling the task. This ensures that we will
375
+ // begin flushing on the next task the task throws an error.
376
+ index = index + 1;
377
+ queue[currentIndex].call();
378
+ // Prevent leaking memory for long chains of recursive calls to `asap`.
379
+ // If we call `asap` within tasks scheduled by `asap`, the queue will
380
+ // grow, but to avoid an O(n) walk for every task we execute, we don't
381
+ // shift tasks off the queue after they have been executed.
382
+ // Instead, we periodically shift 1024 tasks off the queue.
383
+ if (index > capacity) {
384
+ // Manually shift all values starting at the index back to the
385
+ // beginning of the queue.
386
+ for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) {
387
+ queue[scan] = queue[scan + index];
388
+ }
389
+ queue.length -= index;
390
+ index = 0;
391
+ }
392
+ }
393
+ queue.length = 0;
394
+ index = 0;
395
+ flushing = false;
396
+ }
397
+
398
+ rawAsap.requestFlush = requestFlush;
399
+ function requestFlush() {
400
+ // Ensure flushing is not bound to any domain.
401
+ // It is not sufficient to exit the domain, because domains exist on a stack.
402
+ // To execute code outside of any domain, the following dance is necessary.
403
+ var parentDomain = process.domain;
404
+ if (parentDomain) {
405
+ if (!domain) {
406
+ // Lazy execute the domain module.
407
+ // Only employed if the user elects to use domains.
408
+ domain = require$$0;
409
+ }
410
+ domain.active = process.domain = null;
411
+ }
412
+
413
+ // `setImmediate` is slower that `process.nextTick`, but `process.nextTick`
414
+ // cannot handle recursion.
415
+ // `requestFlush` will only be called recursively from `asap.js`, to resume
416
+ // flushing after an error is thrown into a domain.
417
+ // Conveniently, `setImmediate` was introduced in the same version
418
+ // `process.nextTick` started throwing recursion errors.
419
+ if (flushing && hasSetImmediate) {
420
+ setImmediate(flush);
421
+ } else {
422
+ process.nextTick(flush);
423
+ }
424
+
425
+ if (parentDomain) {
426
+ domain.active = process.domain = parentDomain;
427
+ }
428
+ }
429
+ return raw;
430
+ }
431
+
432
+ var asap_1;
433
+ var hasRequiredAsap;
434
+
435
+ function requireAsap () {
436
+ if (hasRequiredAsap) return asap_1;
437
+ hasRequiredAsap = 1;
438
+
439
+ var rawAsap = requireRaw();
440
+ var freeTasks = [];
441
+
442
+ /**
443
+ * Calls a task as soon as possible after returning, in its own event, with
444
+ * priority over IO events. An exception thrown in a task can be handled by
445
+ * `process.on("uncaughtException") or `domain.on("error")`, but will otherwise
446
+ * crash the process. If the error is handled, all subsequent tasks will
447
+ * resume.
448
+ *
449
+ * @param {{call}} task A callable object, typically a function that takes no
450
+ * arguments.
451
+ */
452
+ asap_1 = asap;
453
+ function asap(task) {
454
+ var rawTask;
455
+ if (freeTasks.length) {
456
+ rawTask = freeTasks.pop();
457
+ } else {
458
+ rawTask = new RawTask();
459
+ }
460
+ rawTask.task = task;
461
+ rawTask.domain = process.domain;
462
+ rawAsap(rawTask);
463
+ }
464
+
465
+ function RawTask() {
466
+ this.task = null;
467
+ this.domain = null;
468
+ }
469
+
470
+ RawTask.prototype.call = function () {
471
+ if (this.domain) {
472
+ this.domain.enter();
473
+ }
474
+ var threw = true;
475
+ try {
476
+ this.task.call();
477
+ threw = false;
478
+ // If the task throws an exception (presumably) Node.js restores the
479
+ // domain stack for the next event.
480
+ if (this.domain) {
481
+ this.domain.exit();
482
+ }
483
+ } finally {
484
+ // We use try/finally and a threw flag to avoid messing up stack traces
485
+ // when we catch and release errors.
486
+ if (threw) {
487
+ // In Node.js, uncaught exceptions are considered fatal errors.
488
+ // Re-throw them to interrupt flushing!
489
+ // Ensure that flushing continues if an uncaught exception is
490
+ // suppressed listening process.on("uncaughtException") or
491
+ // domain.on("error").
492
+ rawAsap.requestFlush();
493
+ }
494
+ // If the task threw an error, we do not want to exit the domain here.
495
+ // Exiting the domain would prevent the domain from catching the error.
496
+ this.task = null;
497
+ this.domain = null;
498
+ freeTasks.push(this);
499
+ }
500
+ };
501
+ return asap_1;
502
+ }
503
+
504
+ var dezalgo_1;
505
+ var hasRequiredDezalgo;
506
+
507
+ function requireDezalgo () {
508
+ if (hasRequiredDezalgo) return dezalgo_1;
509
+ hasRequiredDezalgo = 1;
510
+ var wrappy = requireWrappy();
511
+ dezalgo_1 = wrappy(dezalgo);
512
+
513
+ var asap = requireAsap();
514
+
515
+ function dezalgo (cb) {
516
+ var sync = true;
517
+ asap(function () {
518
+ sync = false;
519
+ });
520
+
521
+ return function zalgoSafe() {
522
+ var args = arguments;
523
+ var me = this;
524
+ if (sync)
525
+ asap(function() {
526
+ cb.apply(me, args);
527
+ });
528
+ else
529
+ cb.apply(me, args);
530
+ }
531
+ }
532
+ return dezalgo_1;
533
+ }
534
+
535
+ var dezalgoExports = requireDezalgo();
536
+ var __jsenv_default_import__ = /*@__PURE__*/getDefaultExportFromCjs(dezalgoExports);
537
+
538
+ class OctetStreamParser extends PassThrough {
539
+ constructor(options = {}) {
540
+ super();
541
+ this.globalOptions = { ...options };
542
+ }
543
+ }
544
+
545
+ /* eslint-disable no-underscore-dangle */
546
+
547
+
548
+ const octetStreamType = 'octet-stream';
549
+ // the `options` is also available through the `options` / `formidable.options`
550
+ async function plugin$3(formidable, options) {
551
+ // the `this` context is always formidable, as the first argument of a plugin
552
+ // but this allows us to customize/test each plugin
553
+
554
+ /* istanbul ignore next */
555
+ const self = this || formidable;
556
+
557
+ if (/octet-stream/i.test(self.headers['content-type'])) {
558
+ await init$2.call(self, self, options);
559
+ }
560
+ return self;
561
+ }
562
+
563
+ // Note that it's a good practice (but it's up to you) to use the `this.options` instead
564
+ // of the passed `options` (second) param, because when you decide
565
+ // to test the plugin you can pass custom `this` context to it (and so `this.options`)
566
+ async function init$2(_self, _opts) {
567
+ this.type = octetStreamType;
568
+ const originalFilename = this.headers['x-file-name'];
569
+ const mimetype = this.headers['content-type'];
570
+
571
+ const thisPart = {
572
+ originalFilename,
573
+ mimetype,
574
+ };
575
+ const newFilename = this._getNewName(thisPart);
576
+ const filepath = this._joinDirectoryName(newFilename);
577
+ const file = await this._newFile({
578
+ newFilename,
579
+ filepath,
580
+ originalFilename,
581
+ mimetype,
582
+ });
583
+
584
+ this.emit('fileBegin', originalFilename, file);
585
+ file.open();
586
+ this.openedFiles.push(file);
587
+ this._flushing += 1;
588
+
589
+ this._parser = new OctetStreamParser(this.options);
590
+
591
+ // Keep track of writes that haven't finished so we don't emit the file before it's done being written
592
+ let outstandingWrites = 0;
593
+
594
+ this._parser.on('data', (buffer) => {
595
+ this.pause();
596
+ outstandingWrites += 1;
597
+
598
+ file.write(buffer, () => {
599
+ outstandingWrites -= 1;
600
+ this.resume();
601
+
602
+ if (this.ended) {
603
+ this._parser.emit('doneWritingFile');
604
+ }
605
+ });
606
+ });
607
+
608
+ this._parser.on('end', () => {
609
+ this._flushing -= 1;
610
+ this.ended = true;
611
+
612
+ const done = () => {
613
+ file.end(() => {
614
+ this.emit('file', 'file', file);
615
+ this._maybeEnd();
616
+ });
617
+ };
618
+
619
+ if (outstandingWrites === 0) {
620
+ done();
621
+ } else {
622
+ this._parser.once('doneWritingFile', done);
623
+ }
624
+ });
625
+
626
+ return this;
627
+ }
628
+
629
+ /* eslint-disable no-underscore-dangle */
630
+
631
+
632
+ // This is a buffering parser, have a look at StreamingQuerystring.js for a streaming parser
633
+ class QuerystringParser extends Transform {
634
+ constructor(options = {}) {
635
+ super({ readableObjectMode: true });
636
+ this.globalOptions = { ...options };
637
+ this.buffer = '';
638
+ this.bufferLength = 0;
639
+ }
640
+
641
+ _transform(buffer, encoding, callback) {
642
+ this.buffer += buffer.toString('ascii');
643
+ this.bufferLength = this.buffer.length;
644
+ callback();
645
+ }
646
+
647
+ _flush(callback) {
648
+ const fields = new URLSearchParams(this.buffer);
649
+ for (const [key, value] of fields) {
650
+ this.push({
651
+ key,
652
+ value,
653
+ });
654
+ }
655
+ this.buffer = '';
656
+ callback();
657
+ }
658
+ }
659
+
660
+ /* eslint-disable no-underscore-dangle */
661
+
662
+
663
+ const querystringType = 'urlencoded';
664
+ // the `options` is also available through the `this.options` / `formidable.options`
665
+ function plugin$2(formidable, options) {
666
+ // the `this` context is always formidable, as the first argument of a plugin
667
+ // but this allows us to customize/test each plugin
668
+
669
+ /* istanbul ignore next */
670
+ const self = this || formidable;
671
+
672
+ if (/urlencoded/i.test(self.headers['content-type'])) {
673
+ init$1.call(self, self, options);
674
+ }
675
+ return self;
676
+ }
677
+ // Note that it's a good practice (but it's up to you) to use the `this.options` instead
678
+ // of the passed `options` (second) param, because when you decide
679
+ // to test the plugin you can pass custom `this` context to it (and so `this.options`)
680
+ function init$1(_self, _opts) {
681
+ this.type = querystringType;
682
+
683
+ const parser = new QuerystringParser(this.options);
684
+
685
+ parser.on('data', ({ key, value }) => {
686
+ this.emit('field', key, value);
687
+ });
688
+
689
+ parser.once('end', () => {
690
+ this.ended = true;
691
+ this._maybeEnd();
692
+ });
693
+
694
+ this._parser = parser;
695
+
696
+ return this;
697
+ }
698
+
699
+ const missingPlugin = 1000;
700
+ const pluginFunction = 1001;
701
+ const aborted = 1002;
702
+ const noParser = 1003;
703
+ const uninitializedParser = 1004;
704
+ const filenameNotString = 1005;
705
+ const maxFieldsSizeExceeded = 1006;
706
+ const maxFieldsExceeded = 1007;
707
+ const smallerThanMinFileSize = 1008;
708
+ const biggerThanTotalMaxFileSize = 1009;
709
+ const noEmptyFiles = 1010;
710
+ const missingContentType = 1011;
711
+ const malformedMultipart = 1012;
712
+ const missingMultipartBoundary = 1013;
713
+ const unknownTransferEncoding = 1014;
714
+ const maxFilesExceeded = 1015;
715
+ const biggerThanMaxFileSize = 1016;
716
+ const pluginFailed = 1017;
717
+ const cannotCreateDir = 1018;
718
+
719
+ const FormidableError = class extends Error {
720
+ constructor(message, internalCode, httpCode = 500) {
721
+ super(message);
722
+ this.code = internalCode;
723
+ this.httpCode = httpCode;
724
+ }
725
+ };
726
+
727
+ /* eslint-disable no-fallthrough */
728
+ /* eslint-disable no-bitwise */
729
+ /* eslint-disable no-plusplus */
730
+ /* eslint-disable no-underscore-dangle */
731
+
732
+
733
+ let s = 0;
734
+ const STATE = {
735
+ PARSER_UNINITIALIZED: s++,
736
+ START: s++,
737
+ START_BOUNDARY: s++,
738
+ HEADER_FIELD_START: s++,
739
+ HEADER_FIELD: s++,
740
+ HEADER_VALUE_START: s++,
741
+ HEADER_VALUE: s++,
742
+ HEADER_VALUE_ALMOST_DONE: s++,
743
+ HEADERS_ALMOST_DONE: s++,
744
+ PART_DATA_START: s++,
745
+ PART_DATA: s++,
746
+ PART_END: s++,
747
+ END: s++,
748
+ };
749
+
750
+ let f = 1;
751
+ const FBOUNDARY = { PART_BOUNDARY: f, LAST_BOUNDARY: (f *= 2) };
752
+
753
+ const LF = 10;
754
+ const CR = 13;
755
+ const SPACE = 32;
756
+ const HYPHEN = 45;
757
+ const COLON = 58;
758
+ const A = 97;
759
+ const Z = 122;
760
+
761
+ function lower(c) {
762
+ return c | 0x20;
763
+ }
764
+
765
+ const STATES = {};
766
+
767
+ Object.keys(STATE).forEach((stateName) => {
768
+ STATES[stateName] = STATE[stateName];
769
+ });
770
+
771
+ class MultipartParser extends Transform {
772
+ constructor(options = {}) {
773
+ super({ readableObjectMode: true });
774
+ this.boundary = null;
775
+ this.boundaryChars = null;
776
+ this.lookbehind = null;
777
+ this.bufferLength = 0;
778
+ this.state = STATE.PARSER_UNINITIALIZED;
779
+
780
+ this.globalOptions = { ...options };
781
+ this.index = null;
782
+ this.flags = 0;
783
+ }
784
+
785
+ _endUnexpected() {
786
+ return new FormidableError(
787
+ `MultipartParser.end(): stream ended unexpectedly: ${this.explain()}`,
788
+ malformedMultipart,
789
+ 400,
790
+ );
791
+ }
792
+
793
+ _flush(done) {
794
+ if (
795
+ (this.state === STATE.HEADER_FIELD_START && this.index === 0) ||
796
+ (this.state === STATE.PART_DATA && this.index === this.boundary.length)
797
+ ) {
798
+ this._handleCallback('partEnd');
799
+ this._handleCallback('end');
800
+ done();
801
+ } else if (this.state !== STATE.END) {
802
+ done(this._endUnexpected());
803
+ } else {
804
+ done();
805
+ }
806
+ }
807
+
808
+ initWithBoundary(str) {
809
+ this.boundary = Buffer.from(`\r\n--${str}`);
810
+ this.lookbehind = Buffer.alloc(this.boundary.length + 8);
811
+ this.state = STATE.START;
812
+ this.boundaryChars = {};
813
+
814
+ for (let i = 0; i < this.boundary.length; i++) {
815
+ this.boundaryChars[this.boundary[i]] = true;
816
+ }
817
+ }
818
+
819
+ // eslint-disable-next-line max-params
820
+ _handleCallback(name, buf, start, end) {
821
+ if (start !== undefined && start === end) {
822
+ return;
823
+ }
824
+ this.push({ name, buffer: buf, start, end });
825
+ }
826
+
827
+ // eslint-disable-next-line max-statements
828
+ _transform(buffer, _, done) {
829
+ let i = 0;
830
+ let prevIndex = this.index;
831
+ let { index, state, flags } = this;
832
+ const { lookbehind, boundary, boundaryChars } = this;
833
+ const boundaryLength = boundary.length;
834
+ const boundaryEnd = boundaryLength - 1;
835
+ this.bufferLength = buffer.length;
836
+ let c = null;
837
+ let cl = null;
838
+
839
+ const setMark = (name, idx) => {
840
+ this[`${name}Mark`] = typeof idx === 'number' ? idx : i;
841
+ };
842
+
843
+ const clearMarkSymbol = (name) => {
844
+ delete this[`${name}Mark`];
845
+ };
846
+
847
+ const dataCallback = (name, shouldClear) => {
848
+ const markSymbol = `${name}Mark`;
849
+ if (!(markSymbol in this)) {
850
+ return;
851
+ }
852
+
853
+ if (!shouldClear) {
854
+ this._handleCallback(name, buffer, this[markSymbol], buffer.length);
855
+ setMark(name, 0);
856
+ } else {
857
+ this._handleCallback(name, buffer, this[markSymbol], i);
858
+ clearMarkSymbol(name);
859
+ }
860
+ };
861
+
862
+ for (i = 0; i < this.bufferLength; i++) {
863
+ c = buffer[i];
864
+ switch (state) {
865
+ case STATE.PARSER_UNINITIALIZED:
866
+ done(this._endUnexpected());
867
+ return;
868
+ case STATE.START:
869
+ index = 0;
870
+ state = STATE.START_BOUNDARY;
871
+ case STATE.START_BOUNDARY:
872
+ if (index === boundary.length - 2) {
873
+ if (c === HYPHEN) {
874
+ flags |= FBOUNDARY.LAST_BOUNDARY;
875
+ } else if (c !== CR) {
876
+ done(this._endUnexpected());
877
+ return;
878
+ }
879
+ index++;
880
+ break;
881
+ } else if (index - 1 === boundary.length - 2) {
882
+ if (flags & FBOUNDARY.LAST_BOUNDARY && c === HYPHEN) {
883
+ this._handleCallback('end');
884
+ state = STATE.END;
885
+ flags = 0;
886
+ } else if (!(flags & FBOUNDARY.LAST_BOUNDARY) && c === LF) {
887
+ index = 0;
888
+ this._handleCallback('partBegin');
889
+ state = STATE.HEADER_FIELD_START;
890
+ } else {
891
+ done(this._endUnexpected());
892
+ return;
893
+ }
894
+ break;
895
+ }
896
+
897
+ if (c !== boundary[index + 2]) {
898
+ index = -2;
899
+ }
900
+ if (c === boundary[index + 2]) {
901
+ index++;
902
+ }
903
+ break;
904
+ case STATE.HEADER_FIELD_START:
905
+ state = STATE.HEADER_FIELD;
906
+ setMark('headerField');
907
+ index = 0;
908
+ case STATE.HEADER_FIELD:
909
+ if (c === CR) {
910
+ clearMarkSymbol('headerField');
911
+ state = STATE.HEADERS_ALMOST_DONE;
912
+ break;
913
+ }
914
+
915
+ index++;
916
+ if (c === HYPHEN) {
917
+ break;
918
+ }
919
+
920
+ if (c === COLON) {
921
+ if (index === 1) {
922
+ // empty header field
923
+ done(this._endUnexpected());
924
+ return;
925
+ }
926
+ dataCallback('headerField', true);
927
+ state = STATE.HEADER_VALUE_START;
928
+ break;
929
+ }
930
+
931
+ cl = lower(c);
932
+ if (cl < A || cl > Z) {
933
+ done(this._endUnexpected());
934
+ return;
935
+ }
936
+ break;
937
+ case STATE.HEADER_VALUE_START:
938
+ if (c === SPACE) {
939
+ break;
940
+ }
941
+
942
+ setMark('headerValue');
943
+ state = STATE.HEADER_VALUE;
944
+ case STATE.HEADER_VALUE:
945
+ if (c === CR) {
946
+ dataCallback('headerValue', true);
947
+ this._handleCallback('headerEnd');
948
+ state = STATE.HEADER_VALUE_ALMOST_DONE;
949
+ }
950
+ break;
951
+ case STATE.HEADER_VALUE_ALMOST_DONE:
952
+ if (c !== LF) {
953
+ done(this._endUnexpected());
954
+ return;
955
+ }
956
+ state = STATE.HEADER_FIELD_START;
957
+ break;
958
+ case STATE.HEADERS_ALMOST_DONE:
959
+ if (c !== LF) {
960
+ done(this._endUnexpected());
961
+ return;
962
+ }
963
+
964
+ this._handleCallback('headersEnd');
965
+ state = STATE.PART_DATA_START;
966
+ break;
967
+ case STATE.PART_DATA_START:
968
+ state = STATE.PART_DATA;
969
+ setMark('partData');
970
+ case STATE.PART_DATA:
971
+ prevIndex = index;
972
+
973
+ if (index === 0) {
974
+ // boyer-moore derived algorithm to safely skip non-boundary data
975
+ i += boundaryEnd;
976
+ while (i < this.bufferLength && !(buffer[i] in boundaryChars)) {
977
+ i += boundaryLength;
978
+ }
979
+ i -= boundaryEnd;
980
+ c = buffer[i];
981
+ }
982
+
983
+ if (index < boundary.length) {
984
+ if (boundary[index] === c) {
985
+ if (index === 0) {
986
+ dataCallback('partData', true);
987
+ }
988
+ index++;
989
+ } else {
990
+ index = 0;
991
+ }
992
+ } else if (index === boundary.length) {
993
+ index++;
994
+ if (c === CR) {
995
+ // CR = part boundary
996
+ flags |= FBOUNDARY.PART_BOUNDARY;
997
+ } else if (c === HYPHEN) {
998
+ // HYPHEN = end boundary
999
+ flags |= FBOUNDARY.LAST_BOUNDARY;
1000
+ } else {
1001
+ index = 0;
1002
+ }
1003
+ } else if (index - 1 === boundary.length) {
1004
+ if (flags & FBOUNDARY.PART_BOUNDARY) {
1005
+ index = 0;
1006
+ if (c === LF) {
1007
+ // unset the PART_BOUNDARY flag
1008
+ flags &= ~FBOUNDARY.PART_BOUNDARY;
1009
+ this._handleCallback('partEnd');
1010
+ this._handleCallback('partBegin');
1011
+ state = STATE.HEADER_FIELD_START;
1012
+ break;
1013
+ }
1014
+ } else if (flags & FBOUNDARY.LAST_BOUNDARY) {
1015
+ if (c === HYPHEN) {
1016
+ this._handleCallback('partEnd');
1017
+ this._handleCallback('end');
1018
+ state = STATE.END;
1019
+ flags = 0;
1020
+ } else {
1021
+ index = 0;
1022
+ }
1023
+ } else {
1024
+ index = 0;
1025
+ }
1026
+ }
1027
+
1028
+ if (index > 0) {
1029
+ // when matching a possible boundary, keep a lookbehind reference
1030
+ // in case it turns out to be a false lead
1031
+ lookbehind[index - 1] = c;
1032
+ } else if (prevIndex > 0) {
1033
+ // if our boundary turned out to be rubbish, the captured lookbehind
1034
+ // belongs to partData
1035
+ this._handleCallback('partData', lookbehind, 0, prevIndex);
1036
+ prevIndex = 0;
1037
+ setMark('partData');
1038
+
1039
+ // reconsider the current character even so it interrupted the sequence
1040
+ // it could be the beginning of a new sequence
1041
+ i--;
1042
+ }
1043
+
1044
+ break;
1045
+ case STATE.END:
1046
+ break;
1047
+ default:
1048
+ done(this._endUnexpected());
1049
+ return;
1050
+ }
1051
+ }
1052
+
1053
+ dataCallback('headerField');
1054
+ dataCallback('headerValue');
1055
+ dataCallback('partData');
1056
+
1057
+ this.index = index;
1058
+ this.state = state;
1059
+ this.flags = flags;
1060
+
1061
+ done();
1062
+ return this.bufferLength;
1063
+ }
1064
+
1065
+ explain() {
1066
+ return `state = ${MultipartParser.stateToString(this.state)}`;
1067
+ }
1068
+ }
1069
+
1070
+ // eslint-disable-next-line consistent-return
1071
+ MultipartParser.stateToString = (stateNumber) => {
1072
+ // eslint-disable-next-line no-restricted-syntax, guard-for-in
1073
+ for (const stateName in STATE) {
1074
+ const number = STATE[stateName];
1075
+ if (number === stateNumber) return stateName;
1076
+ }
1077
+ };
1078
+
1079
+ const MultipartParser$1 = Object.assign(MultipartParser, { STATES });
1080
+
1081
+ /* eslint-disable no-underscore-dangle */
1082
+
1083
+
1084
+ const multipartType = 'multipart';
1085
+ // the `options` is also available through the `options` / `formidable.options`
1086
+ function plugin$1(formidable, options) {
1087
+ // the `this` context is always formidable, as the first argument of a plugin
1088
+ // but this allows us to customize/test each plugin
1089
+
1090
+ /* istanbul ignore next */
1091
+ const self = this || formidable;
1092
+
1093
+ // NOTE: we (currently) support both multipart/form-data and multipart/related
1094
+ const multipart = /multipart/i.test(self.headers['content-type']);
1095
+
1096
+ if (multipart) {
1097
+ const m = self.headers['content-type'].match(
1098
+ /boundary=(?:"([^"]+)"|([^;]+))/i,
1099
+ );
1100
+ if (m) {
1101
+ const initMultipart = createInitMultipart(m[1] || m[2]);
1102
+ initMultipart.call(self, self, options); // lgtm [js/superfluous-trailing-arguments]
1103
+ } else {
1104
+ const err = new FormidableError(
1105
+ 'bad content-type header, no multipart boundary',
1106
+ missingMultipartBoundary,
1107
+ 400,
1108
+ );
1109
+ self._error(err);
1110
+ }
1111
+ }
1112
+ return self;
1113
+ }
1114
+
1115
+ // Note that it's a good practice (but it's up to you) to use the `this.options` instead
1116
+ // of the passed `options` (second) param, because when you decide
1117
+ // to test the plugin you can pass custom `this` context to it (and so `this.options`)
1118
+ function createInitMultipart(boundary) {
1119
+ return function initMultipart() {
1120
+ this.type = multipartType;
1121
+
1122
+ const parser = new MultipartParser$1(this.options);
1123
+ let headerField;
1124
+ let headerValue;
1125
+ let part;
1126
+
1127
+ parser.initWithBoundary(boundary);
1128
+
1129
+ // eslint-disable-next-line max-statements, consistent-return
1130
+ parser.on('data', async ({ name, buffer, start, end }) => {
1131
+ if (name === 'partBegin') {
1132
+ part = new Stream();
1133
+ part.readable = true;
1134
+ part.headers = {};
1135
+ part.name = null;
1136
+ part.originalFilename = null;
1137
+ part.mimetype = null;
1138
+
1139
+ part.transferEncoding = this.options.encoding;
1140
+ part.transferBuffer = '';
1141
+
1142
+ headerField = '';
1143
+ headerValue = '';
1144
+ } else if (name === 'headerField') {
1145
+ headerField += buffer.toString(this.options.encoding, start, end);
1146
+ } else if (name === 'headerValue') {
1147
+ headerValue += buffer.toString(this.options.encoding, start, end);
1148
+ } else if (name === 'headerEnd') {
1149
+ headerField = headerField.toLowerCase();
1150
+ part.headers[headerField] = headerValue;
1151
+
1152
+ // matches either a quoted-string or a token (RFC 2616 section 19.5.1)
1153
+ const m = headerValue.match(
1154
+ // eslint-disable-next-line no-useless-escape
1155
+ /\bname=("([^"]*)"|([^\(\)<>@,;:\\"\/\[\]\?=\{\}\s\t/]+))/i,
1156
+ );
1157
+ if (headerField === 'content-disposition') {
1158
+ if (m) {
1159
+ part.name = m[2] || m[3] || '';
1160
+ }
1161
+
1162
+ part.originalFilename = this._getFileName(headerValue);
1163
+ } else if (headerField === 'content-type') {
1164
+ part.mimetype = headerValue;
1165
+ } else if (headerField === 'content-transfer-encoding') {
1166
+ part.transferEncoding = headerValue.toLowerCase();
1167
+ }
1168
+
1169
+ headerField = '';
1170
+ headerValue = '';
1171
+ } else if (name === 'headersEnd') {
1172
+ switch (part.transferEncoding) {
1173
+ case 'binary':
1174
+ case '7bit':
1175
+ case '8bit':
1176
+ case 'utf-8': {
1177
+ const dataPropagation = (ctx) => {
1178
+ if (ctx.name === 'partData') {
1179
+ part.emit('data', ctx.buffer.slice(ctx.start, ctx.end));
1180
+ }
1181
+ };
1182
+ const dataStopPropagation = (ctx) => {
1183
+ if (ctx.name === 'partEnd') {
1184
+ part.emit('end');
1185
+ parser.off('data', dataPropagation);
1186
+ parser.off('data', dataStopPropagation);
1187
+ }
1188
+ };
1189
+ parser.on('data', dataPropagation);
1190
+ parser.on('data', dataStopPropagation);
1191
+ break;
1192
+ }
1193
+ case 'base64': {
1194
+ const dataPropagation = (ctx) => {
1195
+ if (ctx.name === 'partData') {
1196
+ part.transferBuffer += ctx.buffer
1197
+ .slice(ctx.start, ctx.end)
1198
+ .toString('ascii');
1199
+
1200
+ /*
1201
+ four bytes (chars) in base64 converts to three bytes in binary
1202
+ encoding. So we should always work with a number of bytes that
1203
+ can be divided by 4, it will result in a number of bytes that
1204
+ can be divided vy 3.
1205
+ */
1206
+ const offset = parseInt(part.transferBuffer.length / 4, 10) * 4;
1207
+ part.emit(
1208
+ 'data',
1209
+ Buffer.from(
1210
+ part.transferBuffer.substring(0, offset),
1211
+ 'base64',
1212
+ ),
1213
+ );
1214
+ part.transferBuffer = part.transferBuffer.substring(offset);
1215
+ }
1216
+ };
1217
+ const dataStopPropagation = (ctx) => {
1218
+ if (ctx.name === 'partEnd') {
1219
+ part.emit('data', Buffer.from(part.transferBuffer, 'base64'));
1220
+ part.emit('end');
1221
+ parser.off('data', dataPropagation);
1222
+ parser.off('data', dataStopPropagation);
1223
+ }
1224
+ };
1225
+ parser.on('data', dataPropagation);
1226
+ parser.on('data', dataStopPropagation);
1227
+ break;
1228
+ }
1229
+ default:
1230
+ return this._error(
1231
+ new FormidableError(
1232
+ 'unknown transfer-encoding',
1233
+ unknownTransferEncoding,
1234
+ 501,
1235
+ ),
1236
+ );
1237
+ }
1238
+ this._parser.pause();
1239
+ await this.onPart(part);
1240
+ this._parser.resume();
1241
+ } else if (name === 'end') {
1242
+ this.ended = true;
1243
+ this._maybeEnd();
1244
+ }
1245
+ });
1246
+
1247
+ this._parser = parser;
1248
+ };
1249
+ }
1250
+
1251
+ /* eslint-disable no-underscore-dangle */
1252
+
1253
+
1254
+ class JSONParser extends Transform {
1255
+ constructor(options = {}) {
1256
+ super({ readableObjectMode: true });
1257
+ this.chunks = [];
1258
+ this.globalOptions = { ...options };
1259
+ }
1260
+
1261
+ _transform(chunk, encoding, callback) {
1262
+ this.chunks.push(String(chunk)); // todo consider using a string decoder
1263
+ callback();
1264
+ }
1265
+
1266
+ _flush(callback) {
1267
+ try {
1268
+ const fields = JSON.parse(this.chunks.join(''));
1269
+ this.push(fields);
1270
+ } catch (e) {
1271
+ callback(e);
1272
+ return;
1273
+ }
1274
+ this.chunks = null;
1275
+ callback();
1276
+ }
1277
+ }
1278
+
1279
+ /* eslint-disable no-underscore-dangle */
1280
+
1281
+
1282
+ const jsonType = 'json';
1283
+ // the `options` is also available through the `this.options` / `formidable.options`
1284
+ function plugin(formidable, options) {
1285
+ // the `this` context is always formidable, as the first argument of a plugin
1286
+ // but this allows us to customize/test each plugin
1287
+
1288
+ /* istanbul ignore next */
1289
+ const self = this || formidable;
1290
+
1291
+ if (/json/i.test(self.headers['content-type'])) {
1292
+ init.call(self, self, options);
1293
+ }
1294
+
1295
+ return self;
1296
+ }
1297
+ // Note that it's a good practice (but it's up to you) to use the `this.options` instead
1298
+ // of the passed `options` (second) param, because when you decide
1299
+ // to test the plugin you can pass custom `this` context to it (and so `this.options`)
1300
+ function init(_self, _opts) {
1301
+ this.type = jsonType;
1302
+
1303
+ const parser = new JSONParser(this.options);
1304
+
1305
+ parser.on('data', (fields) => {
1306
+ this.fields = fields;
1307
+ });
1308
+
1309
+ parser.once('end', () => {
1310
+ this.ended = true;
1311
+ this._maybeEnd();
1312
+ });
1313
+
1314
+ this._parser = parser;
1315
+ }
1316
+
1317
+ /* eslint-disable no-underscore-dangle */
1318
+
1319
+
1320
+ class DummyParser extends Transform {
1321
+ constructor(incomingForm, options = {}) {
1322
+ super();
1323
+ this.globalOptions = { ...options };
1324
+ this.incomingForm = incomingForm;
1325
+ }
1326
+
1327
+ _flush(callback) {
1328
+ this.incomingForm.ended = true;
1329
+ this.incomingForm._maybeEnd();
1330
+ callback();
1331
+ }
1332
+ }
1333
+
1334
+ /* eslint-disable class-methods-use-this */
1335
+ /* eslint-disable no-underscore-dangle */
1336
+
1337
+
1338
+ const toHexoId = hexoid(25);
1339
+ const DEFAULT_OPTIONS = {
1340
+ maxFields: 1000,
1341
+ maxFieldsSize: 20 * 1024 * 1024,
1342
+ maxFiles: Infinity,
1343
+ maxFileSize: 200 * 1024 * 1024,
1344
+ maxTotalFileSize: undefined,
1345
+ minFileSize: 1,
1346
+ allowEmptyFiles: false,
1347
+ createDirsFromUploads: false,
1348
+ keepExtensions: false,
1349
+ encoding: 'utf-8',
1350
+ hashAlgorithm: false,
1351
+ uploadDir: os.tmpdir(),
1352
+ enabledPlugins: [plugin$3, plugin$2, plugin$1, plugin],
1353
+ fileWriteStreamHandler: null,
1354
+ defaultInvalidName: 'invalid-name',
1355
+ filter(_part) {
1356
+ return true;
1357
+ },
1358
+ filename: undefined,
1359
+ };
1360
+
1361
+ function hasOwnProp(obj, key) {
1362
+ return Object.prototype.hasOwnProperty.call(obj, key);
1363
+ }
1364
+
1365
+
1366
+ const decorateForceSequential = function (promiseCreator) {
1367
+ /* forces a function that returns a promise to be sequential
1368
+ useful for fs for example */
1369
+ let lastPromise = Promise.resolve();
1370
+ return async function (...x) {
1371
+ const promiseWeAreWaitingFor = lastPromise;
1372
+ let currentPromise;
1373
+ let callback;
1374
+ // we need to change lastPromise before await anything,
1375
+ // otherwise 2 calls might wait the same thing
1376
+ lastPromise = new Promise(function (resolve) {
1377
+ callback = resolve;
1378
+ });
1379
+ await promiseWeAreWaitingFor;
1380
+ currentPromise = promiseCreator(...x);
1381
+ currentPromise.then(callback).catch(callback);
1382
+ return currentPromise;
1383
+ };
1384
+ };
1385
+
1386
+ const createNecessaryDirectoriesAsync = decorateForceSequential(function (filePath) {
1387
+ const directoryname = path.dirname(filePath);
1388
+ return fsPromises.mkdir(directoryname, { recursive: true });
1389
+ });
1390
+
1391
+ const invalidExtensionChar = (c) => {
1392
+ const code = c.charCodeAt(0);
1393
+ return !(
1394
+ code === 46 || // .
1395
+ (code >= 48 && code <= 57) ||
1396
+ (code >= 65 && code <= 90) ||
1397
+ (code >= 97 && code <= 122)
1398
+ );
1399
+ };
1400
+
1401
+ class IncomingForm extends EventEmitter {
1402
+ constructor(options = {}) {
1403
+ super();
1404
+
1405
+ this.options = { ...DEFAULT_OPTIONS, ...options };
1406
+ if (!this.options.maxTotalFileSize) {
1407
+ this.options.maxTotalFileSize = this.options.maxFileSize;
1408
+ }
1409
+
1410
+ const dir = path.resolve(
1411
+ this.options.uploadDir || this.options.uploaddir || os.tmpdir(),
1412
+ );
1413
+
1414
+ this.uploaddir = dir;
1415
+ this.uploadDir = dir;
1416
+
1417
+ // initialize with null
1418
+ [
1419
+ 'error',
1420
+ 'headers',
1421
+ 'type',
1422
+ 'bytesExpected',
1423
+ 'bytesReceived',
1424
+ '_parser',
1425
+ 'req',
1426
+ ].forEach((key) => {
1427
+ this[key] = null;
1428
+ });
1429
+
1430
+ this._setUpRename();
1431
+
1432
+ this._flushing = 0;
1433
+ this._fieldsSize = 0;
1434
+ this._totalFileSize = 0;
1435
+ this._plugins = [];
1436
+ this.openedFiles = [];
1437
+
1438
+ this.options.enabledPlugins = []
1439
+ .concat(this.options.enabledPlugins)
1440
+ .filter(Boolean);
1441
+
1442
+ if (this.options.enabledPlugins.length === 0) {
1443
+ throw new FormidableError(
1444
+ 'expect at least 1 enabled builtin plugin, see options.enabledPlugins',
1445
+ missingPlugin,
1446
+ );
1447
+ }
1448
+
1449
+ this.options.enabledPlugins.forEach((plugin) => {
1450
+ this.use(plugin);
1451
+ });
1452
+
1453
+ this._setUpMaxFields();
1454
+ this._setUpMaxFiles();
1455
+ this.ended = undefined;
1456
+ this.type = undefined;
1457
+ }
1458
+
1459
+ use(plugin) {
1460
+ if (typeof plugin !== 'function') {
1461
+ throw new FormidableError(
1462
+ '.use: expect `plugin` to be a function',
1463
+ pluginFunction,
1464
+ );
1465
+ }
1466
+ this._plugins.push(plugin.bind(this));
1467
+ return this;
1468
+ }
1469
+
1470
+ pause () {
1471
+ try {
1472
+ this.req.pause();
1473
+ } catch (err) {
1474
+ // the stream was destroyed
1475
+ if (!this.ended) {
1476
+ // before it was completed, crash & burn
1477
+ this._error(err);
1478
+ }
1479
+ return false;
1480
+ }
1481
+ return true;
1482
+ }
1483
+
1484
+ resume () {
1485
+ try {
1486
+ this.req.resume();
1487
+ } catch (err) {
1488
+ // the stream was destroyed
1489
+ if (!this.ended) {
1490
+ // before it was completed, crash & burn
1491
+ this._error(err);
1492
+ }
1493
+ return false;
1494
+ }
1495
+
1496
+ return true;
1497
+ }
1498
+
1499
+ // returns a promise if no callback is provided
1500
+ async parse(req, cb) {
1501
+ this.req = req;
1502
+ let promise;
1503
+
1504
+ // Setup callback first, so we don't miss anything from data events emitted immediately.
1505
+ if (!cb) {
1506
+ let resolveRef;
1507
+ let rejectRef;
1508
+ promise = new Promise((resolve, reject) => {
1509
+ resolveRef = resolve;
1510
+ rejectRef = reject;
1511
+ });
1512
+ cb = (err, fields, files) => {
1513
+ if (err) {
1514
+ rejectRef(err);
1515
+ } else {
1516
+ resolveRef([fields, files]);
1517
+ }
1518
+ };
1519
+ }
1520
+ const callback = __jsenv_default_import__$1(__jsenv_default_import__(cb));
1521
+ this.fields = {};
1522
+ const files = {};
1523
+
1524
+ this.on('field', (name, value) => {
1525
+ if (this.type === 'multipart' || this.type === 'urlencoded') {
1526
+ if (!hasOwnProp(this.fields, name)) {
1527
+ this.fields[name] = [value];
1528
+ } else {
1529
+ this.fields[name].push(value);
1530
+ }
1531
+ } else {
1532
+ this.fields[name] = value;
1533
+ }
1534
+ });
1535
+ this.on('file', (name, file) => {
1536
+ if (!hasOwnProp(files, name)) {
1537
+ files[name] = [file];
1538
+ } else {
1539
+ files[name].push(file);
1540
+ }
1541
+ });
1542
+ this.on('error', (err) => {
1543
+ callback(err, this.fields, files);
1544
+ });
1545
+ this.on('end', () => {
1546
+ callback(null, this.fields, files);
1547
+ });
1548
+
1549
+ // Parse headers and setup the parser, ready to start listening for data.
1550
+ await this.writeHeaders(req.headers);
1551
+
1552
+ // Start listening for data.
1553
+ req
1554
+ .on('error', (err) => {
1555
+ this._error(err);
1556
+ })
1557
+ .on('aborted', () => {
1558
+ this.emit('aborted');
1559
+ this._error(new FormidableError('Request aborted', aborted));
1560
+ })
1561
+ .on('data', (buffer) => {
1562
+ try {
1563
+ this.write(buffer);
1564
+ } catch (err) {
1565
+ this._error(err);
1566
+ }
1567
+ })
1568
+ .on('end', () => {
1569
+ if (this.error) {
1570
+ return;
1571
+ }
1572
+ if (this._parser) {
1573
+ this._parser.end();
1574
+ }
1575
+ });
1576
+ if (promise) {
1577
+ return promise;
1578
+ }
1579
+ return this;
1580
+ }
1581
+
1582
+ async writeHeaders(headers) {
1583
+ this.headers = headers;
1584
+ this._parseContentLength();
1585
+ await this._parseContentType();
1586
+
1587
+ if (!this._parser) {
1588
+ this._error(
1589
+ new FormidableError(
1590
+ 'no parser found',
1591
+ noParser,
1592
+ 415, // Unsupported Media Type
1593
+ ),
1594
+ );
1595
+ return;
1596
+ }
1597
+
1598
+ this._parser.once('error', (error) => {
1599
+ this._error(error);
1600
+ });
1601
+ }
1602
+
1603
+ write(buffer) {
1604
+ if (this.error) {
1605
+ return null;
1606
+ }
1607
+ if (!this._parser) {
1608
+ this._error(
1609
+ new FormidableError('uninitialized parser', uninitializedParser),
1610
+ );
1611
+ return null;
1612
+ }
1613
+
1614
+ this.bytesReceived += buffer.length;
1615
+ this.emit('progress', this.bytesReceived, this.bytesExpected);
1616
+
1617
+ this._parser.write(buffer);
1618
+
1619
+ return this.bytesReceived;
1620
+ }
1621
+
1622
+ onPart(part) {
1623
+ // this method can be overwritten by the user
1624
+ return this._handlePart(part);
1625
+ }
1626
+
1627
+ async _handlePart(part) {
1628
+ if (part.originalFilename && typeof part.originalFilename !== 'string') {
1629
+ this._error(
1630
+ new FormidableError(
1631
+ `the part.originalFilename should be string when it exists`,
1632
+ filenameNotString,
1633
+ ),
1634
+ );
1635
+ return;
1636
+ }
1637
+
1638
+ // This MUST check exactly for undefined. You can not change it to !part.originalFilename.
1639
+
1640
+ // todo: uncomment when switch tests to Jest
1641
+ // console.log(part);
1642
+
1643
+ // ? NOTE(@tunnckocore): no it can be any falsey value, it most probably depends on what's returned
1644
+ // from somewhere else. Where recently I changed the return statements
1645
+ // and such thing because code style
1646
+ // ? NOTE(@tunnckocore): or even better, if there is no mimetype, then it's for sure a field
1647
+ // ? NOTE(@tunnckocore): originalFilename is an empty string when a field?
1648
+ if (!part.mimetype) {
1649
+ let value = '';
1650
+ const decoder = new StringDecoder(
1651
+ part.transferEncoding || this.options.encoding,
1652
+ );
1653
+
1654
+ part.on('data', (buffer) => {
1655
+ this._fieldsSize += buffer.length;
1656
+ if (this._fieldsSize > this.options.maxFieldsSize) {
1657
+ this._error(
1658
+ new FormidableError(
1659
+ `options.maxFieldsSize (${this.options.maxFieldsSize} bytes) exceeded, received ${this._fieldsSize} bytes of field data`,
1660
+ maxFieldsSizeExceeded,
1661
+ 413, // Payload Too Large
1662
+ ),
1663
+ );
1664
+ return;
1665
+ }
1666
+ value += decoder.write(buffer);
1667
+ });
1668
+
1669
+ part.on('end', () => {
1670
+ this.emit('field', part.name, value);
1671
+ });
1672
+ return;
1673
+ }
1674
+
1675
+ if (!this.options.filter(part)) {
1676
+ return;
1677
+ }
1678
+
1679
+ this._flushing += 1;
1680
+
1681
+ let fileSize = 0;
1682
+ const newFilename = this._getNewName(part);
1683
+ const filepath = this._joinDirectoryName(newFilename);
1684
+ const file = await this._newFile({
1685
+ newFilename,
1686
+ filepath,
1687
+ originalFilename: part.originalFilename,
1688
+ mimetype: part.mimetype,
1689
+ });
1690
+ file.on('error', (err) => {
1691
+ this._error(err);
1692
+ });
1693
+ this.emit('fileBegin', part.name, file);
1694
+
1695
+ file.open();
1696
+ this.openedFiles.push(file);
1697
+
1698
+ part.on('data', (buffer) => {
1699
+ this._totalFileSize += buffer.length;
1700
+ fileSize += buffer.length;
1701
+
1702
+ if (this._totalFileSize > this.options.maxTotalFileSize) {
1703
+ this._error(
1704
+ new FormidableError(
1705
+ `options.maxTotalFileSize (${this.options.maxTotalFileSize} bytes) exceeded, received ${this._totalFileSize} bytes of file data`,
1706
+ biggerThanTotalMaxFileSize,
1707
+ 413,
1708
+ ),
1709
+ );
1710
+ return;
1711
+ }
1712
+ if (buffer.length === 0) {
1713
+ return;
1714
+ }
1715
+ this.pause();
1716
+ file.write(buffer, () => {
1717
+ this.resume();
1718
+ });
1719
+ });
1720
+
1721
+ part.on('end', () => {
1722
+ if (!this.options.allowEmptyFiles && fileSize === 0) {
1723
+ this._error(
1724
+ new FormidableError(
1725
+ `options.allowEmptyFiles is false, file size should be greater than 0`,
1726
+ noEmptyFiles,
1727
+ 400,
1728
+ ),
1729
+ );
1730
+ return;
1731
+ }
1732
+ if (fileSize < this.options.minFileSize) {
1733
+ this._error(
1734
+ new FormidableError(
1735
+ `options.minFileSize (${this.options.minFileSize} bytes) inferior, received ${fileSize} bytes of file data`,
1736
+ smallerThanMinFileSize,
1737
+ 400,
1738
+ ),
1739
+ );
1740
+ return;
1741
+ }
1742
+ if (fileSize > this.options.maxFileSize) {
1743
+ this._error(
1744
+ new FormidableError(
1745
+ `options.maxFileSize (${this.options.maxFileSize} bytes), received ${fileSize} bytes of file data`,
1746
+ biggerThanMaxFileSize,
1747
+ 413,
1748
+ ),
1749
+ );
1750
+ return;
1751
+ }
1752
+
1753
+ file.end(() => {
1754
+ this._flushing -= 1;
1755
+ this.emit('file', part.name, file);
1756
+ this._maybeEnd();
1757
+ });
1758
+ });
1759
+ }
1760
+
1761
+ // eslint-disable-next-line max-statements
1762
+ async _parseContentType() {
1763
+ if (this.bytesExpected === 0) {
1764
+ this._parser = new DummyParser(this, this.options);
1765
+ return;
1766
+ }
1767
+
1768
+ if (!this.headers['content-type']) {
1769
+ this._error(
1770
+ new FormidableError(
1771
+ 'bad content-type header, no content-type',
1772
+ missingContentType,
1773
+ 400,
1774
+ ),
1775
+ );
1776
+ return;
1777
+ }
1778
+
1779
+
1780
+ new DummyParser(this, this.options);
1781
+
1782
+ const results = [];
1783
+ await Promise.all(this._plugins.map(async (plugin, idx) => {
1784
+ let pluginReturn = null;
1785
+ try {
1786
+ pluginReturn = await plugin(this, this.options) || this;
1787
+ } catch (err) {
1788
+ // directly throw from the `form.parse` method;
1789
+ // there is no other better way, except a handle through options
1790
+ const error = new FormidableError(
1791
+ `plugin on index ${idx} failed with: ${err.message}`,
1792
+ pluginFailed,
1793
+ 500,
1794
+ );
1795
+ error.idx = idx;
1796
+ throw error;
1797
+ }
1798
+ Object.assign(this, pluginReturn);
1799
+
1800
+ // todo: use Set/Map and pass plugin name instead of the `idx` index
1801
+ this.emit('plugin', idx, pluginReturn);
1802
+ }));
1803
+ this.emit('pluginsResults', results);
1804
+ }
1805
+
1806
+ _error(err, eventName = 'error') {
1807
+ if (this.error || this.ended) {
1808
+ return;
1809
+ }
1810
+
1811
+ this.req = null;
1812
+ this.error = err;
1813
+ this.emit(eventName, err);
1814
+
1815
+ this.openedFiles.forEach((file) => {
1816
+ file.destroy();
1817
+ });
1818
+ }
1819
+
1820
+ _parseContentLength() {
1821
+ this.bytesReceived = 0;
1822
+ if (this.headers['content-length']) {
1823
+ this.bytesExpected = parseInt(this.headers['content-length'], 10);
1824
+ } else if (this.headers['transfer-encoding'] === undefined) {
1825
+ this.bytesExpected = 0;
1826
+ }
1827
+
1828
+ if (this.bytesExpected !== null) {
1829
+ this.emit('progress', this.bytesReceived, this.bytesExpected);
1830
+ }
1831
+ }
1832
+
1833
+ _newParser() {
1834
+ return new MultipartParser$1(this.options);
1835
+ }
1836
+
1837
+ async _newFile({ filepath, originalFilename, mimetype, newFilename }) {
1838
+ if (this.options.fileWriteStreamHandler) {
1839
+ return new VolatileFile({
1840
+ newFilename,
1841
+ filepath,
1842
+ originalFilename,
1843
+ mimetype,
1844
+ createFileWriteStream: this.options.fileWriteStreamHandler,
1845
+ hashAlgorithm: this.options.hashAlgorithm,
1846
+ });
1847
+ }
1848
+ if (this.options.createDirsFromUploads) {
1849
+ try {
1850
+ await createNecessaryDirectoriesAsync(filepath);
1851
+ } catch (errorCreatingDir) {
1852
+ this._error(new FormidableError(
1853
+ `cannot create directory`,
1854
+ cannotCreateDir,
1855
+ 409,
1856
+ ));
1857
+ }
1858
+ }
1859
+ return new PersistentFile({
1860
+ newFilename,
1861
+ filepath,
1862
+ originalFilename,
1863
+ mimetype,
1864
+ hashAlgorithm: this.options.hashAlgorithm,
1865
+ });
1866
+ }
1867
+
1868
+ _getFileName(headerValue) {
1869
+ // matches either a quoted-string or a token (RFC 2616 section 19.5.1)
1870
+ const m = headerValue.match(
1871
+ /\bfilename=("(.*?)"|([^()<>{}[\]@,;:"?=\s/\t]+))($|;\s)/i,
1872
+ );
1873
+ if (!m) return null;
1874
+
1875
+ const match = m[2] || m[3] || '';
1876
+ let originalFilename = match.substr(match.lastIndexOf('\\') + 1);
1877
+ originalFilename = originalFilename.replace(/%22/g, '"');
1878
+ originalFilename = originalFilename.replace(/&#([\d]{4});/g, (_, code) =>
1879
+ String.fromCharCode(code),
1880
+ );
1881
+
1882
+ return originalFilename;
1883
+ }
1884
+
1885
+ // able to get composed extension with multiple dots
1886
+ // "a.b.c" -> ".b.c"
1887
+ // as opposed to path.extname -> ".c"
1888
+ _getExtension(str) {
1889
+ if (!str) {
1890
+ return '';
1891
+ }
1892
+
1893
+ const basename = path.basename(str);
1894
+ const firstDot = basename.indexOf('.');
1895
+ const lastDot = basename.lastIndexOf('.');
1896
+ let rawExtname = path.extname(basename);
1897
+
1898
+ if (firstDot !== lastDot) {
1899
+ rawExtname = basename.slice(firstDot);
1900
+ }
1901
+
1902
+ let filtered;
1903
+ const firstInvalidIndex = Array.from(rawExtname).findIndex(invalidExtensionChar);
1904
+ if (firstInvalidIndex === -1) {
1905
+ filtered = rawExtname;
1906
+ } else {
1907
+ filtered = rawExtname.substring(0, firstInvalidIndex);
1908
+ }
1909
+ if (filtered === '.') {
1910
+ return '';
1911
+ }
1912
+ return filtered;
1913
+ }
1914
+
1915
+ _joinDirectoryName(name) {
1916
+ const newPath = path.join(this.uploadDir, name);
1917
+
1918
+ // prevent directory traversal attacks
1919
+ if (!newPath.startsWith(this.uploadDir)) {
1920
+ return path.join(this.uploadDir, this.options.defaultInvalidName);
1921
+ }
1922
+
1923
+ return newPath;
1924
+ }
1925
+
1926
+ _setUpRename() {
1927
+ const hasRename = typeof this.options.filename === 'function';
1928
+ if (hasRename) {
1929
+ this._getNewName = (part) => {
1930
+ let ext = '';
1931
+ let name = this.options.defaultInvalidName;
1932
+ if (part.originalFilename) {
1933
+ // can be null
1934
+ ({ ext, name } = path.parse(part.originalFilename));
1935
+ if (this.options.keepExtensions !== true) {
1936
+ ext = '';
1937
+ }
1938
+ }
1939
+ return this.options.filename.call(this, name, ext, part, this);
1940
+ };
1941
+ } else {
1942
+ this._getNewName = (part) => {
1943
+ const name = toHexoId();
1944
+
1945
+ if (part && this.options.keepExtensions) {
1946
+ const originalFilename =
1947
+ typeof part === 'string' ? part : part.originalFilename;
1948
+ return `${name}${this._getExtension(originalFilename)}`;
1949
+ }
1950
+
1951
+ return name;
1952
+ };
1953
+ }
1954
+ }
1955
+
1956
+ _setUpMaxFields() {
1957
+ if (this.options.maxFields !== Infinity) {
1958
+ let fieldsCount = 0;
1959
+ this.on('field', () => {
1960
+ fieldsCount += 1;
1961
+ if (fieldsCount > this.options.maxFields) {
1962
+ this._error(
1963
+ new FormidableError(
1964
+ `options.maxFields (${this.options.maxFields}) exceeded`,
1965
+ maxFieldsExceeded,
1966
+ 413,
1967
+ ),
1968
+ );
1969
+ }
1970
+ });
1971
+ }
1972
+ }
1973
+
1974
+ _setUpMaxFiles() {
1975
+ if (this.options.maxFiles !== Infinity) {
1976
+ let fileCount = 0;
1977
+ this.on('fileBegin', () => {
1978
+ fileCount += 1;
1979
+ if (fileCount > this.options.maxFiles) {
1980
+ this._error(
1981
+ new FormidableError(
1982
+ `options.maxFiles (${this.options.maxFiles}) exceeded`,
1983
+ maxFilesExceeded,
1984
+ 413,
1985
+ ),
1986
+ );
1987
+ }
1988
+ });
1989
+ }
1990
+ }
1991
+
1992
+ _maybeEnd() {
1993
+ if (!this.ended || this._flushing || this.error) {
1994
+ return;
1995
+ }
1996
+ this.req = null;
1997
+ this.emit('end');
1998
+ }
1999
+ }
2000
+
2001
+ export { DEFAULT_OPTIONS, DummyParser, IncomingForm, JSONParser, MultipartParser$1 as MultipartParser, OctetStreamParser, PersistentFile, QuerystringParser, VolatileFile, plugin$3 as plugin, plugin$2 as plugin$1, plugin$1 as plugin$2, plugin as plugin$3 };