prototypes/server/extend/request-postdata.js

const ePrototype = require("application-prototype/constructors/extensions/prototype");
const Busboy = require("busboy");
const { ServerResponse } = require("http");
const { Stream, Readable } = require("stream");

/**
 * @private
 * @method RequestUrlDecorator
 * @param {SGAppsServerRequest} request 
 * @param {SGAppsServerResponse} response 
 * @param {SGAppsServer} server
 * @param {function} callback
 */
module.exports = function RequestUrlDecorator(request, response, server, callback) {
	if (request === null || response === null) {
		callback();
		return;
	}

	/**
	 * post data buffer cache
	 * @memberof SGAppsServerRequest#
	 * @name _postDataBuffer
	 * @type {Buffer}
	 */
	request._postDataBuffer = Buffer.from('', 'binary');

	/**
	 * @typedef {object} SGAppsServerRequestFile
	 * @property {string} fieldName field's name
	 * @property {object} data
	 * @property {string} data.fileName file's name `[duplicate]`
	 * @property {string} data.encoding file's encoding
	 * @property {Readable} data.fileStream () => fileStream
	 * @property {Buffer} data.fileData
	 * @property {number} data.fileSize size in bytes
	 * @property {string} data.contentType file's mimeType
	 * @property {boolean} data.loaded indicate if file is fully loaded into `fileData`
	 */

	/**
	 * @typedef {object} SGAppsServerRequestPostDataItem
	 * @property {string} fieldName field's name
	 * @property {object} data
	 * @property {string} data.value
	 * @property {string} data.encoding file's encoding
	 * @property {string} data.valTruncated 
	 * @property {Buffer} data.fieldNameTruncated
	 * @property {string} data.mimeType file's mimeType
	 */

	let _body = {};
	let _bodyItems = [];

	/**
	 * @memberof SGAppsServerRequest#
	 * @name body
	 * @type {object}
	 */
	Object.defineProperty(request, 'body', {
		get: () => _body,
		set: () => server.logger.warn("[Request.body] is not configurable"),
		enumerable: true,
		configurable: true
	});

	/**
	 * @memberof SGAppsServerRequest#
	 * @name bodyItems
	 * @type {SGAppsServerRequestPostDataItem[]}
	 */
	Object.defineProperty(request, 'bodyItems', {
		get: () => _bodyItems,
		set: () => server.logger.warn("[Request.bodyItems] is not configurable"),
		enumerable: true,
		configurable: true
	});

	/**
	 * @memberof SGAppsServerRequest#
	 * @name files
	 * @type {Object<string,SGAppsServerRequestFile[]>}
	 */
	let _files = {};
	Object.defineProperty(request, 'files', {
		get: () => _files,
		set: () => server.logger.warn("[Request.files] is not configurable"),
		enumerable: true,
		configurable: true
	});


	/**
	 * @memberof SGAppsServerRequest#
	 * @name fileItems
	 * @type {SGAppsServerRequestFile[]}
	 */
	let _fileItems = [];
	Object.defineProperty(request, 'fileItems', {
		get: () => _fileItems,
		set: () => server.logger.warn("[Request.fileItems] is not configurable"),
		enumerable: true,
		configurable: true
	});

	/**
	 * Automatically used procedure for parsing formData field name if option `server._options._REQUEST_FORM_PARAMS_DEEP_PARSE = true`. it's by default enabled but can be disabled when needed
	 * @memberof SGAppsServerRequest#
	 * @method _parseDeepFieldName
	 * @param {object} container
	 * @param {string} fieldName
	 * @param {any} fieldData
	 * @param {object} [options]
	 * @param {boolean} [options.transform2ArrayOnDuplicate=false]
	 * @example
	 * paramsContainer = {};
	 * request._parseDeepFieldName(paramsContainer, 'test[arr][data]', 2);
	 * request._parseDeepFieldName(paramsContainer, 'test[arr][]', new Date());
	 * request._parseDeepFieldName(paramsContainer, 'test[arr][]', 2);
	 * request._parseDeepFieldName(paramsContainer, 'test[data]', 2);
	 * // if _debug enabled warns will be emitted
	 * // [Warn] [Request._parseDeepFieldName] Writing Array field "test[arr][]" into a object
	 * // [Warn] [Request._parseDeepFieldName] Overwriting field "test[data]" value
	 * console.log(paramsContainer)
	 * {
	 *     "test": {
	 *         "arr": {
	 *             "1": "2021-02-12T21:23:01.913Z",
	 *             "2": 2,
	 *             "data": 2
	 *         },
	 *         "data": 2
	 *     }
	 * }
	 * @example
	 * paramsContainer = {};
	 * request._parseDeepFieldName(paramsContainer, 'test[arr][]', new Date());
	 * request._parseDeepFieldName(paramsContainer, 'test[arr][]', 2);
	 * request._parseDeepFieldName(paramsContainer, 'test[arr][data]', 2);
	 * request._parseDeepFieldName(paramsContainer, 'test[data]', 2);
	 * // if _debug enabled warns will be emitted
	 * // [Warn] [Request._parseDeepFieldName] Converting array to object due incorrect field "test[arr][data]" name 
	 * console.log(paramsContainer)
	 * {
	 *     "test": {
	 *         "arr": {
	 *             "0": "2021-02-12T21:34:47.359Z",
	 *             "1": 2,
	 *             "data": 2
	 *         },
	 *         "data": 2
	 *     }
	 * }
	 * @example
	 * paramsContainer = {};
	 * request._parseDeepFieldName(paramsContainer, 'test[arr][]', new Date());
	 * request._parseDeepFieldName(paramsContainer, 'test[arr][]', 2);
	 * request._parseDeepFieldName(paramsContainer, 'test[data]', 2);
	 * console.log(paramsContainer)
	 * {
	 *     "test": {
	 *         "arr": [
	 *             "2021-02-12T21:26:43.766Z",
	 *             2
	 *         ],
	 *         "data": 2
	 *     }
	 * }
	 */
	request._parseDeepFieldName = (container, fieldName, fieldData, options) => {
		if (!fieldName[0] || fieldName[0] === '[') {
			console.warn(
				`[Warn] [Request._parseDeepFieldName] Unable to parse fieldName without base`, {
					container,
					fieldName,
					fieldData
				}
			);
			return;
		}
	
		let fieldNamePrefix = fieldName.replace(/\[.*$/, '');
		container[fieldNamePrefix] = container[fieldNamePrefix] || {};
		let p = container[fieldNamePrefix];
		let pPrev = container;
		const _debug = server.logger._debug;
	
		const parts = fieldName
			.match(/\[[^\[]*\]/g);
	
		if (!parts) {
			if (fieldNamePrefix in container) {
				if (_debug) {
					server.logger.warn(
						`[Warn] [Request._parseDeepFieldName] Overwriting field "${fieldName}" value`, {
							container,
							fieldName,
							fieldData
						}
					);
				}
			}
			container[fieldNamePrefix] = fieldData;
			return;
		}
	
		parts
			.map(v => v.replace(/^\[([^\]]*)\]$/, '$1'))
			.forEach((k, i, a) => {
				if (p && typeof (p) === "object") {
					if (i === a.length - 1) {
						if (k === '') {
							if (pPrev) {
								const prevIndex = i ? a[i - 1] : fieldNamePrefix;
								if (!Array.isArray(pPrev[prevIndex])) {
									if (prevIndex in pPrev) {
										if (pPrev[prevIndex] && typeof (pPrev[prevIndex]) === "object") {
											const index = Object.keys(pPrev[prevIndex]).length;
	
											if (index === 0) {
												pPrev[prevIndex] = [];
												pPrev[prevIndex].push(fieldData);
											} else {
												if (_debug) {
													server.logger.warn(
														`[Warn] [Request._parseDeepFieldName] Writing Array field "${fieldName}" into a object`, {
															container,
															fieldName,
															fieldData
														}
													);
												}
												if (index in pPrev[prevIndex]) {
													if (_debug) {
														server.logger.warn(
															`[Warn] [Request._parseDeepFieldName] Overwriting field "${fieldName}" value`, {
																container,
																fieldName,
																fieldData
															}
														);
													}
												}
												pPrev[prevIndex][index] = fieldData;
											}
										} else {
											pPrev[prevIndex] = [];
											pPrev[prevIndex].push(fieldData);
										}
									} else {
										pPrev[prevIndex] = [];
										pPrev[prevIndex].push(fieldData);
									}
								} else {
									pPrev[prevIndex].push(fieldData);
								}
							} else {
								if (_debug) {
									console.warn(
										`[Warn] [Request._parseDeepFieldName] Unable to parse intermediary array index "[]"`, {
											container,
											fieldName,
											fieldData
										}
									);
								}
								p = null;
							}
						} else {
							if (k in p) {
								if (_debug) {
									console.warn(
										`[Warn] [Request._parseDeepFieldName] Overwriting field "${fieldName}" value`, {
											container,
											fieldName,
											fieldData
										}
									);
								}
							} else {
								if (Array.isArray(p)) {
									if (k.match(/^\d+$/)) {
										if (p[k] === undefined) {
											p[k] = fieldData;
										} else {
											if (_debug) {
												server.logger.warn(
													`[Warn] [Request._parseDeepFieldName] Overwriting field "${fieldName}" value`, {
														container,
														fieldName,
														fieldData
													}
												);
											}
											p[k] = fieldData;
										}
									} else {
										if (_debug) {
											server.logger.warn(
												`[Warn] [Request._parseDeepFieldName] Converting array to object due incorrect field "${fieldName}" name`, {
													container,
													fieldName,
													fieldData
												}
											);
										}
	
										const prevIndex = i ? a[i - 1] : fieldNamePrefix;
										pPrev[prevIndex] = Object.assign({}, p);
										pPrev[prevIndex][k] = fieldData;
									}
								} else {
									p[k] = fieldData;
								}
							}
						}
					} else {
						if (k === '') {
							if (_debug) {
								server.logger.warn(
									`[Warn] [Request._parseDeepFieldName] Unable to parse intermediary array index "[]"`, {
										container,
										fieldName,
										fieldData
									}
								);
							}
							p = null;
						} else {
							p[k] = p[k] || {};
							pPrev = p;
							p = p[k];
						}
					}
				} else {
					if (p !== null) {
						p = null;
						if (_debug) {
							server.logger.warn(
								`[Warn] [Request._parseDeepFieldName] Unable to parse Request params. Setting field "${fieldName}" in structure`, {
									container,
									fieldName,
									fieldData
								}
							);
						}
					}
				}
			});
	};

	/**
	 * request's post received data
	 * @memberof SGAppsServerRequest#
	 * @name postData
	 * @type {Promise<Buffer>}
	 */
	let _postData = null;
	Object.defineProperty(
		request,
		'postData',
		{
			get: () => {
				if (_postData) return _postData;
				_postData = new Promise(function (resolve, reject) {
					let _postDataSize = 0;
					let _canceled = false;
					request.request.on("data", function (chunk) {
						if (_canceled) return;
						if (request.request.aborted) return;
			
						var dataLimit = request.MAX_POST_SIZE;
			
						if (dataLimit < _postDataSize) {
							_canceled = true;
							const err = Error('[Request.MAX_POST_SIZE] exceeded');
							server.logger.error(err);
							reject(err);
							return;
						}
			
						request._postDataBuffer = Buffer.concat([request._postDataBuffer, chunk]);
			
						_postDataSize += chunk.length;
					});
			
					request.request.once("error", function (err) {
						if (_canceled) return;
						server.logger.error(err);
						_canceled = true;
						reject(err);
					});
			
					request.request.once("abort", function () {
						if (_canceled) return;
						const err = Error('[Request] aborted');
						server.logger.error(err);
						_canceled = true;
						reject(err);
					});
			
					request.request.once('end', function () {
						if (_canceled) return;
						if (
							(
								request.request.headers['content-type'] || ''
							).indexOf('multipart/form-data') === 0
						) {
			
							let Readable = require('stream').Readable;
							let readable = new Readable();
							readable._read = () => {}; // _read is required but you can noop it
							readable.push(request._postDataBuffer);
							readable.push(null);
				
							var detectedBoundary = (
								request._postDataBuffer
									.slice(0, 1024).toString()
									.match(/^\-\-(\-{4,}[A-Za-z0-9]{4,}\-*)(\r|)\n/) || []
							)[1] || null;
				
								if (detectedBoundary) {
									var calculatedHeader = 'multipart/form-data; boundary=' + detectedBoundary;
									if (
										calculatedHeader !== request.request.headers['content-type']
									) {
										server.logger.warn(
											"Multipart Form Data: boundary replaced from ",
											request.request.headers['content-type'],
											calculatedHeader
										);
									}
									request.request.headers['content-type'] = calculatedHeader;
								}
				
								/**
								 * @private
								 * @type {Readable}
								 */
								//@ts-ignore
								const busboy = new Busboy({
									headers: request.request.headers,
									limits: {
										fieldNameSize: 255,
										fieldSize: request.MAX_POST_SIZE,
										fileSize: request.MAX_POST_SIZE
									}
								});
			
								busboy.on(
									'file',
									/**
									 * @inner
									 * @param {string} fieldName 
									 * @param {Readable} fileStream 
									 * @param {string} fileName 
									 * @param {string} encoding 
									 * @param {string} mimeType 
									 */
									function (
										fieldName,
										fileStream,
										fileName,
										encoding,
										mimeType
									) {
										const file = {
											fieldName: fieldName,
											data: {
												fileName: fileName,
												encoding: encoding,
												fileStream: () => fileStream,
												fileData: null,
												fileSize: 0,
												contentType: mimeType,
												loaded: false
											}
										};

										//@ts-ignore
										_fileItems.push(file);

										if (server._options._REQUEST_FORM_PARAMS_DEEP_PARSE) {
											request._parseDeepFieldName(
												_files, fieldName, file
											);
										} else {
											if (!(fieldName in _files)) _files[fieldName] = [];
											//@ts-ignore
											_files[fieldName].push(file);
										}
			
										fileStream.on('data', function (data) {
											file.data.fileData.push(data);
											file.data.fileSize += data.length;
										});
										fileStream.on('error', function (err) {
											file.data.error = err;
											server.logger.error(err);
										});
										fileStream.on('end', function () {
											file.data.fileData = Buffer.concat(file.data.fileData);
											if (!file.data.error)
												file.data.loaded = true;
										});
									}
								);
			
								busboy.on('field', function (fieldName, value, fieldNameTruncated, valTruncated, encoding, mimeType) {
									// console.warn("BusBoy Field", arguments);
									_bodyItems.push({
										fieldName: fieldName,
										data: {
											value: value,
											fieldNameTruncated: fieldNameTruncated,
											valTruncated: valTruncated,
											encoding: encoding,
											mimeType: mimeType
										}
									});
									if (server._options._REQUEST_FORM_PARAMS_DEEP_PARSE) {
										request._parseDeepFieldName(
											_body, fieldName, value
										);
									} else {
										_body[fieldName] = value;
									}
								});
				
								busboy.on('error', function (err) {
									server.logger.error(err);
									reject(err);
								});
				
								busboy.on('finish', function () {
									resolve(request._postDataBuffer);
									var err;
									try {
										readable.destroy();
									} catch (err) {};

									try {
										busboy.destroy();
									} catch (err) {};
								});
				
								//@ts-ignore
								readable.pipe(busboy); // consume the Stream
						} else {
							if (
								(
									(
										request.request.headers['content-type'] || ''
									) || ''
								).indexOf('application/json') === 0
							) {
								const data = request._postDataBuffer.toString('utf-8', 0, request._postDataBuffer.length);

								try {
									const jsonData = JSON.parse(data);
									if (jsonData && typeof(jsonData) === "object") {
										Object.assign(_body, jsonData);
									}
								} catch (err) {
									if (server.logger._debug) {
										server.logger.warn(`[Request._body] Unable to parse JSON data`);
									}
								}
							} else if (
								(
									(
										request.request.headers['content-type'] || ''
									) || ''
								).indexOf('application/x-www-form-urlencoded') === 0
							) {
								const data = request._postDataBuffer.toString('utf-8', 0, request._postDataBuffer.length);
								
								//@ts-ignore
								try {
									const formData = data.parseUrlVars(true);
	
									if (formData && typeof(formData) === "object") {
										Object.assign(_body, formData);
									}
								} catch (err) {
									if (server.logger._debug) {
										server.logger.warn(`[Request._body] Unable to parse URL Formed Data data`);
									}
								}
							}
							resolve(request._postDataBuffer);
						}
					});
				});
				return _postData;
			},
			set: (v) => {
				server.logger.warn('[Request.postData] is not writeable');
			}
		}
	);

	// response._destroy.push(function () {
	// 	_postData = null;
	// 	_body = null;
	// 	_bodyItems = null;
	// 	_fileItems = null;
	// 	_files = null;
	// 	delete request._parseDeepFieldName;
	// 	delete request._postDataBuffer;
	// 	delete request.postData;
	// });

	callback();
};