From 3c53efb526e51df51e3beed8ef33f617ab931580 Mon Sep 17 00:00:00 2001 From: Timothy Lai Date: Thu, 7 May 2020 19:01:56 -0700 Subject: [PATCH] fix: curl array support within multipart/form-data (#3838) ft: utils.createObjWithHashedKeys ft: curlify.extractKey test: curlify with array representation --- src/core/curlify.js | 19 ++++++++++-- src/core/utils.js | 63 +++++++++++++++++++++++++++++++++----- test/mocha/core/curlify.js | 20 ++++++++++++ 3 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/core/curlify.js b/src/core/curlify.js index a1385f5f35a..c12f18965a3 100644 --- a/src/core/curlify.js +++ b/src/core/curlify.js @@ -1,5 +1,19 @@ import win from "./window" +/** + * if duplicate key name existed from FormData entries, + * we mutated the key name by appending a hashIdx + * @param {String} k - possibly mutated key name + * @return {String} - src key name + */ +const extractKey = (k) => { + const hashIdx = "_**[]" + if (k.indexOf(hashIdx) < 0) { + return k + } + return k.split(hashIdx)[0].trim() +} + export default function curl( request ){ let curlified = [] let type = "" @@ -21,11 +35,12 @@ export default function curl( request ){ if(type === "multipart/form-data" && request.get("method") === "POST") { for( let [ k,v ] of request.get("body").entrySeq()) { + let extractedKey = extractKey(k) curlified.push( "-F" ) if (v instanceof win.File) { - curlified.push( `"${k}=@${v.name}${v.type ? `;type=${v.type}` : ""}"` ) + curlified.push(`"${extractedKey}=@${v.name}${v.type ? `;type=${v.type}` : ""}"` ) } else { - curlified.push( `"${k}=${v}"` ) + curlified.push(`"${extractedKey}=${v}"` ) } } } else { diff --git a/src/core/utils.js b/src/core/utils.js index ebb00cdffa2..36baa2e8e63 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -67,17 +67,64 @@ export function arrayify (thing) { return normalizeArray(thing) } -export function fromJSOrdered (js) { - if(isImmutable(js)) +export function fromJSOrdered(js) { + if (isImmutable(js)) { return js // Can't do much here - - if (js instanceof win.File) + } + if (js instanceof win.File) { + return js + } + if (!isObject(js)) { return js + } + if (Array.isArray(js)) { + return Im.Seq(js).map(fromJSOrdered).toList() + } + if (js.entries) { + // handle multipart/form-data + const objWithHashedKeys = createObjWithHashedKeys(js) + return Im.OrderedMap(objWithHashedKeys).map(fromJSOrdered) + } + return Im.OrderedMap(js).map(fromJSOrdered) +} - return !isObject(js) ? js : - Array.isArray(js) ? - Im.Seq(js).map(fromJSOrdered).toList() : - Im.OrderedMap(js).map(fromJSOrdered) +/** + * Convert a FormData object into plain object + * Append a hashIdx and counter to the key name, if multiple exists + * if single, key name = + * if multiple, key name = + * @param {FormData} fdObj - a FormData object + * @return {Object} - a plain object + */ +export function createObjWithHashedKeys (fdObj) { + if (!fdObj.entries) { + return fdObj // not a FormData object with iterable + } + const newObj = {} + const hashIdx = "_**[]" // our internal identifier + const trackKeys = {} + for (let pair of fdObj.entries()) { + if (!newObj[pair[0]] && !(trackKeys[pair[0]] && trackKeys[pair[0]].containsMultiple)) { + newObj[pair[0]] = pair[1] // first key name: no hash required + } else { + if (!trackKeys[pair[0]]) { + // initiate tracking key for multiple + trackKeys[pair[0]] = { + containsMultiple: true, + length: 1 + } + // "reassign" first pair to matching hashed format for multiple + let hashedKeyFirst = `${pair[0]}${hashIdx}${trackKeys[pair[0]].length}` + newObj[hashedKeyFirst] = newObj[pair[0]] + // remove non-hashed key of multiple + delete newObj[pair[0]] // first + } + trackKeys[pair[0]].length += 1 + let hashedKeyCurrent = `${pair[0]}${hashIdx}${trackKeys[pair[0]].length}` + newObj[hashedKeyCurrent] = pair[1] + } + } + return newObj } export function bindToState(obj, state) { diff --git a/test/mocha/core/curlify.js b/test/mocha/core/curlify.js index 1467620e523..61aad97486f 100644 --- a/test/mocha/core/curlify.js +++ b/test/mocha/core/curlify.js @@ -143,6 +143,26 @@ describe("curlify", function() { expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"content-type: multipart/form-data\" -F \"id=123\" -F \"name=Sahar\"") }) + it("should print a curl with formData that extracts array representation with hashIdx", function() { + // Note: hashIdx = `_**[]${counter}` + // Usage of hashIdx is an internal SwaggerUI method to convert formData array into something curlify can handle + const req = { + url: "http://example.com", + method: "POST", + headers: { "content-type": "multipart/form-data" }, + body: { + id: "123", + "fruits[]_**[]1": "apple", + "fruits[]_**[]2": "banana", + "fruits[]_**[]3": "grape" + } + } + + let curlified = curl(Im.fromJS(req)) + + expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"content-type: multipart/form-data\" -F \"id=123\" -F \"fruits[]=apple\" -F \"fruits[]=banana\" -F \"fruits[]=grape\"") + }) + it("should print a curl with formData and file", function() { var file = new win.File() file.name = "file.txt"