diff --git a/src/core/components/info.jsx b/src/core/components/info.jsx index 0ebf8811707..2926bd77a1d 100644 --- a/src/core/components/info.jsx +++ b/src/core/components/info.jsx @@ -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 (
@@ -88,7 +92,7 @@ export class InfoUrl extends React.PureComponent { const Link = getComponent("Link") - return { url } + return { url } } } @@ -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 {
{ - termsOfService &&
- Terms of service + termsOfServiceUrl &&
+ Terms of service
} - {contact && contact.size ? : null } - {license && license.size ? : null } - { externalDocsUrl ? + {contact && contact.size ? : null } + {license && license.size ? : null } + { externalDocs ? {externalDocsDescription || externalDocsUrl} : null } diff --git a/src/core/components/operation-tag.jsx b/src/core/components/operation-tag.jsx index d2e3f31da62..1e71c58797f 100644 --- a/src/core/components/operation-tag.jsx +++ b/src/core/components/operation-tag.jsx @@ -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") diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index a4a799772ff..117d8227137 100644 --- a/src/core/components/operation.jsx +++ b/src/core/components/operation.jsx @@ -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 {
} { - externalDocs && externalDocs.url ? + externalDocsUrl ?

Find more details

- {externalDocs.url} + {externalDocsUrl}
: null } diff --git a/src/core/components/operations.jsx b/src/core/components/operations.jsx index 68244177452..505217ffbda 100644 --- a/src/core/components/operations.jsx +++ b/src/core/components/operations.jsx @@ -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") diff --git a/src/core/containers/info.jsx b/src/core/containers/info.jsx index 4ae558c7d50..ebf01a1f71d 100644 --- a/src/core/containers/info.jsx +++ b/src/core/containers/info.jsx @@ -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 {
{info && info.count() ? ( + getComponent={getComponent} selectedServer={selectedServer} /> ) : null}
) diff --git a/src/core/utils.js b/src/core/utils.js index 00078cec501..30b3f5fd641 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -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 diff --git a/src/core/utils/url.js b/src/core/utils/url.js new file mode 100644 index 00000000000..2f063445de4 --- /dev/null +++ b/src/core/utils/url.js @@ -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 +} diff --git a/test/mocha/components/info-wrapper.jsx b/test/mocha/components/info-wrapper.jsx index fcb98d6f4f8..ba8be69634e 100644 --- a/test/mocha/components/info-wrapper.jsx +++ b/test/mocha/components/info-wrapper.jsx @@ -17,7 +17,10 @@ describe("", function () { url () {}, basePath () {}, host () {}, - externalDocs () {} + externalDocs () {}, + }, + oas3Selectors: { + selectedServer () {}, }, getComponent: c => components[c] } diff --git a/test/mocha/components/operations.jsx b/test/mocha/components/operations.jsx index 844a4931be1..4da89427696 100644 --- a/test/mocha/components/operations.jsx +++ b/test/mocha/components/operations.jsx @@ -29,6 +29,7 @@ describe("", function(){ }, specSelectors: { isOAS3() { return false }, + url() { return "https://petstore.swagger.io/v2/swagger.json" }, taggedOperations() { return fromJS({ "default": { @@ -83,6 +84,7 @@ describe("", function(){ }, specSelectors: { isOAS3() { return true }, + url() { return "https://petstore.swagger.io/v2/swagger.json" }, taggedOperations() { return fromJS({ "default": { diff --git a/test/mocha/core/utils.js b/test/mocha/core/utils.js index b7cbe8f9d76..e55c4cbb3d9 100644 --- a/test/mocha/core/utils.js +++ b/test/mocha/core/utils.js @@ -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") diff --git a/test/mocha/xss/info-sanitization.jsx b/test/mocha/xss/info-sanitization.jsx index e868fe9ffa6..f73b770af3e 100644 --- a/test/mocha/xss/info-sanitization.jsx +++ b/test/mocha/xss/info-sanitization.jsx @@ -18,7 +18,8 @@ describe(" Sanitization", function(){ description: "Description *with* " }), host: "example.test", - basePath: "/api" + basePath: "/api", + selectedServer: "https://example.test", } it("renders sanitized .title content", function(){