diff --git a/src/core/components/info.jsx b/src/core/components/info.jsx index 9dfd9db3b3f..43465a4fe88 100644 --- a/src/core/components/info.jsx +++ b/src/core/components/info.jsx @@ -2,7 +2,7 @@ import React from "react" import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" import { sanitizeUrl } from "core/utils" -import { buildUrl } from "core/utils/url" +import { safeBuildUrl } from "core/utils/url" export class InfoBasePath extends React.Component { @@ -35,7 +35,7 @@ class Contact extends React.Component { render(){ let { data, getComponent, selectedServer, url: specUrl} = this.props let name = data.get("name") || "the developer" - let url = buildUrl(data.get("url"), specUrl, {selectedServer}) + let url = safeBuildUrl(data.get("url"), specUrl, {selectedServer}) let email = data.get("email") const Link = getComponent("Link") @@ -66,8 +66,8 @@ class License extends React.Component { let { license, getComponent, selectedServer, url: specUrl } = this.props const Link = getComponent("Link") - let name = license.get("name") || "License" - let url = buildUrl(license.get("url"), specUrl, {selectedServer}) + let name = license.get("name") || "License" + let url = safeBuildUrl(license.get("url"), specUrl, {selectedServer}) return (
@@ -113,11 +113,11 @@ export default class Info extends React.Component { let version = info.get("version") let description = info.get("description") let title = info.get("title") - let termsOfServiceUrl = buildUrl(info.get("termsOfService"), specUrl, {selectedServer}) + let termsOfServiceUrl = safeBuildUrl(info.get("termsOfService"), specUrl, {selectedServer}) let contact = info.get("contact") let license = info.get("license") let rawExternalDocsUrl = externalDocs && externalDocs.get("url") - let externalDocsUrl = buildUrl(rawExternalDocsUrl, specUrl, {selectedServer}) + let externalDocsUrl = safeBuildUrl(rawExternalDocsUrl, specUrl, {selectedServer}) let externalDocsDescription = externalDocs && externalDocs.get("description") const Markdown = getComponent("Markdown", true) diff --git a/src/core/components/operation-tag.jsx b/src/core/components/operation-tag.jsx index da2edb122ad..8dbc39a5bc6 100644 --- a/src/core/components/operation-tag.jsx +++ b/src/core/components/operation-tag.jsx @@ -3,7 +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" +import { safeBuildUrl } from "core/utils/url" import { isFunc } from "core/utils" export default class OperationTag extends React.Component { @@ -59,7 +59,7 @@ export default class OperationTag extends React.Component { let rawTagExternalDocsUrl = tagObj.getIn(["tagDetails", "externalDocs", "url"]) let tagExternalDocsUrl if (isFunc(oas3Selectors) && isFunc(oas3Selectors.selectedServer)) { - tagExternalDocsUrl = buildUrl( rawTagExternalDocsUrl, specUrl, { selectedServer: oas3Selectors.selectedServer() } ) + tagExternalDocsUrl = safeBuildUrl( rawTagExternalDocsUrl, specUrl, { selectedServer: oas3Selectors.selectedServer() } ) } else { tagExternalDocsUrl = rawTagExternalDocsUrl } diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index f5a799d9a38..d1ce910f9cf 100644 --- a/src/core/components/operation.jsx +++ b/src/core/components/operation.jsx @@ -2,7 +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 { safeBuildUrl } from "core/utils/url" import { Iterable, List } from "immutable" import ImPropTypes from "react-immutable-proptypes" @@ -82,7 +82,7 @@ export default class Operation extends PureComponent { schemes } = op - const externalDocsUrl = externalDocs ? buildUrl(externalDocs.url, specSelectors.url(), { selectedServer: oas3Selectors.selectedServer() }) : "" + const externalDocsUrl = externalDocs ? safeBuildUrl(externalDocs.url, specSelectors.url(), { selectedServer: oas3Selectors.selectedServer() }) : "" let operation = operationProps.getIn(["op"]) let responses = operation.get("responses") let parameters = getList(operation, ["parameters"]) diff --git a/src/core/utils/url.js b/src/core/utils/url.js index e7237d3216d..d721ca7b4e6 100644 --- a/src/core/utils/url.js +++ b/src/core/utils/url.js @@ -12,11 +12,11 @@ export function buildBaseUrl(selectedServer, specUrl) { if (!selectedServer) return specUrl if (isAbsoluteUrl(selectedServer)) return addProtocol(selectedServer) - return new URL(selectedServer, specUrl).href + return new URL(selectedServer, specUrl).href } export function buildUrl(url, specUrl, { selectedServer="" } = {}) { - if (!url) return + if (!url) return undefined if (isAbsoluteUrl(url)) return url const baseUrl = buildBaseUrl(selectedServer, specUrl) @@ -25,3 +25,15 @@ export function buildUrl(url, specUrl, { selectedServer="" } = {}) { } return new URL(url, baseUrl).href } + +/** + * Safe version of buildUrl function. `selectedServer` can contain server variables + * which can fail the URL resolution. + */ +export function safeBuildUrl(url, specUrl, { selectedServer="" } = {}) { + try { + return buildUrl(url, specUrl, { selectedServer }) + } catch { + return undefined + } +} diff --git a/test/unit/core/utils.js b/test/unit/core/utils.js index a22891e3dcf..42ab9b55b9b 100644 --- a/test/unit/core/utils.js +++ b/test/unit/core/utils.js @@ -1,4 +1,3 @@ - import { Map, fromJS } from "immutable" import { mapToList, @@ -36,6 +35,7 @@ import { isAbsoluteUrl, buildBaseUrl, buildUrl, + safeBuildUrl, } from "core/utils/url" import win from "core/window" @@ -1445,6 +1445,7 @@ describe("utils", () => { const absoluteServerUrl = "https://server-example.com/base-path/path" const serverUrlRelativeToBase = "server-example/base-path/path" const serverUrlRelativeToHost = "/server-example/base-path/path" + const serverUrlWithVariables = "https://api.example.com:{port}/{basePath}" const specUrlAsInvalidUrl = "./examples/test.yaml" const specUrlOas2NonUrlString = "an allowed OAS2 TermsOfService description string" @@ -1488,6 +1489,85 @@ describe("utils", () => { it("build relative url when no servers defined AND specUrl is OAS2 non-url string", () => { expect(buildUrl(urlRelativeToHost, specUrlOas2NonUrlString, { selectedServer: noServerSelected })).toBe("http://localhost/relative-url/base-path/path") }) + + it("throws error when server url contains non-transcluded server variables", () => { + const buildUrlThunk = () => buildUrl(urlRelativeToHost, specUrl, { selectedServer: serverUrlWithVariables }) + + expect(buildUrlThunk).toThrow(/^Invalid base URL/) + }) + }) + + describe("safeBuildUrl", () => { + const { location } = window + beforeAll(() => { + delete window.location + window.location = { + href: "http://localhost/", + } + }) + afterAll(() => { + window.location = location + }) + + 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" + const serverUrlWithVariables = "https://api.example.com:{port}/{basePath}" + + const specUrlAsInvalidUrl = "./examples/test.yaml" + const specUrlOas2NonUrlString = "an allowed OAS2 TermsOfService description string" + + it("build no url", () => { + expect(safeBuildUrl(noUrl, specUrl, { selectedServer: absoluteServerUrl })).toBe(undefined) + expect(safeBuildUrl(noUrl, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe(undefined) + expect(safeBuildUrl(noUrl, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe(undefined) + }) + + it("build absolute url", () => { + expect(safeBuildUrl(absoluteUrl, specUrl, { selectedServer: absoluteServerUrl })).toBe("https://example.com/base-path/path") + expect(safeBuildUrl(absoluteUrl, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe("https://example.com/base-path/path") + expect(safeBuildUrl(absoluteUrl, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe("https://example.com/base-path/path") + }) + + it("build relative url with no server selected", () => { + expect(safeBuildUrl(urlRelativeToBase, specUrl, { selectedServer: noServerSelected })).toBe("https://petstore.swagger.io/v2/relative-url/base-path/path") + expect(safeBuildUrl(urlRelativeToHost, specUrl, { selectedServer: noServerSelected })).toBe("https://petstore.swagger.io/relative-url/base-path/path") + }) + + it("build relative url with absolute server url", () => { + expect(safeBuildUrl(urlRelativeToBase, specUrl, { selectedServer: absoluteServerUrl })).toBe("https://server-example.com/base-path/relative-url/base-path/path") + expect(safeBuildUrl(urlRelativeToHost, specUrl, { selectedServer: absoluteServerUrl })).toBe("https://server-example.com/relative-url/base-path/path") + }) + + it("build relative url with server url relative to base", () => { + expect(safeBuildUrl(urlRelativeToBase, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe("https://petstore.swagger.io/v2/server-example/base-path/relative-url/base-path/path") + expect(safeBuildUrl(urlRelativeToHost, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe("https://petstore.swagger.io/relative-url/base-path/path") + }) + + it("build relative url with server url relative to host", () => { + expect(safeBuildUrl(urlRelativeToBase, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe("https://petstore.swagger.io/server-example/base-path/relative-url/base-path/path") + expect(safeBuildUrl(urlRelativeToHost, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe("https://petstore.swagger.io/relative-url/base-path/path") + }) + + it("build relative url when no servers defined AND specUrl is invalid Url", () => { + expect(safeBuildUrl(urlRelativeToHost, specUrlAsInvalidUrl, { selectedServer: noServerSelected })).toBe("http://localhost/relative-url/base-path/path") + }) + + it("build relative url when no servers defined AND specUrl is OAS2 non-url string", () => { + expect(safeBuildUrl(urlRelativeToHost, specUrlOas2NonUrlString, { selectedServer: noServerSelected })).toBe("http://localhost/relative-url/base-path/path") + }) + + it("build no url when server url contains non-transcluded server variables", () => { + expect(safeBuildUrl(urlRelativeToHost, specUrl, { selectedServer: serverUrlWithVariables })).toBe(undefined) + }) }) describe("requiresValidationURL", () => {