twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

form_data.js (12249B)


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