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

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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