Skip to content

Commit 651e857

Browse files
abcangtim-lai
andauthored
fix: Encode form data according to Encoding Object (#1500)
Refs: #1470 Co-authored-by: Tim Lai <[email protected]>
1 parent 9672aea commit 651e857

File tree

8 files changed

+311
-184
lines changed

8 files changed

+311
-184
lines changed

src/execute/oas3/build-request.js

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import assign from 'lodash/assign'
44
import get from 'lodash/get'
55
import btoa from 'btoa'
6-
import {Buffer} from 'buffer/'
76

87
export default function (options, req) {
98
const {
@@ -50,49 +49,15 @@ export default function (options, req) {
5049
if (requestBodyMediaTypes.indexOf(requestContentType) > -1) {
5150
// only attach body if the requestBody has a definition for the
5251
// contentType that has been explicitly set
53-
if (requestContentType === 'application/x-www-form-urlencoded' || requestContentType.indexOf('multipart/') === 0) {
52+
if (requestContentType === 'application/x-www-form-urlencoded' || requestContentType === 'multipart/form-data') {
5453
if (typeof requestBody === 'object') {
54+
const encoding = (requestBodyDef.content[requestContentType] || {}).encoding || {}
55+
5556
req.form = {}
5657
Object.keys(requestBody).forEach((k) => {
57-
const val = requestBody[k]
58-
let newVal
59-
let isFile
60-
let isOAS3formatArray = false // oas3 query (default false) vs oas3 multipart
61-
62-
if (typeof File !== 'undefined') {
63-
isFile = val instanceof File // eslint-disable-line no-undef
64-
}
65-
66-
if (typeof Blob !== 'undefined') {
67-
isFile = isFile || val instanceof Blob // eslint-disable-line no-undef
68-
}
69-
70-
if (typeof Buffer !== 'undefined') {
71-
isFile = isFile || Buffer.isBuffer(val)
72-
}
73-
74-
if (typeof val === 'object' && !isFile) {
75-
if (Array.isArray(val)) {
76-
if (requestContentType === 'application/x-www-form-urlencoded') {
77-
newVal = val.toString()
78-
}
79-
else {
80-
// multipart case
81-
newVal = val // keep as array
82-
isOAS3formatArray = true
83-
}
84-
}
85-
else {
86-
newVal = JSON.stringify(val)
87-
}
88-
}
89-
else {
90-
newVal = val
91-
}
92-
9358
req.form[k] = {
94-
value: newVal,
95-
isOAS3formatArray
59+
value: requestBody[k],
60+
encoding: encoding[k] || {},
9661
}
9762
})
9863
}

src/execute/oas3/parameter-builders.js

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pick from 'lodash/pick'
12
import stylize, {encodeDisallowedCharacters} from './style-serializer'
23
import serialize from './content-serializer'
34

@@ -46,58 +47,9 @@ export function query({req, value, parameter}) {
4647
}
4748

4849
if (value) {
49-
const type = typeof value
50-
51-
if (parameter.style === 'deepObject') {
52-
const valueKeys = Object.keys(value)
53-
valueKeys.forEach((k) => {
54-
const v = value[k]
55-
req.query[`${parameter.name}[${k}]`] = {
56-
value: stylize({
57-
key: k,
58-
value: v,
59-
style: 'deepObject',
60-
escape: parameter.allowReserved ? 'unsafe' : 'reserved',
61-
}),
62-
skipEncoding: true
63-
}
64-
})
65-
}
66-
else if (
67-
type === 'object' &&
68-
!Array.isArray(value) &&
69-
(parameter.style === 'form' || !parameter.style) &&
70-
(parameter.explode || parameter.explode === undefined)
71-
) {
72-
// form explode needs to be handled here,
73-
// since we aren't assigning to `req.query[parameter.name]`
74-
// like we usually do.
75-
const valueKeys = Object.keys(value)
76-
valueKeys.forEach((k) => {
77-
const v = value[k]
78-
req.query[k] = {
79-
value: stylize({
80-
key: k,
81-
value: v,
82-
style: parameter.style || 'form',
83-
escape: parameter.allowReserved ? 'unsafe' : 'reserved',
84-
}),
85-
skipEncoding: true
86-
}
87-
})
88-
}
89-
else {
90-
const encodedParamName = encodeURIComponent(parameter.name)
91-
req.query[encodedParamName] = {
92-
value: stylize({
93-
key: encodedParamName,
94-
value,
95-
style: parameter.style || 'form',
96-
explode: typeof parameter.explode === 'undefined' ? true : parameter.explode,
97-
escape: parameter.allowReserved ? 'unsafe' : 'reserved',
98-
}),
99-
skipEncoding: true
100-
}
50+
req.query[parameter.name] = {
51+
value,
52+
serializationOption: pick(parameter, ['style', 'explode', 'allowReserved'])
10153
}
10254
}
10355
else if (parameter.allowEmptyValue && value !== undefined) {

src/http.js

Lines changed: 142 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import 'cross-fetch/polyfill' /* global fetch */
22
import qs from 'qs'
33
import jsYaml from 'js-yaml'
4-
import isString from 'lodash/isString'
4+
import pick from 'lodash/pick'
55
import isFunction from 'lodash/isFunction'
6-
import isNil from 'lodash/isNil'
76
import FormData from './internal/form-data-monkey-patch'
7+
import {encodeDisallowedCharacters} from './execute/oas3/style-serializer'
8+
89

910
// For testing
1011
export const self = {
@@ -156,78 +157,169 @@ export function isFile(obj, navigatorObj) {
156157
}
157158
return false
158159
}
159-
if (typeof File !== 'undefined') {
160-
// eslint-disable-next-line no-undef
161-
return obj instanceof File
160+
161+
if (typeof File !== 'undefined' && obj instanceof File) { // eslint-disable-line no-undef
162+
return true
163+
}
164+
if (typeof Blob !== 'undefined' && obj instanceof Blob) { // eslint-disable-line no-undef
165+
return true
166+
}
167+
if (typeof Buffer !== 'undefined' && obj instanceof Buffer) {
168+
return true
162169
}
170+
163171
return obj !== null && typeof obj === 'object' && typeof obj.pipe === 'function'
164172
}
165173

166-
function formatValue(input, skipEncoding) {
167-
const {collectionFormat, allowEmptyValue} = input
168-
// `input` can be string in OAS3 contexts
169-
const value = typeof input === 'object' ? input.value : input
170-
const SEPARATORS = {
171-
csv: ',',
172-
ssv: '%20',
173-
tsv: '%09',
174-
pipes: '|'
175-
}
174+
function isArrayOfFile(obj, navigatorObj) {
175+
return (Array.isArray(obj) && obj.some(v => isFile(v, navigatorObj)))
176+
}
177+
178+
const STYLE_SEPARATORS = {
179+
form: ',',
180+
spaceDelimited: '%20',
181+
pipeDelimited: '|'
182+
}
183+
184+
const SEPARATORS = {
185+
csv: ',',
186+
ssv: '%20',
187+
tsv: '%09',
188+
pipes: '|'
189+
}
190+
191+
// Formats a key-value and returns an array of key-value pairs.
192+
//
193+
// Return value example 1: [['color', 'blue']]
194+
// Return value example 2: [['color', 'blue,black,brown']]
195+
// Return value example 3: [['color', ['blue', 'black', 'brown']]]
196+
// Return value example 4: [['color', 'R,100,G,200,B,150']]
197+
// Return value example 5: [['R', '100'], ['G', '200'], ['B', '150']]
198+
// Return value example 6: [['color[R]', '100'], ['color[G]', '200'], ['color[B]', '150']]
199+
function formatKeyValue(key, input, skipEncoding = false) {
200+
const {collectionFormat, allowEmptyValue, serializationOption, encoding} = input
201+
// `input` can be string
202+
const value = (typeof input === 'object' && !Array.isArray(input)) ? input.value : input
203+
const encodeFn = skipEncoding ? (k => k.toString()) : (k => encodeURIComponent(k))
204+
const encodedKey = encodeFn(key)
176205

177206
if (typeof value === 'undefined' && allowEmptyValue) {
178-
return ''
207+
return [[encodedKey, '']]
208+
}
209+
210+
// file
211+
if (isFile(value) || isArrayOfFile(value)) {
212+
return [[encodedKey, value]]
179213
}
180214

181-
if (isFile(value) || typeof value === 'boolean') {
182-
return value
215+
// for OAS 3 Parameter Object for serialization
216+
if (serializationOption) {
217+
return formatKeyValueBySerializationOption(key, value, skipEncoding, serializationOption)
183218
}
184219

185-
let encodeFn = encodeURIComponent
186-
// skipEncoding is an option to skip using the encodeURIComponent
187-
// and allow reassignment to a different "encoding" function
188-
// we should only use encodeURIComponent for known url strings
189-
if (skipEncoding) {
190-
if (isString(value) || Array.isArray(value)) {
191-
encodeFn = str => str
220+
// for OAS 3 Encoding Object
221+
if (encoding) {
222+
if ([typeof encoding.style, typeof encoding.explode, typeof encoding.allowReserved].some(type => type !== 'undefined')) {
223+
return formatKeyValueBySerializationOption(key, value, skipEncoding, pick(encoding, ['style', 'explode', 'allowReserved']))
192224
}
193-
else {
194-
encodeFn = obj => JSON.stringify(obj)
225+
226+
if (encoding.contentType) {
227+
if (encoding.contentType === 'application/json') {
228+
// If value is a string, assume value is already a JSON string
229+
const json = typeof value === 'string' ? value : JSON.stringify(value)
230+
return [[encodedKey, encodeFn(json)]]
231+
}
232+
return [[encodedKey, encodeFn(value.toString())]]
233+
}
234+
235+
// Primitive
236+
if (typeof value !== 'object') {
237+
return [[encodedKey, encodeFn(value)]]
238+
}
239+
240+
// Array of primitives
241+
if (Array.isArray(value) && value.every(v => typeof v !== 'object')) {
242+
return [[encodedKey, value.map(encodeFn).join(',')]]
243+
}
244+
245+
// Array or object
246+
return [[encodedKey, encodeFn(JSON.stringify(value))]]
247+
}
248+
249+
// for OAS 2 Parameter Object
250+
// Primitive
251+
if (typeof value !== 'object') {
252+
return [[encodedKey, encodeFn(value)]]
253+
}
254+
255+
// Array
256+
if (Array.isArray(value)) {
257+
if (collectionFormat === 'multi') {
258+
// In case of multipart/formdata, it is used as array.
259+
// Otherwise, the caller will convert it to a query by qs.stringify.
260+
return [[encodedKey, value.map(encodeFn)]]
195261
}
262+
263+
return [[encodedKey, value.map(encodeFn).join(SEPARATORS[collectionFormat || 'csv'])]]
196264
}
197265

198-
if (typeof value === 'object' && !Array.isArray(value)) {
199-
return ''
266+
// Object
267+
return [[encodedKey, '']]
268+
}
269+
270+
function formatKeyValueBySerializationOption(key, value, skipEncoding, serializationOption) {
271+
const style = serializationOption.style || 'form'
272+
const explode = typeof serializationOption.explode === 'undefined' ? style === 'form' : serializationOption.explode
273+
// eslint-disable-next-line no-nested-ternary
274+
const escape = skipEncoding ? false : (serializationOption && serializationOption.allowReserved ? 'unsafe' : 'reserved')
275+
const encodeFn = v => encodeDisallowedCharacters(v, {escape})
276+
const encodeKeyFn = skipEncoding ? (k => k) : (k => encodeDisallowedCharacters(k, {escape}))
277+
278+
// Primitive
279+
if (typeof value !== 'object') {
280+
return [[encodeKeyFn(key), encodeFn(value)]]
200281
}
201282

202-
if (!Array.isArray(value)) {
203-
return encodeFn(value)
283+
// Array
284+
if (Array.isArray(value)) {
285+
if (explode) {
286+
// In case of multipart/formdata, it is used as array.
287+
// Otherwise, the caller will convert it to a query by qs.stringify.
288+
return [[encodeKeyFn(key), value.map(encodeFn)]]
289+
}
290+
return [[encodeKeyFn(key), value.map(encodeFn).join(STYLE_SEPARATORS[style])]]
204291
}
205292

206-
if (Array.isArray(value) && !collectionFormat) {
207-
return value.map(encodeFn).join(',')
293+
// Object
294+
if (style === 'deepObject') {
295+
return Object.keys(value).map(valueKey => [encodeKeyFn(`${key}[${valueKey}]`), encodeFn(value[valueKey])])
208296
}
209-
if (collectionFormat === 'multi') {
210-
// query case (not multipart/formdata)
211-
return value.map(encodeFn)
297+
298+
if (explode) {
299+
return Object.keys(value).map(valueKey => [encodeKeyFn(valueKey), encodeFn(value[valueKey])])
212300
}
213-
return value.map(encodeFn).join(SEPARATORS[collectionFormat])
301+
302+
return [[encodeKeyFn(key), Object.keys(value).map(valueKey => [`${encodeKeyFn(valueKey)},${encodeFn(value[valueKey])}`]).join(',')]]
214303
}
215304

216305
function buildFormData(reqForm) {
217306
/**
218307
* Build a new FormData instance, support array as field value
219-
* OAS2.0 - via collectionFormat in spec definition
220-
* OAS3.0 - via oas3BuildRequest, isOAS3formatArray flag
308+
* OAS2.0 - when collectionFormat is multi
309+
* OAS3.0 - when explode of Encoding Object is true
221310
* @param {Object} reqForm - ori req.form
222311
* @return {FormData} - new FormData instance
223312
*/
224313
return Object.entries(reqForm).reduce((formData, [name, input]) => {
225-
if ((isNil(input.collectionFormat) || input.collectionFormat !== 'multi') && !input.isOAS3formatArray) {
226-
formData.append(name, formatValue(input, true))
227-
}
228-
else {
229-
input.value.forEach(item =>
230-
formData.append(name, formatValue({...input, value: item}, true)))
314+
for (const [key, value] of formatKeyValue(name, input, true)) {
315+
if (Array.isArray(value)) {
316+
for (const v of value) {
317+
formData.append(key, v)
318+
}
319+
}
320+
else {
321+
formData.append(key, value)
322+
}
231323
}
232324
return formData
233325
}, new FormData())
@@ -242,14 +334,9 @@ export function encodeFormOrQuery(data) {
242334
* @return {object} encoded parameter names and values
243335
*/
244336
const encodedQuery = Object.keys(data).reduce((result, parameterName) => {
245-
const isObject = a => a && typeof a === 'object'
246-
const paramValue = data[parameterName]
247-
const skipEncoding = !!paramValue.skipEncoding
248-
const encodedParameterName = skipEncoding ? parameterName : encodeURIComponent(parameterName)
249-
const notArray = isObject(paramValue) && !Array.isArray(paramValue)
250-
result[encodedParameterName] = formatValue(
251-
notArray ? paramValue : {value: paramValue}, skipEncoding
252-
)
337+
for (const [key, value] of formatKeyValue(parameterName, data[parameterName])) {
338+
result[key] = value
339+
}
253340
return result
254341
}, {})
255342
return qs.stringify(encodedQuery, {encode: false, indices: false}) || ''
@@ -266,7 +353,8 @@ export function mergeInQueryOrForm(req = {}) {
266353

267354
if (form) {
268355
const hasFile = Object.keys(form).some((key) => {
269-
return isFile(form[key].value)
356+
const value = form[key].value
357+
return isFile(value) || isArrayOfFile(value)
270358
})
271359

272360
const contentType = req.headers['content-type'] || req.headers['Content-Type']

0 commit comments

Comments
 (0)