CoolFace
Apppublic

strong-tie/inbound-calls

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
form_data.js504 linesDownload Raw Back to lib
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 setToStringTag = require('es-set-tostringtag');12var populate = require('./populate.js');13 14// Public API15module.exports = FormData;16 17// make it a Stream18util.inherits(FormData, CombinedStream);19 20/**21 * Create readable "multipart/form-data" streams.22 * Can be used to submit forms23 * and file uploads to other web applications.24 *25 * @constructor26 * @param {Object} options - Properties to be added/overriden for FormData and CombinedStream27 */28function FormData(options) {29  if (!(this instanceof FormData)) {30    return new FormData(options);31  }32 33  this._overheadLength = 0;34  this._valueLength = 0;35  this._valuesToMeasure = [];36 37  CombinedStream.call(this);38 39  options = options || {};40  for (var option in options) {41    this[option] = options[option];42  }43}44 45FormData.LINE_BREAK = '\r\n';46FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream';47 48FormData.prototype.append = function(field, value, options) {49 50  options = options || {};51 52  // allow filename as single option53  if (typeof options == 'string') {54    options = {filename: options};55  }56 57  var append = CombinedStream.prototype.append.bind(this);58 59  // all that streamy business can't handle numbers60  if (typeof value == 'number') {61    value = '' + value;62  }63 64  // https://github.com/felixge/node-form-data/issues/3865  if (Array.isArray(value)) {66    // Please convert your array into string67    // the way web server expects it68    this._error(new Error('Arrays are not supported.'));69    return;70  }71 72  var header = this._multiPartHeader(field, value, options);73  var footer = this._multiPartFooter();74 75  append(header);76  append(value);77  append(footer);78 79  // pass along options.knownLength80  this._trackLength(header, value, options);81};82 83FormData.prototype._trackLength = function(header, value, options) {84  var valueLength = 0;85 86  // used w/ getLengthSync(), when length is known.87  // e.g. for streaming directly from a remote server,88  // w/ a known file a size, and not wanting to wait for89  // incoming file to finish to get its size.90  if (options.knownLength != null) {91    valueLength += +options.knownLength;92  } else if (Buffer.isBuffer(value)) {93    valueLength = value.length;94  } else if (typeof value === 'string') {95    valueLength = Buffer.byteLength(value);96  }97 98  this._valueLength += valueLength;99 100  // @check why add CRLF? does this account for custom/multiple CRLFs?101  this._overheadLength +=102    Buffer.byteLength(header) +103    FormData.LINE_BREAK.length;104 105  // empty or either doesn't have path or not an http response or not a stream106  if (!value || ( !value.path && !(value.readable && Object.prototype.hasOwnProperty.call(value, 'httpVersion')) && !(value instanceof Stream))) {107    return;108  }109 110  // no need to bother with the length111  if (!options.knownLength) {112    this._valuesToMeasure.push(value);113  }114};115 116FormData.prototype._lengthRetriever = function(value, callback) {117  if (Object.prototype.hasOwnProperty.call(value, 'fd')) {118 119    // take read range into a account120    // `end` = Infinity โ€“> read file till the end121    //122    // TODO: Looks like there is bug in Node fs.createReadStream123    // it doesn't respect `end` options without `start` options124    // Fix it when node fixes it.125    // https://github.com/joyent/node/issues/7819126    if (value.end != undefined && value.end != Infinity && value.start != undefined) {127 128      // when end specified129      // no need to calculate range130      // inclusive, starts with 0131      callback(null, value.end + 1 - (value.start ? value.start : 0));132 133    // not that fast snoopy134    } else {135      // still need to fetch file size from fs136      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 options146        fileSize = stat.size - (value.start ? value.start : 0);147        callback(null, fileSize);148      });149    }150 151  // or http response152  } else if (Object.prototype.hasOwnProperty.call(value, 'httpVersion')) {153    callback(null, +value.headers['content-length']);154 155  // or request stream http://github.com/mikeal/request156  } else if (Object.prototype.hasOwnProperty.call(value, 'httpModule')) {157    // wait till response come back158    value.on('response', function(response) {159      value.pause();160      callback(null, +response.headers['content-length']);161    });162    value.resume();163 164  // something else165  } 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 boundary173  // (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 not184    'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []),185    // if no content type. allow it to be empty array186    '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 (Object.prototype.hasOwnProperty.call(headers, prop)) {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 216  return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK;217};218 219FormData.prototype._getContentDisposition = function(value, options) {220 221  var filename222    , contentDisposition223    ;224 225  if (typeof options.filepath === 'string') {226    // custom filepath for relative paths227    filename = path.normalize(options.filepath).replace(/\\/g, '/');228  } else if (options.filename || value.name || value.path) {229    // custom filename take precedence230    // formidable and the browser add a name property231    // fs- and request- streams have path property232    filename = path.basename(options.filename || value.name || value.path);233  } else if (value.readable && Object.prototype.hasOwnProperty.call(value, 'httpVersion')) {234    // or try http response235    filename = path.basename(value.client._httpMessage.path || '');236  }237 238  if (filename) {239    contentDisposition = 'filename="' + filename + '"';240  }241 242  return contentDisposition;243};244 245FormData.prototype._getContentType = function(value, options) {246 247  // use custom content-type above all248  var contentType = options.contentType;249 250  // or try `name` from formidable, browser251  if (!contentType && value.name) {252    contentType = mime.lookup(value.name);253  }254 255  // or try `path` from fs-, request- streams256  if (!contentType && value.path) {257    contentType = mime.lookup(value.path);258  }259 260  // or if it's http-reponse261  if (!contentType && value.readable && Object.prototype.hasOwnProperty.call(value, 'httpVersion')) {262    contentType = value.headers['content-type'];263  }264 265  // or guess it from the filepath or filename266  if (!contentType && (options.filepath || options.filename)) {267    contentType = mime.lookup(options.filepath || options.filename);268  }269 270  // fallback to the default content type if `value` is not simple value271  if (!contentType && typeof value == 'object') {272    contentType = FormData.DEFAULT_CONTENT_TYPE;273  }274 275  return contentType;276};277 278FormData.prototype._multiPartFooter = function() {279  return function(next) {280    var footer = FormData.LINE_BREAK;281 282    var lastPart = (this._streams.length === 0);283    if (lastPart) {284      footer += this._lastBoundary();285    }286 287    next(footer);288  }.bind(this);289};290 291FormData.prototype._lastBoundary = function() {292  return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK;293};294 295FormData.prototype.getHeaders = function(userHeaders) {296  var header;297  var formHeaders = {298    'content-type': 'multipart/form-data; boundary=' + this.getBoundary()299  };300 301  for (header in userHeaders) {302    if (Object.prototype.hasOwnProperty.call(userHeaders, header)) {303      formHeaders[header.toLowerCase()] = userHeaders[header];304    }305  }306 307  return formHeaders;308};309 310FormData.prototype.setBoundary = function(boundary) {311  this._boundary = boundary;312};313 314FormData.prototype.getBoundary = function() {315  if (!this._boundary) {316    this._generateBoundary();317  }318 319  return this._boundary;320};321 322FormData.prototype.getBuffer = function() {323  var dataBuffer = new Buffer.alloc(0);324  var boundary = this.getBoundary();325 326  // Create the form content. Add Line breaks to the end of data.327  for (var i = 0, len = this._streams.length; i < len; i++) {328    if (typeof this._streams[i] !== 'function') {329 330      // Add content to the buffer.331      if(Buffer.isBuffer(this._streams[i])) {332        dataBuffer = Buffer.concat( [dataBuffer, this._streams[i]]);333      }else {334        dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(this._streams[i])]);335      }336 337      // Add break after content.338      if (typeof this._streams[i] !== 'string' || this._streams[i].substring( 2, boundary.length + 2 ) !== boundary) {339        dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(FormData.LINE_BREAK)] );340      }341    }342  }343 344  // Add the footer and return the Buffer object.345  return Buffer.concat( [dataBuffer, Buffer.from(this._lastBoundary())] );346};347 348FormData.prototype._generateBoundary = function() {349  // This generates a 50 character boundary similar to those used by Firefox.350  // They are optimized for boyer-moore parsing.351  var boundary = '--------------------------';352  for (var i = 0; i < 24; i++) {353    boundary += Math.floor(Math.random() * 10).toString(16);354  }355 356  this._boundary = boundary;357};358 359// Note: getLengthSync DOESN'T calculate streams length360// As workaround one can calculate file size manually361// and add it as knownLength option362FormData.prototype.getLengthSync = function() {363  var knownLength = this._overheadLength + this._valueLength;364 365  // Don't get confused, there are 3 "internal" streams for each keyval pair366  // so it basically checks if there is any value added to the form367  if (this._streams.length) {368    knownLength += this._lastBoundary().length;369  }370 371  // https://github.com/form-data/form-data/issues/40372  if (!this.hasKnownLength()) {373    // Some async length retrievers are present374    // therefore synchronous length calculation is false.375    // Please use getLength(callback) to get proper length376    this._error(new Error('Cannot calculate proper length in synchronous way.'));377  }378 379  return knownLength;380};381 382// Public API to check if length of added values is known383// https://github.com/form-data/form-data/issues/196384// https://github.com/form-data/form-data/issues/262385FormData.prototype.hasKnownLength = function() {386  var hasKnownLength = true;387 388  if (this._valuesToMeasure.length) {389    hasKnownLength = false;390  }391 392  return hasKnownLength;393};394 395FormData.prototype.getLength = function(cb) {396  var knownLength = this._overheadLength + this._valueLength;397 398  if (this._streams.length) {399    knownLength += this._lastBoundary().length;400  }401 402  if (!this._valuesToMeasure.length) {403    process.nextTick(cb.bind(this, null, knownLength));404    return;405  }406 407  asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) {408    if (err) {409      cb(err);410      return;411    }412 413    values.forEach(function(length) {414      knownLength += length;415    });416 417    cb(null, knownLength);418  });419};420 421FormData.prototype.submit = function(params, cb) {422  var request423    , options424    , defaults = {method: 'post'}425    ;426 427  // parse provided url if it's string428  // or treat it as options object429  if (typeof params == 'string') {430 431    params = parseUrl(params);432    options = populate({433      port: params.port,434      path: params.pathname,435      host: params.hostname,436      protocol: params.protocol437    }, defaults);438 439  // use custom params440  } else {441 442    options = populate(params, defaults);443    // if no port provided use default one444    if (!options.port) {445      options.port = options.protocol == 'https:' ? 443 : 80;446    }447  }448 449  // put that good code in getHeaders to some use450  options.headers = this.getHeaders(params.headers);451 452  // https if specified, fallback to http in any other case453  if (options.protocol == 'https:') {454    request = https.request(options);455  } else {456    request = http.request(options);457  }458 459  // get content length and fire away460  this.getLength(function(err, length) {461    if (err && err !== 'Unknown stream') {462      this._error(err);463      return;464    }465 466    // add content length467    if (length) {468      request.setHeader('Content-Length', length);469    }470 471    this.pipe(request);472    if (cb) {473      var onResponse;474 475      var callback = function (error, responce) {476        request.removeListener('error', callback);477        request.removeListener('response', onResponse);478 479        return cb.call(this, error, responce);480      };481 482      onResponse = callback.bind(this, null);483 484      request.on('error', callback);485      request.on('response', onResponse);486    }487  }.bind(this));488 489  return request;490};491 492FormData.prototype._error = function(err) {493  if (!this.error) {494    this.error = err;495    this.pause();496    this.emit('error', err);497  }498};499 500FormData.prototype.toString = function () {501  return '[object FormData]';502};503setToStringTag(FormData, 'FormData');504