source: frontend/node_modules/form-data/lib/form_data.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 14.2 KB
Line 
1'use strict';
2
3var CombinedStream = require('combined-stream');
4var util = require('util');
5var path = require('path');
6var http = require('http');
7var https = require('https');
8var parseUrl = require('url').parse;
9var fs = require('fs');
10var Stream = require('stream').Stream;
11var crypto = require('crypto');
12var mime = require('mime-types');
13var asynckit = require('asynckit');
14var setToStringTag = require('es-set-tostringtag');
15var hasOwn = require('hasown');
16var populate = require('./populate.js');
17
18/**
19 * Create readable "multipart/form-data" streams.
20 * Can be used to submit forms
21 * and file uploads to other web applications.
22 *
23 * @constructor
24 * @param {object} options - Properties to be added/overriden for FormData and CombinedStream
25 */
26function FormData(options) {
27 if (!(this instanceof FormData)) {
28 return new FormData(options);
29 }
30
31 this._overheadLength = 0;
32 this._valueLength = 0;
33 this._valuesToMeasure = [];
34
35 CombinedStream.call(this);
36
37 options = options || {}; // eslint-disable-line no-param-reassign
38 for (var option in options) { // eslint-disable-line no-restricted-syntax
39 this[option] = options[option];
40 }
41}
42
43// make it a Stream
44util.inherits(FormData, CombinedStream);
45
46FormData.LINE_BREAK = '\r\n';
47FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream';
48
49FormData.prototype.append = function (field, value, options) {
50 options = options || {}; // eslint-disable-line no-param-reassign
51
52 // allow filename as single option
53 if (typeof options === 'string') {
54 options = { filename: options }; // eslint-disable-line no-param-reassign
55 }
56
57 var append = CombinedStream.prototype.append.bind(this);
58
59 // all that streamy business can't handle numbers
60 if (typeof value === 'number' || value == null) {
61 value = String(value); // eslint-disable-line no-param-reassign
62 }
63
64 // https://github.com/felixge/node-form-data/issues/38
65 if (Array.isArray(value)) {
66 /*
67 * Please convert your array into string
68 * the way web server expects it
69 */
70 this._error(new Error('Arrays are not supported.'));
71 return;
72 }
73
74 var header = this._multiPartHeader(field, value, options);
75 var footer = this._multiPartFooter();
76
77 append(header);
78 append(value);
79 append(footer);
80
81 // pass along options.knownLength
82 this._trackLength(header, value, options);
83};
84
85FormData.prototype._trackLength = function (header, value, options) {
86 var valueLength = 0;
87
88 /*
89 * used w/ getLengthSync(), when length is known.
90 * e.g. for streaming directly from a remote server,
91 * w/ a known file a size, and not wanting to wait for
92 * incoming file to finish to get its size.
93 */
94 if (options.knownLength != null) {
95 valueLength += Number(options.knownLength);
96 } else if (Buffer.isBuffer(value)) {
97 valueLength = value.length;
98 } else if (typeof value === 'string') {
99 valueLength = Buffer.byteLength(value);
100 }
101
102 this._valueLength += valueLength;
103
104 // @check why add CRLF? does this account for custom/multiple CRLFs?
105 this._overheadLength += Buffer.byteLength(header) + FormData.LINE_BREAK.length;
106
107 // empty or either doesn't have path or not an http response or not a stream
108 if (!value || (!value.path && !(value.readable && hasOwn(value, 'httpVersion')) && !(value instanceof Stream))) {
109 return;
110 }
111
112 // no need to bother with the length
113 if (!options.knownLength) {
114 this._valuesToMeasure.push(value);
115 }
116};
117
118FormData.prototype._lengthRetriever = function (value, callback) {
119 if (hasOwn(value, 'fd')) {
120 // take read range into a account
121 // `end` = Infinity –> read file till the end
122 //
123 // TODO: Looks like there is bug in Node fs.createReadStream
124 // it doesn't respect `end` options without `start` options
125 // Fix it when node fixes it.
126 // https://github.com/joyent/node/issues/7819
127 if (value.end != undefined && value.end != Infinity && value.start != undefined) {
128 // when end specified
129 // no need to calculate range
130 // inclusive, starts with 0
131 callback(null, value.end + 1 - (value.start ? value.start : 0)); // eslint-disable-line callback-return
132
133 // not that fast snoopy
134 } else {
135 // still need to fetch file size from fs
136 fs.stat(value.path, function (err, stat) {
137 if (err) {
138 callback(err);
139 return;
140 }
141
142 // update final size based on the range options
143 var fileSize = stat.size - (value.start ? value.start : 0);
144 callback(null, fileSize);
145 });
146 }
147
148 // or http response
149 } else if (hasOwn(value, 'httpVersion')) {
150 callback(null, Number(value.headers['content-length'])); // eslint-disable-line callback-return
151
152 // or request stream http://github.com/mikeal/request
153 } else if (hasOwn(value, 'httpModule')) {
154 // wait till response come back
155 value.on('response', function (response) {
156 value.pause();
157 callback(null, Number(response.headers['content-length']));
158 });
159 value.resume();
160
161 // something else
162 } else {
163 callback('Unknown stream'); // eslint-disable-line callback-return
164 }
165};
166
167FormData.prototype._multiPartHeader = function (field, value, options) {
168 /*
169 * custom header specified (as string)?
170 * it becomes responsible for boundary
171 * (e.g. to handle extra CRLFs on .NET servers)
172 */
173 if (typeof options.header === 'string') {
174 return options.header;
175 }
176
177 var contentDisposition = this._getContentDisposition(value, options);
178 var contentType = this._getContentType(value, options);
179
180 var contents = '';
181 var headers = {
182 // add custom disposition as third element or keep it two elements if not
183 'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []),
184 // if no content type. allow it to be empty array
185 'Content-Type': [].concat(contentType || [])
186 };
187
188 // allow custom headers.
189 if (typeof options.header === 'object') {
190 populate(headers, options.header);
191 }
192
193 var header;
194 for (var prop in headers) { // eslint-disable-line no-restricted-syntax
195 if (hasOwn(headers, prop)) {
196 header = headers[prop];
197
198 // skip nullish headers.
199 if (header == null) {
200 continue; // eslint-disable-line no-restricted-syntax, no-continue
201 }
202
203 // convert all headers to arrays.
204 if (!Array.isArray(header)) {
205 header = [header];
206 }
207
208 // add non-empty headers.
209 if (header.length) {
210 contents += prop + ': ' + header.join('; ') + FormData.LINE_BREAK;
211 }
212 }
213 }
214
215 return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK;
216};
217
218FormData.prototype._getContentDisposition = function (value, options) { // eslint-disable-line consistent-return
219 var filename;
220
221 if (typeof options.filepath === 'string') {
222 // custom filepath for relative paths
223 filename = path.normalize(options.filepath).replace(/\\/g, '/');
224 } else if (options.filename || (value && (value.name || value.path))) {
225 /*
226 * custom filename take precedence
227 * formidable and the browser add a name property
228 * fs- and request- streams have path property
229 */
230 filename = path.basename(options.filename || (value && (value.name || value.path)));
231 } else if (value && value.readable && hasOwn(value, 'httpVersion')) {
232 // or try http response
233 filename = path.basename(value.client._httpMessage.path || '');
234 }
235
236 if (filename) {
237 return 'filename="' + filename + '"';
238 }
239};
240
241FormData.prototype._getContentType = function (value, options) {
242 // use custom content-type above all
243 var contentType = options.contentType;
244
245 // or try `name` from formidable, browser
246 if (!contentType && value && value.name) {
247 contentType = mime.lookup(value.name);
248 }
249
250 // or try `path` from fs-, request- streams
251 if (!contentType && value && value.path) {
252 contentType = mime.lookup(value.path);
253 }
254
255 // or if it's http-reponse
256 if (!contentType && value && value.readable && hasOwn(value, 'httpVersion')) {
257 contentType = value.headers['content-type'];
258 }
259
260 // or guess it from the filepath or filename
261 if (!contentType && (options.filepath || options.filename)) {
262 contentType = mime.lookup(options.filepath || options.filename);
263 }
264
265 // fallback to the default content type if `value` is not simple value
266 if (!contentType && value && typeof value === 'object') {
267 contentType = FormData.DEFAULT_CONTENT_TYPE;
268 }
269
270 return contentType;
271};
272
273FormData.prototype._multiPartFooter = function () {
274 return function (next) {
275 var footer = FormData.LINE_BREAK;
276
277 var lastPart = this._streams.length === 0;
278 if (lastPart) {
279 footer += this._lastBoundary();
280 }
281
282 next(footer);
283 }.bind(this);
284};
285
286FormData.prototype._lastBoundary = function () {
287 return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK;
288};
289
290FormData.prototype.getHeaders = function (userHeaders) {
291 var header;
292 var formHeaders = {
293 'content-type': 'multipart/form-data; boundary=' + this.getBoundary()
294 };
295
296 for (header in userHeaders) { // eslint-disable-line no-restricted-syntax
297 if (hasOwn(userHeaders, header)) {
298 formHeaders[header.toLowerCase()] = userHeaders[header];
299 }
300 }
301
302 return formHeaders;
303};
304
305FormData.prototype.setBoundary = function (boundary) {
306 if (typeof boundary !== 'string') {
307 throw new TypeError('FormData boundary must be a string');
308 }
309 this._boundary = boundary;
310};
311
312FormData.prototype.getBoundary = function () {
313 if (!this._boundary) {
314 this._generateBoundary();
315 }
316
317 return this._boundary;
318};
319
320FormData.prototype.getBuffer = function () {
321 var dataBuffer = new Buffer.alloc(0); // eslint-disable-line new-cap
322 var boundary = this.getBoundary();
323
324 // Create the form content. Add Line breaks to the end of data.
325 for (var i = 0, len = this._streams.length; i < len; i++) {
326 if (typeof this._streams[i] !== 'function') {
327 // Add content to the buffer.
328 if (Buffer.isBuffer(this._streams[i])) {
329 dataBuffer = Buffer.concat([dataBuffer, this._streams[i]]);
330 } else {
331 dataBuffer = Buffer.concat([dataBuffer, Buffer.from(this._streams[i])]);
332 }
333
334 // Add break after content.
335 if (typeof this._streams[i] !== 'string' || this._streams[i].substring(2, boundary.length + 2) !== boundary) {
336 dataBuffer = Buffer.concat([dataBuffer, Buffer.from(FormData.LINE_BREAK)]);
337 }
338 }
339 }
340
341 // Add the footer and return the Buffer object.
342 return Buffer.concat([dataBuffer, Buffer.from(this._lastBoundary())]);
343};
344
345FormData.prototype._generateBoundary = function () {
346 // This generates a 50 character boundary similar to those used by Firefox.
347
348 // They are optimized for boyer-moore parsing.
349 this._boundary = '--------------------------' + crypto.randomBytes(12).toString('hex');
350};
351
352// Note: getLengthSync DOESN'T calculate streams length
353// As workaround one can calculate file size manually and add it as knownLength option
354FormData.prototype.getLengthSync = function () {
355 var knownLength = this._overheadLength + this._valueLength;
356
357 // Don't get confused, there are 3 "internal" streams for each keyval pair so it basically checks if there is any value added to the form
358 if (this._streams.length) {
359 knownLength += this._lastBoundary().length;
360 }
361
362 // https://github.com/form-data/form-data/issues/40
363 if (!this.hasKnownLength()) {
364 /*
365 * Some async length retrievers are present
366 * therefore synchronous length calculation is false.
367 * Please use getLength(callback) to get proper length
368 */
369 this._error(new Error('Cannot calculate proper length in synchronous way.'));
370 }
371
372 return knownLength;
373};
374
375// Public API to check if length of added values is known
376// https://github.com/form-data/form-data/issues/196
377// https://github.com/form-data/form-data/issues/262
378FormData.prototype.hasKnownLength = function () {
379 var hasKnownLength = true;
380
381 if (this._valuesToMeasure.length) {
382 hasKnownLength = false;
383 }
384
385 return hasKnownLength;
386};
387
388FormData.prototype.getLength = function (cb) {
389 var knownLength = this._overheadLength + this._valueLength;
390
391 if (this._streams.length) {
392 knownLength += this._lastBoundary().length;
393 }
394
395 if (!this._valuesToMeasure.length) {
396 process.nextTick(cb.bind(this, null, knownLength));
397 return;
398 }
399
400 asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function (err, values) {
401 if (err) {
402 cb(err);
403 return;
404 }
405
406 values.forEach(function (length) {
407 knownLength += length;
408 });
409
410 cb(null, knownLength);
411 });
412};
413
414FormData.prototype.submit = function (params, cb) {
415 var request;
416 var options;
417 var defaults = { method: 'post' };
418
419 // parse provided url if it's string or treat it as options object
420 if (typeof params === 'string') {
421 params = parseUrl(params); // eslint-disable-line no-param-reassign
422 /* eslint sort-keys: 0 */
423 options = populate({
424 port: params.port,
425 path: params.pathname,
426 host: params.hostname,
427 protocol: params.protocol
428 }, defaults);
429 } else { // use custom params
430 options = populate(params, defaults);
431 // if no port provided use default one
432 if (!options.port) {
433 options.port = options.protocol === 'https:' ? 443 : 80;
434 }
435 }
436
437 // put that good code in getHeaders to some use
438 options.headers = this.getHeaders(params.headers);
439
440 // https if specified, fallback to http in any other case
441 if (options.protocol === 'https:') {
442 request = https.request(options);
443 } else {
444 request = http.request(options);
445 }
446
447 // get content length and fire away
448 this.getLength(function (err, length) {
449 if (err && err !== 'Unknown stream') {
450 this._error(err);
451 return;
452 }
453
454 // add content length
455 if (length) {
456 request.setHeader('Content-Length', length);
457 }
458
459 this.pipe(request);
460 if (cb) {
461 var onResponse;
462
463 var callback = function (error, responce) {
464 request.removeListener('error', callback);
465 request.removeListener('response', onResponse);
466
467 return cb.call(this, error, responce);
468 };
469
470 onResponse = callback.bind(this, null);
471
472 request.on('error', callback);
473 request.on('response', onResponse);
474 }
475 }.bind(this));
476
477 return request;
478};
479
480FormData.prototype._error = function (err) {
481 if (!this.error) {
482 this.error = err;
483 this.pause();
484 this.emit('error', err);
485 }
486};
487
488FormData.prototype.toString = function () {
489 return '[object FormData]';
490};
491setToStringTag(FormData.prototype, 'FormData');
492
493// Public API
494module.exports = FormData;
Note: See TracBrowser for help on using the repository browser.