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>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { fromJS } from "immutable"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
import { sanitizeUrl } from "core/utils"
|
||||
import { buildUrl } from "core/utils/url"
|
||||
|
||||
|
||||
export class InfoBasePath extends React.Component {
|
||||
@@ -26,13 +26,16 @@ export class InfoBasePath extends React.Component {
|
||||
class Contact extends React.Component {
|
||||
static propTypes = {
|
||||
data: PropTypes.object,
|
||||
getComponent: PropTypes.func.isRequired
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
specSelectors: PropTypes.object.isRequired,
|
||||
selectedServer: PropTypes.string,
|
||||
url: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
render(){
|
||||
let { data, getComponent } = this.props
|
||||
let { data, getComponent, selectedServer, url: specUrl} = this.props
|
||||
let name = data.get("name") || "the developer"
|
||||
let url = data.get("url")
|
||||
let url = buildUrl(data.get("url"), specUrl, {selectedServer})
|
||||
let email = data.get("email")
|
||||
|
||||
const Link = getComponent("Link")
|
||||
@@ -53,17 +56,18 @@ class Contact extends React.Component {
|
||||
class License extends React.Component {
|
||||
static propTypes = {
|
||||
license: PropTypes.object,
|
||||
getComponent: PropTypes.func.isRequired
|
||||
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
specSelectors: PropTypes.object.isRequired,
|
||||
selectedServer: PropTypes.string,
|
||||
url: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
render(){
|
||||
let { license, getComponent } = this.props
|
||||
let { license, getComponent, selectedServer, url: specUrl } = this.props
|
||||
|
||||
const Link = getComponent("Link")
|
||||
|
||||
let name = license.get("name") || "License"
|
||||
let url = license.get("url")
|
||||
let name = license.get("name") || "License"
|
||||
let url = buildUrl(license.get("url"), specUrl, {selectedServer})
|
||||
|
||||
return (
|
||||
<div className="info__license">
|
||||
@@ -88,7 +92,7 @@ export class InfoUrl extends React.PureComponent {
|
||||
|
||||
const Link = getComponent("Link")
|
||||
|
||||
return <Link target="_blank" href={ sanitizeUrl(url) }><span className="url"> { url } </span></Link>
|
||||
return <Link target="_blank" href={ sanitizeUrl(url) }><span className="url"> { url }</span></Link>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,17 +104,21 @@ export default class Info extends React.Component {
|
||||
basePath: PropTypes.string,
|
||||
externalDocs: ImPropTypes.map,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
oas3selectors: PropTypes.func,
|
||||
selectedServer: PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
let { info, url, host, basePath, getComponent, externalDocs } = this.props
|
||||
let { info, url, host, basePath, getComponent, externalDocs, selectedServer, url: specUrl } = this.props
|
||||
let version = info.get("version")
|
||||
let description = info.get("description")
|
||||
let title = info.get("title")
|
||||
let termsOfService = info.get("termsOfService")
|
||||
let termsOfServiceUrl = buildUrl(info.get("termsOfService"), specUrl, {selectedServer})
|
||||
let contact = info.get("contact")
|
||||
let license = info.get("license")
|
||||
const { url:externalDocsUrl, description:externalDocsDescription } = (externalDocs || fromJS({})).toJS()
|
||||
let rawExternalDocsUrl = externalDocs && externalDocs.get("url")
|
||||
let externalDocsUrl = buildUrl(rawExternalDocsUrl, specUrl, {selectedServer})
|
||||
let externalDocsDescription = externalDocs && externalDocs.get("description")
|
||||
|
||||
const Markdown = getComponent("Markdown", true)
|
||||
const Link = getComponent("Link")
|
||||
@@ -133,14 +141,14 @@ export default class Info extends React.Component {
|
||||
</div>
|
||||
|
||||
{
|
||||
termsOfService && <div className="info__tos">
|
||||
<Link target="_blank" href={ sanitizeUrl(termsOfService) }>Terms of service</Link>
|
||||
termsOfServiceUrl && <div className="info__tos">
|
||||
<Link target="_blank" href={ sanitizeUrl(termsOfServiceUrl) }>Terms of service</Link>
|
||||
</div>
|
||||
}
|
||||
|
||||
{contact && contact.size ? <Contact getComponent={getComponent} data={ contact } /> : null }
|
||||
{license && license.size ? <License getComponent={getComponent} license={ license } /> : null }
|
||||
{ externalDocsUrl ?
|
||||
{contact && contact.size ? <Contact getComponent={getComponent} data={ contact } selectedServer={selectedServer} url={url} /> : null }
|
||||
{license && license.size ? <License getComponent={getComponent} license={ license } selectedServer={selectedServer} url={url}/> : null }
|
||||
{ externalDocs ?
|
||||
<Link className="info__extdocs" target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</Link>
|
||||
: null }
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from "prop-types"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
import Im from "immutable"
|
||||
import { createDeepLinkPath, escapeDeepLinkPath, sanitizeUrl } from "core/utils"
|
||||
import { buildUrl } from "core/utils/url"
|
||||
|
||||
export default class OperationTag extends React.Component {
|
||||
|
||||
@@ -15,12 +16,15 @@ export default class OperationTag extends React.Component {
|
||||
tagObj: ImPropTypes.map.isRequired,
|
||||
tag: PropTypes.string.isRequired,
|
||||
|
||||
oas3Selectors: PropTypes.func.isRequired,
|
||||
layoutSelectors: PropTypes.object.isRequired,
|
||||
layoutActions: PropTypes.object.isRequired,
|
||||
|
||||
getConfigs: PropTypes.func.isRequired,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
|
||||
specUrl: PropTypes.string.isRequired,
|
||||
|
||||
children: PropTypes.element,
|
||||
}
|
||||
|
||||
@@ -29,11 +33,12 @@ export default class OperationTag extends React.Component {
|
||||
tagObj,
|
||||
tag,
|
||||
children,
|
||||
|
||||
oas3Selectors,
|
||||
layoutSelectors,
|
||||
layoutActions,
|
||||
getConfigs,
|
||||
getComponent,
|
||||
specUrl,
|
||||
} = this.props
|
||||
|
||||
let {
|
||||
@@ -50,7 +55,8 @@ export default class OperationTag extends React.Component {
|
||||
|
||||
let tagDescription = tagObj.getIn(["tagDetails", "description"], null)
|
||||
let tagExternalDocsDescription = tagObj.getIn(["tagDetails", "externalDocs", "description"])
|
||||
let tagExternalDocsUrl = tagObj.getIn(["tagDetails", "externalDocs", "url"])
|
||||
let rawTagExternalDocsUrl = tagObj.getIn(["tagDetails", "externalDocs", "url"])
|
||||
let tagExternalDocsUrl = buildUrl( rawTagExternalDocsUrl, specUrl, {selectedServer: oas3Selectors.selectedServer()} )
|
||||
|
||||
let isShownKey = ["operations-tag", tag]
|
||||
let showTag = layoutSelectors.isShown(isShownKey, docExpansion === "full" || docExpansion === "list")
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { PureComponent } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { getList } from "core/utils"
|
||||
import { getExtensions, sanitizeUrl, escapeDeepLinkPath } from "core/utils"
|
||||
import { buildUrl } from "core/utils/url"
|
||||
import { Iterable, List } from "immutable"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
|
||||
@@ -81,6 +82,7 @@ export default class Operation extends PureComponent {
|
||||
schemes
|
||||
} = op
|
||||
|
||||
const externalDocsUrl = externalDocs ? buildUrl(externalDocs.url, specSelectors.url(), { selectedServer: oas3Selectors.selectedServer() }) : ""
|
||||
let operation = operationProps.getIn(["op"])
|
||||
let responses = operation.get("responses")
|
||||
let parameters = getList(operation, ["parameters"])
|
||||
@@ -127,14 +129,14 @@ export default class Operation extends PureComponent {
|
||||
</div>
|
||||
}
|
||||
{
|
||||
externalDocs && externalDocs.url ?
|
||||
externalDocsUrl ?
|
||||
<div className="opblock-external-docs-wrapper">
|
||||
<h4 className="opblock-title_normal">Find more details</h4>
|
||||
<div className="opblock-external-docs">
|
||||
<span className="opblock-external-docs__description">
|
||||
<Markdown source={ externalDocs.description } />
|
||||
</span>
|
||||
<Link target="_blank" className="opblock-external-docs__link" href={sanitizeUrl(externalDocs.url)}>{externalDocs.url}</Link>
|
||||
<Link target="_blank" className="opblock-external-docs__link" href={sanitizeUrl(externalDocsUrl)}>{externalDocsUrl}</Link>
|
||||
</div>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export default class Operations extends React.Component {
|
||||
specActions: PropTypes.object.isRequired,
|
||||
oas3Actions: PropTypes.object.isRequired,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
oas3Selectors: PropTypes.func.isRequired,
|
||||
layoutSelectors: PropTypes.object.isRequired,
|
||||
layoutActions: PropTypes.object.isRequired,
|
||||
authActions: PropTypes.object.isRequired,
|
||||
@@ -28,6 +29,7 @@ export default class Operations extends React.Component {
|
||||
let {
|
||||
specSelectors,
|
||||
getComponent,
|
||||
oas3Selectors,
|
||||
layoutSelectors,
|
||||
layoutActions,
|
||||
getConfigs,
|
||||
@@ -65,10 +67,12 @@ export default class Operations extends React.Component {
|
||||
key={"operation-" + tag}
|
||||
tagObj={tagObj}
|
||||
tag={tag}
|
||||
oas3Selectors={oas3Selectors}
|
||||
layoutSelectors={layoutSelectors}
|
||||
layoutActions={layoutActions}
|
||||
getConfigs={getConfigs}
|
||||
getComponent={getComponent}>
|
||||
getComponent={getComponent}
|
||||
specUrl={specSelectors.url()}>
|
||||
{
|
||||
operations.map( op => {
|
||||
const path = op.get("path")
|
||||
|
||||
@@ -7,16 +7,18 @@ export default class InfoContainer extends React.Component {
|
||||
specActions: PropTypes.object.isRequired,
|
||||
specSelectors: PropTypes.object.isRequired,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
oas3Selectors: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render () {
|
||||
const {specSelectors, getComponent} = this.props
|
||||
const {specSelectors, getComponent, oas3Selectors} = this.props
|
||||
|
||||
const info = specSelectors.info()
|
||||
const url = specSelectors.url()
|
||||
const basePath = specSelectors.basePath()
|
||||
const host = specSelectors.host()
|
||||
const externalDocs = specSelectors.externalDocs()
|
||||
const selectedServer = oas3Selectors.selectedServer()
|
||||
|
||||
const Info = getComponent("info")
|
||||
|
||||
@@ -24,7 +26,7 @@ export default class InfoContainer extends React.Component {
|
||||
<div>
|
||||
{info && info.count() ? (
|
||||
<Info info={info} url={url} host={host} basePath={basePath} externalDocs={externalDocs}
|
||||
getComponent={getComponent}/>
|
||||
getComponent={getComponent} selectedServer={selectedServer} />
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -399,7 +399,7 @@ export const validatePattern = (val, rxPattern) => {
|
||||
|
||||
// validation of parameters before execute
|
||||
export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => {
|
||||
|
||||
|
||||
let errors = []
|
||||
|
||||
let paramRequired = param.get("required")
|
||||
@@ -436,7 +436,7 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
|
||||
let objectStringCheck = type === "object" && typeof value === "string" && value
|
||||
|
||||
const allChecks = [
|
||||
stringCheck, arrayCheck, arrayListCheck, arrayStringCheck, fileCheck,
|
||||
stringCheck, arrayCheck, arrayListCheck, arrayStringCheck, fileCheck,
|
||||
booleanCheck, numberCheck, integerCheck, objectCheck, objectStringCheck,
|
||||
]
|
||||
|
||||
@@ -640,7 +640,6 @@ export function sanitizeUrl(url) {
|
||||
return braintreeSanitizeUrl(url)
|
||||
}
|
||||
|
||||
|
||||
export function requiresValidationURL(uri) {
|
||||
if (!uri || uri.indexOf("localhost") >= 0 || uri.indexOf("127.0.0.1") >= 0 || uri === "none") {
|
||||
return false
|
||||
|
||||
23
src/core/utils/url.js
Normal file
23
src/core/utils/url.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export function isAbsoluteUrl(url) {
|
||||
return url.match(/^(?:[a-z]+:)?\/\//i) // Matches http://, HTTP://, https://, ftp://, //example.com,
|
||||
}
|
||||
|
||||
export function addProtocol(url) {
|
||||
if(!url.match(/^\/\//i)) return url // Checks if protocol is missing e.g. //example.com
|
||||
return `${window.location.protocol}${url}`
|
||||
}
|
||||
|
||||
export function buildBaseUrl(selectedServer, specUrl) {
|
||||
if(!selectedServer) return specUrl
|
||||
if(isAbsoluteUrl(selectedServer)) return addProtocol(selectedServer)
|
||||
|
||||
return new URL(selectedServer, specUrl).href
|
||||
}
|
||||
|
||||
export function buildUrl(url, specUrl, { selectedServer="" } = {}) {
|
||||
if(!url) return
|
||||
if(isAbsoluteUrl(url)) return url
|
||||
|
||||
const baseUrl = buildBaseUrl(selectedServer, specUrl)
|
||||
return new URL(url, baseUrl).href
|
||||
}
|
||||
@@ -17,7 +17,10 @@ describe("<InfoContainer/>", function () {
|
||||
url () {},
|
||||
basePath () {},
|
||||
host () {},
|
||||
externalDocs () {}
|
||||
externalDocs () {},
|
||||
},
|
||||
oas3Selectors: {
|
||||
selectedServer () {},
|
||||
},
|
||||
getComponent: c => components[c]
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ describe("<Operations/>", function(){
|
||||
},
|
||||
specSelectors: {
|
||||
isOAS3() { return false },
|
||||
url() { return "https://petstore.swagger.io/v2/swagger.json" },
|
||||
taggedOperations() {
|
||||
return fromJS({
|
||||
"default": {
|
||||
@@ -83,6 +84,7 @@ describe("<Operations/>", function(){
|
||||
},
|
||||
specSelectors: {
|
||||
isOAS3() { return true },
|
||||
url() { return "https://petstore.swagger.io/v2/swagger.json" },
|
||||
taggedOperations() {
|
||||
return fromJS({
|
||||
"default": {
|
||||
|
||||
@@ -32,6 +32,13 @@ import {
|
||||
generateCodeVerifier,
|
||||
createCodeChallenge,
|
||||
} from "core/utils"
|
||||
|
||||
import {
|
||||
isAbsoluteUrl,
|
||||
buildBaseUrl,
|
||||
buildUrl,
|
||||
} from "core/utils/url"
|
||||
|
||||
import win from "core/window"
|
||||
|
||||
describe("utils", function() {
|
||||
@@ -1334,6 +1341,92 @@ describe("utils", function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe("isAbsoluteUrl", function() {
|
||||
|
||||
it("check if url is absolute", function() {
|
||||
expect(!!isAbsoluteUrl("http://example.com")).toEqual(true)
|
||||
expect(!!isAbsoluteUrl("https://secure-example.com")).toEqual(true)
|
||||
expect(!!isAbsoluteUrl("HTTP://uppercase-example.com")).toEqual(true)
|
||||
expect(!!isAbsoluteUrl("HTTP://uppercase-secure-example.com")).toEqual(true)
|
||||
expect(!!isAbsoluteUrl("http://trailing-slash.com/")).toEqual(true)
|
||||
expect(!!isAbsoluteUrl("ftp://file-transfer-protocol.com")).toEqual(true)
|
||||
expect(!!isAbsoluteUrl("//no-protocol.com")).toEqual(true)
|
||||
})
|
||||
|
||||
it("check if url is not absolute", function() {
|
||||
expect(!!isAbsoluteUrl("/url-relative-to-host/base-path/path")).toEqual(false)
|
||||
expect(!!isAbsoluteUrl("url-relative-to-base/base-path/path")).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildBaseUrl", function() {
|
||||
const specUrl = "https://petstore.swagger.io/v2/swagger.json"
|
||||
|
||||
const noServerSelected = ""
|
||||
const absoluteServerUrl = "https://server-example.com/base-path/path"
|
||||
const serverUrlRelativeToBase = "server-example/base-path/path"
|
||||
const serverUrlRelativeToHost = "/server-example/base-path/path"
|
||||
|
||||
it("build base url with no server selected", function() {
|
||||
expect(buildBaseUrl(noServerSelected, specUrl)).toBe("https://petstore.swagger.io/v2/swagger.json")
|
||||
})
|
||||
|
||||
it("build base url from absolute server url", function() {
|
||||
expect(buildBaseUrl(absoluteServerUrl, specUrl)).toBe("https://server-example.com/base-path/path")
|
||||
})
|
||||
|
||||
it("build base url from relative server url", function() {
|
||||
expect(buildBaseUrl(serverUrlRelativeToBase, specUrl)).toBe("https://petstore.swagger.io/v2/server-example/base-path/path")
|
||||
expect(buildBaseUrl(serverUrlRelativeToHost, specUrl)).toBe("https://petstore.swagger.io/server-example/base-path/path")
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildUrl", function() {
|
||||
const specUrl = "https://petstore.swagger.io/v2/swagger.json"
|
||||
|
||||
const noUrl = ""
|
||||
const absoluteUrl = "https://example.com/base-path/path"
|
||||
const urlRelativeToBase = "relative-url/base-path/path"
|
||||
const urlRelativeToHost = "/relative-url/base-path/path"
|
||||
|
||||
const noServerSelected = ""
|
||||
const absoluteServerUrl = "https://server-example.com/base-path/path"
|
||||
const serverUrlRelativeToBase = "server-example/base-path/path"
|
||||
const serverUrlRelativeToHost = "/server-example/base-path/path"
|
||||
|
||||
it("build no url", function() {
|
||||
expect(buildUrl(noUrl, specUrl, { selectedServer: absoluteServerUrl })).toBe(undefined)
|
||||
expect(buildUrl(noUrl, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe(undefined)
|
||||
expect(buildUrl(noUrl, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe(undefined)
|
||||
})
|
||||
|
||||
it("build absolute url", function() {
|
||||
expect(buildUrl(absoluteUrl, specUrl, { selectedServer: absoluteServerUrl })).toBe("https://example.com/base-path/path")
|
||||
expect(buildUrl(absoluteUrl, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe("https://example.com/base-path/path")
|
||||
expect(buildUrl(absoluteUrl, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe("https://example.com/base-path/path")
|
||||
})
|
||||
|
||||
it("build relative url with no server selected", function() {
|
||||
expect(buildUrl(urlRelativeToBase, specUrl, { selectedServer: noServerSelected })).toBe("https://petstore.swagger.io/v2/relative-url/base-path/path")
|
||||
expect(buildUrl(urlRelativeToHost, specUrl, { selectedServer: noServerSelected })).toBe("https://petstore.swagger.io/relative-url/base-path/path")
|
||||
})
|
||||
|
||||
it("build relative url with absolute server url", function() {
|
||||
expect(buildUrl(urlRelativeToBase, specUrl, { selectedServer: absoluteServerUrl })).toBe("https://server-example.com/base-path/relative-url/base-path/path")
|
||||
expect(buildUrl(urlRelativeToHost, specUrl, { selectedServer: absoluteServerUrl })).toBe("https://server-example.com/relative-url/base-path/path")
|
||||
})
|
||||
|
||||
it("build relative url with server url relative to base", function() {
|
||||
expect(buildUrl(urlRelativeToBase, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe("https://petstore.swagger.io/v2/server-example/base-path/relative-url/base-path/path")
|
||||
expect(buildUrl(urlRelativeToHost, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe("https://petstore.swagger.io/relative-url/base-path/path")
|
||||
})
|
||||
|
||||
it("build relative url with server url relative to host", function() {
|
||||
expect(buildUrl(urlRelativeToBase, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe("https://petstore.swagger.io/server-example/base-path/relative-url/base-path/path")
|
||||
expect(buildUrl(urlRelativeToHost, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe("https://petstore.swagger.io/relative-url/base-path/path")
|
||||
})
|
||||
})
|
||||
|
||||
describe("requiresValidationURL", function() {
|
||||
it("Should tell us if we require a ValidationURL", function() {
|
||||
const res = requiresValidationURL("https://example.com")
|
||||
|
||||
@@ -18,7 +18,8 @@ describe("<Info/> Sanitization", function(){
|
||||
description: "Description *with* <script>Markdown</script>"
|
||||
}),
|
||||
host: "example.test",
|
||||
basePath: "/api"
|
||||
basePath: "/api",
|
||||
selectedServer: "https://example.test",
|
||||
}
|
||||
|
||||
it("renders sanitized .title content", function(){
|
||||
|
||||
Reference in New Issue
Block a user