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