* 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>bubble
@@ -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 | |||
@@ -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(){ | |||