Files
swagger-ui/src/core/utils.js
geraldglynn d9f5691f65 fix(OAS3): relative urls (#5341)
* Added tooling for appending OAS3 relative URLs to selected Server

Info
* Terms of service URL
* Contact URL
* License URL
* External Docs URL

Tag
* Tag External Docs URL

Operation
* Operation External Docs
** Operation Tag


Co-authored-by: Tim Lai <timothy.lai@gmail.com>
2020-08-04 13:21:19 -07:00

810 lines
22 KiB
JavaScript

/*
ATTENTION! This file (but not the functions within) is deprecated.
You should probably add a new file to `./helpers/` instead of adding a new
function here.
One-function-per-file is a better pattern than what we have here.
If you're refactoring something in here, feel free to break it out to a file
in `./helpers` if you have the time.
*/
import Im from "immutable"
import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url"
import camelCase from "lodash/camelCase"
import upperFirst from "lodash/upperFirst"
import _memoize from "lodash/memoize"
import find from "lodash/find"
import some from "lodash/some"
import eq from "lodash/eq"
import isFunction from "lodash/isFunction"
import { memoizedSampleFromSchema, memoizedCreateXMLExample } from "core/plugins/samples/fn"
import win from "./window"
import cssEscape from "css.escape"
import getParameterSchema from "../helpers/get-parameter-schema"
import randomBytes from "randombytes"
import shaJs from "sha.js"
const DEFAULT_RESPONSE_KEY = "default"
export const isImmutable = (maybe) => Im.Iterable.isIterable(maybe)
export function isJSONObject (str) {
try {
var o = JSON.parse(str)
// Handle non-exception-throwing cases:
// Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking,
// but... JSON.parse(null) returns null, and typeof null === "object",
// so we must check for that, too. Thankfully, null is falsey, so this suffices:
if (o && typeof o === "object") {
return o
}
}
catch (e) {
// do nothing
}
return false
}
export function objectify (thing) {
if(!isObject(thing))
return {}
if(isImmutable(thing))
return thing.toJS()
return thing
}
export function arrayify (thing) {
if(!thing)
return []
if(thing.toArray)
return thing.toArray()
return normalizeArray(thing)
}
export function fromJSOrdered(js) {
if (isImmutable(js)) {
return js // Can't do much here
}
if (js instanceof win.File) {
return js
}
if (!isObject(js)) {
return js
}
if (Array.isArray(js)) {
return Im.Seq(js).map(fromJSOrdered).toList()
}
if (isFunction(js.entries)) {
// handle multipart/form-data
const objWithHashedKeys = createObjWithHashedKeys(js)
return Im.OrderedMap(objWithHashedKeys).map(fromJSOrdered)
}
return 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 = <original>
* if multiple, key name = <original><hashIdx><count>
* @example <caption>single entry for vegetable</caption>
* fdObj.entries.vegtables: "carrot"
* // returns newObj.vegetables : "carrot"
* @example <caption>multiple entries for fruits[]</caption>
* fdObj.entries.fruits[]: "apple"
* // returns newObj.fruits[]_**[]1 : "apple"
* fdObj.entries.fruits[]: "banana"
* // returns newObj.fruits[]_**[]2 : "banana"
* fdObj.entries.fruits[]: "grape"
* // returns newObj.fruits[]_**[]3 : "grape"
* @param {FormData} fdObj - a FormData object
* @return {Object} - a plain object
*/
export function createObjWithHashedKeys (fdObj) {
if (!isFunction(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) {
var newObj = {}
Object.keys(obj)
.filter(key => typeof obj[key] === "function")
.forEach(key => newObj[key] = obj[key].bind(null, state))
return newObj
}
export function normalizeArray(arr) {
if(Array.isArray(arr))
return arr
return [arr]
}
export function isFn(fn) {
return typeof fn === "function"
}
export function isObject(obj) {
return !!obj && typeof obj === "object"
}
export function isFunc(thing) {
return typeof(thing) === "function"
}
export function isArray(thing) {
return Array.isArray(thing)
}
// I've changed memoize libs more than once, so I'm using this a way to make that simpler
export const memoize = _memoize
export function objMap(obj, fn) {
return Object.keys(obj).reduce((newObj, key) => {
newObj[key] = fn(obj[key], key)
return newObj
}, {})
}
export function objReduce(obj, fn) {
return Object.keys(obj).reduce((newObj, key) => {
let res = fn(obj[key], key)
if(res && typeof res === "object")
Object.assign(newObj, res)
return newObj
}, {})
}
// Redux middleware that exposes the system to async actions (like redux-thunk, but with out system instead of (dispatch, getState)
export function systemThunkMiddleware(getSystem) {
return ({ dispatch, getState }) => { // eslint-disable-line no-unused-vars
return next => action => {
if (typeof action === "function") {
return action(getSystem())
}
return next(action)
}
}
}
export function defaultStatusCode ( responses ) {
let codes = responses.keySeq()
return codes.contains(DEFAULT_RESPONSE_KEY) ? DEFAULT_RESPONSE_KEY : codes.filter( key => (key+"")[0] === "2").sort().first()
}
/**
* Returns an Immutable List, safely
* @param {Immutable.Iterable} iterable the iterable to get the key from
* @param {String|[String]} key either an array of keys, or a single key
* @returns {Immutable.List} either iterable.get(keys) or an empty Immutable.List
*/
export function getList(iterable, keys) {
if(!Im.Iterable.isIterable(iterable)) {
return Im.List()
}
let val = iterable.getIn(Array.isArray(keys) ? keys : [keys])
return Im.List.isList(val) ? val : Im.List()
}
/**
* Take an immutable map, and convert to a list.
* Where the keys are merged with the value objects
* @param {Immutable.Map} map, the map to convert
* @param {String} key the key to use, when merging the `key`
* @returns {Immutable.List}
*/
export function mapToList(map, keyNames="key", collectedKeys=Im.Map()) {
if(!Im.Map.isMap(map) || !map.size) {
return Im.List()
}
if(!Array.isArray(keyNames)) {
keyNames = [ keyNames ]
}
if(keyNames.length < 1) {
return map.merge(collectedKeys)
}
// I need to avoid `flatMap` from merging in the Maps, as well as the lists
let list = Im.List()
let keyName = keyNames[0]
for(let entry of map.entries()) {
let [key, val] = entry
let nextList = mapToList(val, keyNames.slice(1), collectedKeys.set(keyName, key))
if(Im.List.isList(nextList)) {
list = list.concat(nextList)
} else {
list = list.push(nextList)
}
}
return list
}
export function extractFileNameFromContentDispositionHeader(value){
let patterns = [
/filename\*=[^']+'\w*'"([^"]+)";?/i,
/filename\*=[^']+'\w*'([^;]+);?/i,
/filename="([^;]*);?"/i,
/filename=([^;]*);?/i
]
let responseFilename
patterns.some(regex => {
responseFilename = regex.exec(value)
return responseFilename !== null
})
if (responseFilename !== null && responseFilename.length > 1) {
try {
return decodeURIComponent(responseFilename[1])
} catch(e) {
console.error(e)
}
}
return null
}
// PascalCase, aka UpperCamelCase
export function pascalCase(str) {
return upperFirst(camelCase(str))
}
// Remove the ext of a filename, and pascalCase it
export function pascalCaseFilename(filename) {
return pascalCase(filename.replace(/\.[^./]*$/, ""))
}
// Check if ...
// - new props
// - If immutable, use .is()
// - if in explicit objectList, then compare using _.eq
// - else use ===
export const propChecker = (props, nextProps, objectList=[], ignoreList=[]) => {
if(Object.keys(props).length !== Object.keys(nextProps).length) {
return true
}
return (
some(props, (a, name) => {
if(ignoreList.includes(name)) {
return false
}
let b = nextProps[name]
if(Im.Iterable.isIterable(a)) {
return !Im.is(a,b)
}
// Not going to compare objects
if(typeof a === "object" && typeof b === "object") {
return false
}
return a !== b
})
|| objectList.some( objectPropName => !eq(props[objectPropName], nextProps[objectPropName])))
}
export const validateMaximum = ( val, max ) => {
if (val > max) {
return `Value must be less than ${max}`
}
}
export const validateMinimum = ( val, min ) => {
if (val < min) {
return `Value must be greater than ${min}`
}
}
export const validateNumber = ( val ) => {
if (!/^-?\d+(\.?\d+)?$/.test(val)) {
return "Value must be a number"
}
}
export const validateInteger = ( val ) => {
if (!/^-?\d+$/.test(val)) {
return "Value must be an integer"
}
}
export const validateFile = ( val ) => {
if ( val && !(val instanceof win.File) ) {
return "Value must be a file"
}
}
export const validateBoolean = ( val ) => {
if ( !(val === "true" || val === "false" || val === true || val === false) ) {
return "Value must be a boolean"
}
}
export const validateString = ( val ) => {
if ( val && typeof val !== "string" ) {
return "Value must be a string"
}
}
export const validateDateTime = (val) => {
if (isNaN(Date.parse(val))) {
return "Value must be a DateTime"
}
}
export const validateGuid = (val) => {
val = val.toString().toLowerCase()
if (!/^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[)}]?$/.test(val)) {
return "Value must be a Guid"
}
}
export const validateMaxLength = (val, max) => {
if (val.length > max) {
return `Value must be no longer than ${max} character${max !== 1 ? "s" : ""}`
}
}
export const validateMinLength = (val, min) => {
if (val.length < min) {
return `Value must be at least ${min} character${min !== 1 ? "s" : ""}`
}
}
export const validatePattern = (val, rxPattern) => {
var patt = new RegExp(rxPattern)
if (!patt.test(val)) {
return "Value must follow pattern " + rxPattern
}
}
// validation of parameters before execute
export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => {
let errors = []
let paramRequired = param.get("required")
let { schema: paramDetails, parameterContentMediaType } = getParameterSchema(param, { isOAS3 })
if(!paramDetails) return errors
let required = paramDetails.get("required")
let maximum = paramDetails.get("maximum")
let minimum = paramDetails.get("minimum")
let type = paramDetails.get("type")
let format = paramDetails.get("format")
let maxLength = paramDetails.get("maxLength")
let minLength = paramDetails.get("minLength")
let pattern = paramDetails.get("pattern")
/*
If the parameter is required OR the parameter has a value (meaning optional, but filled in)
then we should do our validation routine.
Only bother validating the parameter if the type was specified.
*/
if ( type && (paramRequired || required || value) ) {
// These checks should evaluate to true if there is a parameter
let stringCheck = type === "string" && value
let arrayCheck = type === "array" && Array.isArray(value) && value.length
let arrayListCheck = type === "array" && Im.List.isList(value) && value.count()
let arrayStringCheck = type === "array" && typeof value === "string" && value
let fileCheck = type === "file" && value instanceof win.File
let booleanCheck = type === "boolean" && (value || value === false)
let numberCheck = type === "number" && (value || value === 0)
let integerCheck = type === "integer" && (value || value === 0)
let objectCheck = type === "object" && typeof value === "object" && value !== null
let objectStringCheck = type === "object" && typeof value === "string" && value
const allChecks = [
stringCheck, arrayCheck, arrayListCheck, arrayStringCheck, fileCheck,
booleanCheck, numberCheck, integerCheck, objectCheck, objectStringCheck,
]
const passedAnyCheck = allChecks.some(v => !!v)
if ((paramRequired || required) && !passedAnyCheck && !bypassRequiredCheck ) {
errors.push("Required field is not provided")
return errors
}
if (
type === "object" &&
typeof value === "string" &&
(parameterContentMediaType === null ||
parameterContentMediaType === "application/json")
) {
try {
JSON.parse(value)
} catch (e) {
errors.push("Parameter string value must be valid JSON")
return errors
}
}
if (pattern) {
let err = validatePattern(value, pattern)
if (err) errors.push(err)
}
if (maxLength || maxLength === 0) {
let err = validateMaxLength(value, maxLength)
if (err) errors.push(err)
}
if (minLength) {
let err = validateMinLength(value, minLength)
if (err) errors.push(err)
}
if (maximum || maximum === 0) {
let err = validateMaximum(value, maximum)
if (err) errors.push(err)
}
if (minimum || minimum === 0) {
let err = validateMinimum(value, minimum)
if (err) errors.push(err)
}
if ( type === "string" ) {
let err
if (format === "date-time") {
err = validateDateTime(value)
} else if (format === "uuid") {
err = validateGuid(value)
} else {
err = validateString(value)
}
if (!err) return errors
errors.push(err)
} else if ( type === "boolean" ) {
let err = validateBoolean(value)
if (!err) return errors
errors.push(err)
} else if ( type === "number" ) {
let err = validateNumber(value)
if (!err) return errors
errors.push(err)
} else if ( type === "integer" ) {
let err = validateInteger(value)
if (!err) return errors
errors.push(err)
} else if ( type === "array" ) {
let itemType
if ( !arrayListCheck || !value.count() ) { return errors }
itemType = paramDetails.getIn(["items", "type"])
value.forEach((item, index) => {
let err
if (itemType === "number") {
err = validateNumber(item)
} else if (itemType === "integer") {
err = validateInteger(item)
} else if (itemType === "string") {
err = validateString(item)
}
if ( err ) {
errors.push({ index: index, error: err})
}
})
} else if ( type === "file" ) {
let err = validateFile(value)
if (!err) return errors
errors.push(err)
}
}
return errors
}
export const getSampleSchema = (schema, contentType="", config={}) => {
if (/xml/.test(contentType)) {
if (!schema.xml || !schema.xml.name) {
schema.xml = schema.xml || {}
if (schema.$$ref) {
let match = schema.$$ref.match(/\S*\/(\S+)$/)
schema.xml.name = match[1]
} else if (schema.type || schema.items || schema.properties || schema.additionalProperties) {
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- XML example cannot be generated; root element name is undefined -->"
} else {
return null
}
}
return memoizedCreateXMLExample(schema, config)
}
const res = memoizedSampleFromSchema(schema, config)
return typeof res === "object" ? JSON.stringify(res, null, 2) : res
}
export const parseSearch = () => {
let map = {}
let search = win.location.search
if(!search)
return {}
if ( search != "" ) {
let params = search.substr(1).split("&")
for (let i in params) {
if (!params.hasOwnProperty(i)) {
continue
}
i = params[i].split("=")
map[decodeURIComponent(i[0])] = (i[1] && decodeURIComponent(i[1])) || ""
}
}
return map
}
export const serializeSearch = (searchMap) => {
return Object.keys(searchMap).map(k => {
return encodeURIComponent(k) + "=" + encodeURIComponent(searchMap[k])
}).join("&")
}
export const btoa = (str) => {
let buffer
if (str instanceof Buffer) {
buffer = str
} else {
buffer = new Buffer(str.toString(), "utf-8")
}
return buffer.toString("base64")
}
export const sorters = {
operationsSorter: {
alpha: (a, b) => a.get("path").localeCompare(b.get("path")),
method: (a, b) => a.get("method").localeCompare(b.get("method"))
},
tagsSorter: {
alpha: (a, b) => a.localeCompare(b)
}
}
export const buildFormData = (data) => {
let formArr = []
for (let name in data) {
let val = data[name]
if (val !== undefined && val !== "") {
formArr.push([name, "=", encodeURIComponent(val).replace(/%20/g,"+")].join(""))
}
}
return formArr.join("&")
}
// Is this really required as a helper? Perhaps. TODO: expose the system of presets.apis in docs, so we know what is supported
export const shallowEqualKeys = (a,b, keys) => {
return !!find(keys, (key) => {
return eq(a[key], b[key])
})
}
export function sanitizeUrl(url) {
if(typeof url !== "string" || url === "") {
return ""
}
return braintreeSanitizeUrl(url)
}
export function requiresValidationURL(uri) {
if (!uri || uri.indexOf("localhost") >= 0 || uri.indexOf("127.0.0.1") >= 0 || uri === "none") {
return false
}
return true
}
export function getAcceptControllingResponse(responses) {
if(!Im.OrderedMap.isOrderedMap(responses)) {
// wrong type!
return null
}
if(!responses.size) {
// responses is empty
return null
}
const suitable2xxResponse = responses.find((res, k) => {
return k.startsWith("2") && Object.keys(res.get("content") || {}).length > 0
})
// try to find a suitable `default` responses
const defaultResponse = responses.get("default") || Im.OrderedMap()
const defaultResponseMediaTypes = (defaultResponse.get("content") || Im.OrderedMap()).keySeq().toJS()
const suitableDefaultResponse = defaultResponseMediaTypes.length ? defaultResponse : null
return suitable2xxResponse || suitableDefaultResponse
}
// suitable for use in URL fragments
export const createDeepLinkPath = (str) => typeof str == "string" || str instanceof String ? str.trim().replace(/\s/g, "%20") : ""
// suitable for use in CSS classes and ids
export const escapeDeepLinkPath = (str) => cssEscape( createDeepLinkPath(str).replace(/%20/g, "_") )
export const getExtensions = (defObj) => defObj.filter((v, k) => /^x-/.test(k))
export const getCommonExtensions = (defObj) => defObj.filter((v, k) => /^pattern|maxLength|minLength|maximum|minimum/.test(k))
// Deeply strips a specific key from an object.
//
// `predicate` can be used to discriminate the stripping further,
// by preserving the key's place in the object based on its value.
export function deeplyStripKey(input, keyToStrip, predicate = () => true) {
if(typeof input !== "object" || Array.isArray(input) || input === null || !keyToStrip) {
return input
}
const obj = Object.assign({}, input)
Object.keys(obj).forEach(k => {
if(k === keyToStrip && predicate(obj[k], k)) {
delete obj[k]
return
}
obj[k] = deeplyStripKey(obj[k], keyToStrip, predicate)
})
return obj
}
export function stringify(thing) {
if (typeof thing === "string") {
return thing
}
if (thing && thing.toJS) {
thing = thing.toJS()
}
if (typeof thing === "object" && thing !== null) {
try {
return JSON.stringify(thing, null, 2)
}
catch (e) {
return String(thing)
}
}
if(thing === null || thing === undefined) {
return ""
}
return thing.toString()
}
export function numberToString(thing) {
if(typeof thing === "number") {
return thing.toString()
}
return thing
}
export function paramToIdentifier(param, { returnAll = false, allowHashes = true } = {}) {
if(!Im.Map.isMap(param)) {
throw new Error("paramToIdentifier: received a non-Im.Map parameter as input")
}
const paramName = param.get("name")
const paramIn = param.get("in")
let generatedIdentifiers = []
// Generate identifiers in order of most to least specificity
if (param && param.hashCode && paramIn && paramName && allowHashes) {
generatedIdentifiers.push(`${paramIn}.${paramName}.hash-${param.hashCode()}`)
}
if(paramIn && paramName) {
generatedIdentifiers.push(`${paramIn}.${paramName}`)
}
generatedIdentifiers.push(paramName)
// Return the most preferred identifier, or all if requested
return returnAll ? generatedIdentifiers : (generatedIdentifiers[0] || "")
}
export function paramToValue(param, paramValues) {
const allIdentifiers = paramToIdentifier(param, { returnAll: true })
// Map identifiers to values in the provided value hash, filter undefined values,
// and return the first value found
const values = allIdentifiers
.map(id => {
return paramValues[id]
})
.filter(value => value !== undefined)
return values[0]
}
// adapted from https://auth0.com/docs/flows/guides/auth-code-pkce/includes/create-code-verifier
export function generateCodeVerifier() {
return b64toB64UrlEncoded(
randomBytes(32).toString("base64")
)
}
export function createCodeChallenge(codeVerifier) {
return b64toB64UrlEncoded(
shaJs("sha256")
.update(codeVerifier)
.digest("base64")
)
}
function b64toB64UrlEncoded(str) {
return str
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "")
}
export const isEmptyValue = (value) => {
if (!value) {
return true
}
if (isImmutable(value) && value.isEmpty()) {
return true
}
return false
}