basant307/AI_Governance_Project
048
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 