CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
Formidable.js686 linesDownload Raw Back to src
1/* eslint-disable class-methods-use-this */2/* eslint-disable no-underscore-dangle */3 4import { init as cuid2init } from '@paralleldrive/cuid2';5import dezalgo from 'dezalgo';6import { EventEmitter } from 'node:events';7import fsPromises from 'node:fs/promises';8import os from 'node:os';9import path from 'node:path';10import { StringDecoder } from 'node:string_decoder';11import once from 'once';12import FormidableError, * as errors from './FormidableError.js';13import PersistentFile from './PersistentFile.js';14import VolatileFile from './VolatileFile.js';15import DummyParser from './parsers/Dummy.js';16import MultipartParser from './parsers/Multipart.js';17import { json, multipart, octetstream, querystring } from './plugins/index.js';18 19const CUID2_FINGERPRINT = `${process.env.NODE_ENV}-${os.platform()}-${os.hostname()}`20const createId = cuid2init({ length: 25, fingerprint: CUID2_FINGERPRINT.toLowerCase() });21 22const DEFAULT_OPTIONS = {23  maxFields: 1000,24  maxFieldsSize: 20 * 1024 * 1024,25  maxFiles: Infinity,26  maxFileSize: 200 * 1024 * 1024,27  maxTotalFileSize: undefined,28  minFileSize: 1,29  allowEmptyFiles: false,30  createDirsFromUploads: false,31  keepExtensions: false,32  encoding: 'utf-8',33  hashAlgorithm: false,34  uploadDir: os.tmpdir(),35  enabledPlugins: [octetstream, querystring, multipart, json],36  fileWriteStreamHandler: null,37  defaultInvalidName: 'invalid-name',38  filter(_part) {39    return true;40  },41  filename: undefined,42};43 44function hasOwnProp(obj, key) {45  return Object.prototype.hasOwnProperty.call(obj, key);46}47 48 49const decorateForceSequential = function (promiseCreator) {50  /* forces a function that returns a promise to be sequential51  useful for fs  for example */52  let lastPromise = Promise.resolve();53  return async function (...x) {54      const promiseWeAreWaitingFor = lastPromise;55      let currentPromise;56      let callback;57      // we need to change lastPromise before await anything,58      // otherwise 2 calls might wait the same thing59      lastPromise = new Promise(function (resolve) {60          callback = resolve;61      });62      await promiseWeAreWaitingFor;63      currentPromise = promiseCreator(...x);64      currentPromise.then(callback).catch(callback);65      return currentPromise;66  };67};68 69const createNecessaryDirectoriesAsync = decorateForceSequential(function (filePath) {70  const directoryname = path.dirname(filePath);71  return fsPromises.mkdir(directoryname, { recursive: true });72});73 74const invalidExtensionChar = (c) => {75  const code = c.charCodeAt(0);76  return !(77    code === 46 || // .78    (code >= 48 && code <= 57) ||79    (code >= 65 && code <= 90) ||80    (code >= 97 && code <= 122)81  );82};83 84class IncomingForm extends EventEmitter {85  constructor(options = {}) {86    super();87 88    this.options = { ...DEFAULT_OPTIONS, ...options };89    if (!this.options.maxTotalFileSize) {90      this.options.maxTotalFileSize = this.options.maxFileSize91    }92 93    const dir = path.resolve(94      this.options.uploadDir || this.options.uploaddir || os.tmpdir(),95    );96 97    this.uploaddir = dir;98    this.uploadDir = dir;99 100    // initialize with null101    [102      'error',103      'headers',104      'type',105      'bytesExpected',106      'bytesReceived',107      '_parser',108      'req',109    ].forEach((key) => {110      this[key] = null;111    });112 113    this._setUpRename();114 115    this._flushing = 0;116    this._fieldsSize = 0;117    this._totalFileSize = 0;118    this._plugins = [];119    this.openedFiles = [];120 121    this.options.enabledPlugins = []122      .concat(this.options.enabledPlugins)123      .filter(Boolean);124 125    if (this.options.enabledPlugins.length === 0) {126      throw new FormidableError(127        'expect at least 1 enabled builtin plugin, see options.enabledPlugins',128        errors.missingPlugin,129      );130    }131 132    this.options.enabledPlugins.forEach((plugin) => {133      this.use(plugin);134    });135 136    this._setUpMaxFields();137    this._setUpMaxFiles();138    this.ended = undefined;139    this.type = undefined;140  }141 142  use(plugin) {143    if (typeof plugin !== 'function') {144      throw new FormidableError(145        '.use: expect `plugin` to be a function',146        errors.pluginFunction,147      );148    }149    this._plugins.push(plugin.bind(this));150    return this;151  }152 153  pause () {154    try {155      this.req.pause();156    } catch (err) {157      // the stream was destroyed158      if (!this.ended) {159        // before it was completed, crash & burn160        this._error(err);161      }162      return false;163    }164    return true;165  }166 167  resume () {168    try {169      this.req.resume();170    } catch (err) {171      // the stream was destroyed172      if (!this.ended) {173        // before it was completed, crash & burn174        this._error(err);175      }176      return false;177    }178 179    return true;180  }181 182  // returns a promise if no callback is provided183  async parse(req, cb) {184    this.req = req;185    let promise;186 187    // Setup callback first, so we don't miss anything from data events emitted immediately.188    if (!cb) {189      let resolveRef;190      let rejectRef;191      promise = new Promise((resolve, reject) => {192        resolveRef = resolve;193        rejectRef = reject;194      });195      cb = (err, fields, files) => {196        if (err) {197          rejectRef(err);198        } else {199          resolveRef([fields, files]);200        }201      }202    }203    const callback = once(dezalgo(cb));204    this.fields = {};205    const files = {};206 207    this.on('field', (name, value) => {208      if (this.type === 'multipart' || this.type === 'urlencoded') {209        if (!hasOwnProp(this.fields, name)) {210          this.fields[name] = [value];211        } else {212          this.fields[name].push(value);213        }214      } else {215        this.fields[name] = value;216      }217    });218    this.on('file', (name, file) => {219      if (!hasOwnProp(files, name)) {220        files[name] = [file];221      } else {222        files[name].push(file);223      }224    });225    this.on('error', (err) => {226      callback(err, this.fields, files);227    });228    this.on('end', () => {229      callback(null, this.fields, files);230    });231 232    // Parse headers and setup the parser, ready to start listening for data.233    await this.writeHeaders(req.headers);234 235    // Start listening for data.236    req237      .on('error', (err) => {238        this._error(err);239      })240      .on('aborted', () => {241        this.emit('aborted');242        this._error(new FormidableError('Request aborted', errors.aborted));243      })244      .on('data', (buffer) => {245        try {246          this.write(buffer);247        } catch (err) {248          this._error(err);249        }250      })251      .on('end', () => {252        if (this.error) {253          return;254        }255        if (this._parser) {256          this._parser.end();257        }258      });259    if (promise) {260      return promise;261    }262    return this;263  }264 265  async writeHeaders(headers) {266    this.headers = headers;267    this._parseContentLength();268    await this._parseContentType();269 270    if (!this._parser) {271      this._error(272        new FormidableError(273          'no parser found',274          errors.noParser,275          415, // Unsupported Media Type276        ),277      );278      return;279    }280 281    this._parser.once('error', (error) => {282      this._error(error);283    });284  }285 286  write(buffer) {287    if (this.error) {288      return null;289    }290    if (!this._parser) {291      this._error(292        new FormidableError('uninitialized parser', errors.uninitializedParser),293      );294      return null;295    }296 297    this.bytesReceived += buffer.length;298    this.emit('progress', this.bytesReceived, this.bytesExpected);299 300    this._parser.write(buffer);301 302    return this.bytesReceived;303  }304 305  onPart(part) {306    // this method can be overwritten by the user307    return this._handlePart(part);308  }309 310  async _handlePart(part) {311    if (part.originalFilename && typeof part.originalFilename !== 'string') {312      this._error(313        new FormidableError(314          `the part.originalFilename should be string when it exists`,315          errors.filenameNotString,316        ),317      );318      return;319    }320 321    // This MUST check exactly for undefined. You can not change it to !part.originalFilename.322 323    // todo: uncomment when switch tests to Jest324    // console.log(part);325 326    // ? NOTE(@tunnckocore): no it can be any falsey value, it most probably depends on what's returned327    // from somewhere else. Where recently I changed the return statements328    // and such thing because code style329    // ? NOTE(@tunnckocore): or even better, if there is no mimetype, then it's for sure a field330    // ? NOTE(@tunnckocore): originalFilename is an empty string when a field?331    if (!part.mimetype) {332      let value = '';333      const decoder = new StringDecoder(334        part.transferEncoding || this.options.encoding,335      );336 337      part.on('data', (buffer) => {338        this._fieldsSize += buffer.length;339        if (this._fieldsSize > this.options.maxFieldsSize) {340          this._error(341            new FormidableError(342              `options.maxFieldsSize (${this.options.maxFieldsSize} bytes) exceeded, received ${this._fieldsSize} bytes of field data`,343              errors.maxFieldsSizeExceeded,344              413, // Payload Too Large345            ),346          );347          return;348        }349        value += decoder.write(buffer);350      });351 352      part.on('end', () => {353        this.emit('field', part.name, value);354      });355      return;356    }357 358    if (!this.options.filter(part)) {359      return;360    }361 362    this._flushing += 1;363 364    let fileSize = 0;365    const newFilename = this._getNewName(part);366    const filepath = this._joinDirectoryName(newFilename);367    const file = await this._newFile({368      newFilename,369      filepath,370      originalFilename: part.originalFilename,371      mimetype: part.mimetype,372    });373    file.on('error', (err) => {374      this._error(err);375    });376    this.emit('fileBegin', part.name, file);377 378    file.open();379    this.openedFiles.push(file);380 381    part.on('data', (buffer) => {382      this._totalFileSize += buffer.length;383      fileSize += buffer.length;384 385      if (this._totalFileSize > this.options.maxTotalFileSize) {386        this._error(387          new FormidableError(388            `options.maxTotalFileSize (${this.options.maxTotalFileSize} bytes) exceeded, received ${this._totalFileSize} bytes of file data`,389            errors.biggerThanTotalMaxFileSize,390            413,391          ),392        );393        return;394      }395      if (buffer.length === 0) {396        return;397      }398      this.pause();399      file.write(buffer, () => {400        this.resume();401      });402    });403 404    part.on('end', () => {405      if (!this.options.allowEmptyFiles && fileSize === 0) {406        this._error(407          new FormidableError(408            `options.allowEmptyFiles is false, file size should be greater than 0`,409            errors.noEmptyFiles,410            400,411          ),412        );413        return;414      }415      if (fileSize < this.options.minFileSize) {416        this._error(417          new FormidableError(418            `options.minFileSize (${this.options.minFileSize} bytes) inferior, received ${fileSize} bytes of file data`,419            errors.smallerThanMinFileSize,420            400,421          ),422        );423        return;424      }425      if (fileSize > this.options.maxFileSize) {426        this._error(427          new FormidableError(428            `options.maxFileSize (${this.options.maxFileSize} bytes), received ${fileSize} bytes of file data`,429            errors.biggerThanMaxFileSize,430            413,431          ),432        );433        return;434      }435 436      file.end(() => {437        this._flushing -= 1;438        this.emit('file', part.name, file);439        this._maybeEnd();440      });441    });442  }443 444  // eslint-disable-next-line max-statements445  async _parseContentType() {446    if (this.bytesExpected === 0) {447      this._parser = new DummyParser(this, this.options);448      return;449    }450 451    if (!this.headers['content-type']) {452      this._error(453        new FormidableError(454          'bad content-type header, no content-type',455          errors.missingContentType,456          400,457        ),458      );459      return;460    }461 462 463    new DummyParser(this, this.options);464 465    const results = [];466    await Promise.all(this._plugins.map(async (plugin, idx) => {467      let pluginReturn = null;468      try {469        pluginReturn = await plugin(this, this.options) || this;470      } catch (err) {471        // directly throw from the `form.parse` method;472        // there is no other better way, except a handle through options473        const error = new FormidableError(474          `plugin on index ${idx} failed with: ${err.message}`,475          errors.pluginFailed,476          500,477        );478        error.idx = idx;479        throw error;480      }481      Object.assign(this, pluginReturn);482 483      // todo: use Set/Map and pass plugin name instead of the `idx` index484      this.emit('plugin', idx, pluginReturn);485    }));486    this.emit('pluginsResults', results);487  }488 489  _error(err, eventName = 'error') {490    if (this.error || this.ended) {491      return;492    }493 494    this.req = null;495    this.error = err;496    this.emit(eventName, err);497 498    this.openedFiles.forEach((file) => {499      file.destroy();500    });501  }502 503  _parseContentLength() {504    this.bytesReceived = 0;505    if (this.headers['content-length']) {506      this.bytesExpected = parseInt(this.headers['content-length'], 10);507    } else if (this.headers['transfer-encoding'] === undefined) {508      this.bytesExpected = 0;509    }510 511    if (this.bytesExpected !== null) {512      this.emit('progress', this.bytesReceived, this.bytesExpected);513    }514  }515 516  _newParser() {517    return new MultipartParser(this.options);518  }519 520  async _newFile({ filepath, originalFilename, mimetype, newFilename }) {521    if (this.options.fileWriteStreamHandler) {522      return new VolatileFile({523        newFilename,524        filepath,525        originalFilename,526        mimetype,527        createFileWriteStream: this.options.fileWriteStreamHandler,528        hashAlgorithm: this.options.hashAlgorithm,529      });530    }531    if (this.options.createDirsFromUploads) {532      try {533        await createNecessaryDirectoriesAsync(filepath);534      } catch (errorCreatingDir) {535        this._error(new FormidableError(536          `cannot create directory`,537          errors.cannotCreateDir,538          409,539        ));540      }541    }542    return new PersistentFile({543      newFilename,544      filepath,545      originalFilename,546      mimetype,547      hashAlgorithm: this.options.hashAlgorithm,548    });549  }550 551  _getFileName(headerValue) {552    // matches either a quoted-string or a token (RFC 2616 section 19.5.1)553    const m = headerValue.match(554      /\bfilename=("(.*?)"|([^()<>{}[\]@,;:"?=\s/\t]+))($|;\s)/i,555    );556    if (!m) return null;557 558    const match = m[2] || m[3] || '';559    let originalFilename = match.substr(match.lastIndexOf('\\') + 1);560    originalFilename = originalFilename.replace(/%22/g, '"');561    originalFilename = originalFilename.replace(/&#([\d]{4});/g, (_, code) =>562      String.fromCharCode(code),563    );564 565    return originalFilename;566  }567 568  // able to get composed extension with multiple dots569  // "a.b.c" -> ".b.c"570  // as opposed to path.extname -> ".c"571  _getExtension(str) {572    if (!str) {573      return '';574    }575 576    const basename = path.basename(str);577    const firstDot = basename.indexOf('.');578    const lastDot = basename.lastIndexOf('.');579    let rawExtname = path.extname(basename);580 581    if (firstDot !== lastDot) {582      rawExtname =  basename.slice(firstDot);583    }584 585    let filtered;586    const firstInvalidIndex = Array.from(rawExtname).findIndex(invalidExtensionChar);587    if (firstInvalidIndex === -1) {588      filtered = rawExtname;589    } else {590      filtered = rawExtname.substring(0, firstInvalidIndex);591    }592    if (filtered === '.') {593      return '';594    }595    return filtered;596  }597 598  _joinDirectoryName(name) {599    const newPath = path.join(this.uploadDir, name);600 601    // prevent directory traversal attacks602    if (!newPath.startsWith(this.uploadDir)) {603      return path.join(this.uploadDir, this.options.defaultInvalidName);604    }605 606    return newPath;607  }608 609  _setUpRename() {610    const hasRename = typeof this.options.filename === 'function';611    if (hasRename) {612      this._getNewName = (part) => {613        let ext = '';614        let name = this.options.defaultInvalidName;615        if (part.originalFilename) {616          // can be null617          ({ ext, name } = path.parse(part.originalFilename));618          if (this.options.keepExtensions !== true) {619            ext = '';620          }621        }622        return this.options.filename.call(this, name, ext, part, this);623      };624    } else {625      this._getNewName = (part) => {626        const name = createId();627 628        if (part && this.options.keepExtensions) {629          const originalFilename =630            typeof part === 'string' ? part : part.originalFilename;631          return `${name}${this._getExtension(originalFilename)}`;632        }633 634        return name;635      };636    }637  }638 639  _setUpMaxFields() {640    if (this.options.maxFields !== Infinity) {641      let fieldsCount = 0;642      this.on('field', () => {643        fieldsCount += 1;644        if (fieldsCount > this.options.maxFields) {645          this._error(646            new FormidableError(647              `options.maxFields (${this.options.maxFields}) exceeded`,648              errors.maxFieldsExceeded,649              413,650            ),651          );652        }653      });654    }655  }656 657  _setUpMaxFiles() {658    if (this.options.maxFiles !== Infinity) {659      let fileCount = 0;660      this.on('fileBegin', () => {661        fileCount += 1;662        if (fileCount > this.options.maxFiles) {663          this._error(664            new FormidableError(665              `options.maxFiles (${this.options.maxFiles}) exceeded`,666              errors.maxFilesExceeded,667              413,668            ),669          );670        }671      });672    }673  }674 675  _maybeEnd() {676    if (!this.ended || this._flushing || this.error) {677      return;678    }679    this.req = null;680    this.emit('end');681  }682}683 684export default IncomingForm;685export { DEFAULT_OPTIONS };686 
basant307/AI_Governance_Project · CoolFace