Merge branch 'master' into bearer-dialog-fix

This commit is contained in:
kyle
2017-11-02 16:58:36 -07:00
committed by GitHub
21 changed files with 256 additions and 191 deletions

View File

@@ -1,6 +1,9 @@
<!--- <!---
Thanks for filing an issue 😄 ! Before you submit, please read the following: Thanks for filing an issue 😄 ! Before you submit, please read the following:
If you're here to report a security issue, please STOP writing an issue and contact us
at security@swagger.io instead!
Search open/closed issues before submitting since someone might have asked the same thing before! Search open/closed issues before submitting since someone might have asked the same thing before!
Issues on GitHub are only related to problems of Swagger-UI itself. We'll try to offer support Issues on GitHub are only related to problems of Swagger-UI itself. We'll try to offer support

View File

@@ -22,7 +22,7 @@ The OpenAPI Specification has undergone 5 revisions since initial creation in 20
Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes
------------------ | ------------ | -------------------------- | ----- ------------------ | ------------ | -------------------------- | -----
3.4.1 | 2017-10-20 | 2.0, 3.0 | [tag v3.4.1](https://github.com/swagger-api/swagger-ui/tree/v3.4.1) 3.4.2 | 2017-10-30 | 2.0, 3.0 | [tag v3.4.2](https://github.com/swagger-api/swagger-ui/tree/v3.4.2)
3.0.21 | 2017-07-26 | 2.0 | [tag v3.0.21](https://github.com/swagger-api/swagger-ui/tree/v3.0.21) 3.0.21 | 2017-07-26 | 2.0 | [tag v3.0.21](https://github.com/swagger-api/swagger-ui/tree/v3.0.21)
2.2.10 | 2017-01-04 | 1.1, 1.2, 2.0 | [tag v2.2.10](https://github.com/swagger-api/swagger-ui/tree/v2.2.10) 2.2.10 | 2017-01-04 | 1.1, 1.2, 2.0 | [tag v2.2.10](https://github.com/swagger-api/swagger-ui/tree/v2.2.10)
2.1.5 | 2016-07-20 | 1.1, 1.2, 2.0 | [tag v2.1.5](https://github.com/swagger-api/swagger-ui/tree/v2.1.5) 2.1.5 | 2016-07-20 | 1.1, 1.2, 2.0 | [tag v2.1.5](https://github.com/swagger-api/swagger-ui/tree/v2.1.5)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/swagger-ui.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -20,8 +20,8 @@ Some distinct identifiers to Swagger-UI 3.X:
If you've determined this is the version you have, to find the exact version: If you've determined this is the version you have, to find the exact version:
- Open your browser's web console (changes between browsers) - Open your browser's web console (changes between browsers)
- Type `versions` in the console and execute the call. - Type `JSON.stringify(versions)` in the console and execute the call.
- You might need to expand the result, until you get a string similar to `swaggerUi : Object { version: "3.1.6", gitRevision: "g786cd47", gitDirty: true, … }`. - The result should look similar to `swaggerUi : Object { version: "3.1.6", gitRevision: "g786cd47", gitDirty: true, … }`.
- The version taken from that example would be `3.1.6`. - The version taken from that example would be `3.1.6`.
Note: This functionality was added in 3.0.8. If you're unable to execute it, you're likely to use an older version, and in that case the first step would be to upgrade. Note: This functionality was added in 3.0.8. If you're unable to execute it, you're likely to use an older version, and in that case the first step would be to upgrade.
@@ -51,4 +51,4 @@ If you've determined this is the version you have, to find the exact version:
* @link http://swagger.io * @link http://swagger.io
* @license Apache-2.0 * @license Apache-2.0
*/ */
``` ```

View File

@@ -1,6 +1,6 @@
{ {
"name": "swagger-ui", "name": "swagger-ui",
"version": "3.4.1", "version": "3.4.2",
"main": "dist/swagger-ui.js", "main": "dist/swagger-ui.js",
"repository": "git@github.com:swagger-api/swagger-ui.git", "repository": "git@github.com:swagger-api/swagger-ui.git",
"contributors": [ "contributors": [
@@ -39,6 +39,7 @@
"e2e": "npm-run-all --parallel -r hot-server mock-api test-e2e" "e2e": "npm-run-all --parallel -r hot-server mock-api test-e2e"
}, },
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^2.0.2",
"base64-js": "^1.2.0", "base64-js": "^1.2.0",
"brace": "0.7.0", "brace": "0.7.0",
"classnames": "^2.2.5", "classnames": "^2.2.5",
@@ -55,12 +56,12 @@
"memoizee": "0.4.1", "memoizee": "0.4.1",
"promise-worker": "^1.1.1", "promise-worker": "^1.1.1",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"react": "^15.4.0", "react": "^15.6.2",
"react-addons-perf": "^15.4.0", "react-addons-perf": "^15.4.0",
"react-addons-shallow-compare": "0.14.8", "react-addons-shallow-compare": "0.14.8",
"react-addons-test-utils": "^15.4.0", "react-addons-test-utils": "^15.6.2",
"react-collapse": "2.3.1", "react-collapse": "2.3.1",
"react-dom": "^15.4.0", "react-dom": "^15.6.2",
"react-height": "^2.0.0", "react-height": "^2.0.0",
"react-hot-loader": "1.3.1", "react-hot-loader": "1.3.1",
"react-immutable-proptypes": "2.1.0", "react-immutable-proptypes": "2.1.0",
@@ -83,6 +84,7 @@
"whatwg-fetch": "0.11.1", "whatwg-fetch": "0.11.1",
"worker-loader": "^0.7.1", "worker-loader": "^0.7.1",
"xml": "1.0.1", "xml": "1.0.1",
"xml-but-prettier": "^1.0.1",
"yaml-js": "0.2.0" "yaml-js": "0.2.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -2,6 +2,7 @@ import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import { fromJS } from "immutable" import { fromJS } from "immutable"
import ImPropTypes from "react-immutable-proptypes" import ImPropTypes from "react-immutable-proptypes"
import { sanitizeUrl } from "core/utils"
class Path extends React.Component { class Path extends React.Component {
@@ -35,9 +36,9 @@ class Contact extends React.Component {
return ( return (
<div> <div>
{ url && <div><a href={ url } target="_blank">{ name } - Website</a></div> } { url && <div><a href={ sanitizeUrl(url) } target="_blank">{ name } - Website</a></div> }
{ email && { email &&
<a href={`mailto:${email}`}> <a href={sanitizeUrl(`mailto:${email}`)}>
{ url ? `Send email to ${name}` : `Contact ${name}`} { url ? `Send email to ${name}` : `Contact ${name}`}
</a> </a>
} }
@@ -59,7 +60,7 @@ class License extends React.Component {
return ( return (
<div> <div>
{ {
url ? <a target="_blank" href={ url }>{ name }</a> url ? <a target="_blank" href={ sanitizeUrl(url) }>{ name }</a>
: <span>{ name }</span> : <span>{ name }</span>
} }
</div> </div>
@@ -97,7 +98,7 @@ export default class Info extends React.Component {
{ version && <VersionStamp version={version}></VersionStamp> } { version && <VersionStamp version={version}></VersionStamp> }
</h2> </h2>
{ host || basePath ? <Path host={ host } basePath={ basePath } /> : null } { host || basePath ? <Path host={ host } basePath={ basePath } /> : null }
{ url && <a target="_blank" href={ url }><span className="url"> { url } </span></a> } { url && <a target="_blank" href={ sanitizeUrl(url) }><span className="url"> { url } </span></a> }
</hgroup> </hgroup>
<div className="description"> <div className="description">
@@ -106,14 +107,14 @@ export default class Info extends React.Component {
{ {
termsOfService && <div> termsOfService && <div>
<a target="_blank" href={ termsOfService }>Terms of service</a> <a target="_blank" href={ sanitizeUrl(termsOfService) }>Terms of service</a>
</div> </div>
} }
{ contact && contact.size ? <Contact data={ contact } /> : null } { contact && contact.size ? <Contact data={ contact } /> : null }
{ license && license.size ? <License license={ license } /> : null } { license && license.size ? <License license={ license } /> : null }
{ externalDocsUrl ? { externalDocsUrl ?
<a target="_blank" href={externalDocsUrl}>{externalDocsDescription || externalDocsUrl}</a> <a target="_blank" href={sanitizeUrl(externalDocsUrl)}>{externalDocsDescription || externalDocsUrl}</a>
: null } : null }
</div> </div>

View File

@@ -1,5 +1,6 @@
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import { sanitizeUrl } from "core/utils"
export default class OnlineValidatorBadge extends React.Component { export default class OnlineValidatorBadge extends React.Component {
static propTypes = { static propTypes = {
@@ -32,6 +33,8 @@ export default class OnlineValidatorBadge extends React.Component {
let { getConfigs } = this.props let { getConfigs } = this.props
let { spec } = getConfigs() let { spec } = getConfigs()
let sanitizedValidatorUrl = sanitizeUrl(this.state.validatorUrl)
if ( typeof spec === "object" && Object.keys(spec).length) return null if ( typeof spec === "object" && Object.keys(spec).length) return null
if (!this.state.url || !this.state.validatorUrl || this.state.url.indexOf("localhost") >= 0 if (!this.state.url || !this.state.validatorUrl || this.state.url.indexOf("localhost") >= 0
@@ -40,8 +43,8 @@ export default class OnlineValidatorBadge extends React.Component {
} }
return (<span style={{ float: "right"}}> return (<span style={{ float: "right"}}>
<a target="_blank" href={`${ this.state.validatorUrl }/debug?url=${ this.state.url }`}> <a target="_blank" href={`${ sanitizedValidatorUrl }/debug?url=${ this.state.url }`}>
<ValidatorImage src={`${ this.state.validatorUrl }?url=${ this.state.url }`} alt="Online validator badge"/> <ValidatorImage src={`${ sanitizedValidatorUrl }?url=${ this.state.url }`} alt="Online validator badge"/>
</a> </a>
</span>) </span>)
} }

View File

@@ -2,6 +2,7 @@ import React, { PureComponent } from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import { getList } from "core/utils" import { getList } from "core/utils"
import * as CustomPropTypes from "core/proptypes" import * as CustomPropTypes from "core/proptypes"
import { sanitizeUrl } from "core/utils"
//import "less/opblock" //import "less/opblock"
@@ -206,7 +207,7 @@ export default class Operation extends PureComponent {
<span className="opblock-external-docs__description"> <span className="opblock-external-docs__description">
<Markdown source={ externalDocs.get("description") } /> <Markdown source={ externalDocs.get("description") } />
</span> </span>
<a className="opblock-external-docs__link" href={ externalDocs.get("url") }>{ externalDocs.get("url") }</a> <a className="opblock-external-docs__link" href={ sanitizeUrl(externalDocs.get("url")) }>{ externalDocs.get("url") }</a>
</div> </div>
</div> : null </div> : null
} }

View File

@@ -1,7 +1,7 @@
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import { helpers } from "swagger-client" import { helpers } from "swagger-client"
import { createDeepLinkPath } from "core/utils" import { createDeepLinkPath, sanitizeUrl } from "core/utils"
const { opId } = helpers const { opId } = helpers
export default class Operations extends React.Component { export default class Operations extends React.Component {
@@ -101,7 +101,7 @@ export default class Operations extends React.Component {
{ tagExternalDocsUrl ? ": " : null } { tagExternalDocsUrl ? ": " : null }
{ tagExternalDocsUrl ? { tagExternalDocsUrl ?
<a <a
href={tagExternalDocsUrl} href={sanitizeUrl(tagExternalDocsUrl)}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
target={"_blank"} target={"_blank"}
>{tagExternalDocsUrl}</a> : null >{tagExternalDocsUrl}</a> : null

View File

@@ -1,6 +1,6 @@
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import { formatXml } from "core/utils" import formatXml from "xml-but-prettier"
import lowerCase from "lodash/lowerCase" import lowerCase from "lodash/lowerCase"
export default class ResponseBody extends React.Component { export default class ResponseBody extends React.Component {
@@ -31,7 +31,9 @@ export default class ResponseBody extends React.Component {
// XML // XML
} else if (/xml/i.test(contentType)) { } else if (/xml/i.test(contentType)) {
body = formatXml(content) body = formatXml(content, {
textNodesOnSameLine: true
})
bodyEl = <HighlightCode value={ body } /> bodyEl = <HighlightCode value={ body } />
// HTML or Plain Text // HTML or Plain Text

View File

@@ -58,13 +58,12 @@ module.exports = function SwaggerUI(opts) {
plugins: [ plugins: [
], ],
// Initial state
initialState: { },
// Inline Plugin // Inline Plugin
fn: { }, fn: { },
components: { }, components: { },
state: { },
// Override some core configs... at your own risk
store: { },
} }
let queryConfig = parseSearch() let queryConfig = parseSearch()
@@ -74,12 +73,12 @@ module.exports = function SwaggerUI(opts) {
const constructorConfig = deepExtend({}, defaults, opts, queryConfig) const constructorConfig = deepExtend({}, defaults, opts, queryConfig)
const storeConfigs = deepExtend({}, constructorConfig.store, { const storeConfigs = {
system: { system: {
configs: constructorConfig.configs configs: constructorConfig.configs
}, },
plugins: constructorConfig.presets, plugins: constructorConfig.presets,
state: { state: deepExtend({
layout: { layout: {
layout: constructorConfig.layout, layout: constructorConfig.layout,
filter: constructorConfig.filter filter: constructorConfig.filter
@@ -88,8 +87,8 @@ module.exports = function SwaggerUI(opts) {
spec: "", spec: "",
url: constructorConfig.url url: constructorConfig.url
} }
} }, constructorConfig.initialState)
}) }
let inlinePlugin = ()=> { let inlinePlugin = ()=> {
return { return {

View File

@@ -10,6 +10,7 @@ import auth from "core/plugins/auth"
import util from "core/plugins/util" import util from "core/plugins/util"
import SplitPaneModePlugin from "core/plugins/split-pane-mode" import SplitPaneModePlugin from "core/plugins/split-pane-mode"
import downloadUrlPlugin from "core/plugins/download-url" import downloadUrlPlugin from "core/plugins/download-url"
import configsPlugin from "plugins/configs"
import deepLinkingPlugin from "core/plugins/deep-linking" import deepLinkingPlugin from "core/plugins/deep-linking"
import App from "core/components/app" import App from "core/components/app"
@@ -122,6 +123,7 @@ export default function() {
} }
return [ return [
configsPlugin,
util, util,
logs, logs,
view, view,

View File

@@ -1,5 +1,5 @@
import Im from "immutable" import Im from "immutable"
import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url"
import camelCase from "lodash/camelCase" import camelCase from "lodash/camelCase"
import upperFirst from "lodash/upperFirst" import upperFirst from "lodash/upperFirst"
import _memoize from "lodash/memoize" import _memoize from "lodash/memoize"
@@ -155,83 +155,6 @@ export function getList(iterable, keys) {
return Im.List.isList(val) ? val : Im.List() return Im.List.isList(val) ? val : Im.List()
} }
// Adapted from http://stackoverflow.com/a/2893259/454004
// Note: directly ported from CoffeeScript
export function formatXml (xml) {
var contexp, fn, formatted, indent, l, lastType, len, lines, ln, reg, transitions, wsexp
reg = /(>)(<)(\/*)/g
wsexp = /[ ]*(.*)[ ]+\n/g
contexp = /(<.+>)(.+\n)/g
xml = xml.replace(/\r\n/g, "\n").replace(reg, "$1\n$2$3").replace(wsexp, "$1\n").replace(contexp, "$1\n$2")
formatted = ""
lines = xml.split("\n")
indent = 0
lastType = "other"
transitions = {
"single->single": 0,
"single->closing": -1,
"single->opening": 0,
"single->other": 0,
"closing->single": 0,
"closing->closing": -1,
"closing->opening": 0,
"closing->other": 0,
"opening->single": 1,
"opening->closing": 0,
"opening->opening": 1,
"opening->other": 1,
"other->single": 0,
"other->closing": -1,
"other->opening": 0,
"other->other": 0
}
fn = function(ln) {
var fromTo, key, padding, type, types, value
types = {
single: Boolean(ln.match(/<.+\/>/)),
closing: Boolean(ln.match(/<\/.+>/)),
opening: Boolean(ln.match(/<[^!?].*>/))
}
type = ((function() {
var results
results = []
for (key in types) {
value = types[key]
if (value) {
results.push(key)
}
}
return results
})())[0]
type = type === void 0 ? "other" : type
fromTo = lastType + "->" + type
lastType = type
padding = ""
indent += transitions[fromTo]
padding = ((function() {
/* eslint-disable no-unused-vars */
var m, ref1, results, j
results = []
for (j = m = 0, ref1 = indent; 0 <= ref1 ? m < ref1 : m > ref1; j = 0 <= ref1 ? ++m : --m) {
results.push(" ")
}
/* eslint-enable no-unused-vars */
return results
})()).join("")
if (fromTo === "opening->closing") {
formatted = formatted.substr(0, formatted.length - 1) + ln + "\n"
} else {
formatted += padding + ln + "\n"
}
}
for (l = 0, len = lines.length; l < len; l++) {
ln = lines[l]
fn(ln)
}
return formatted
}
/** /**
* Adapted from http://github.com/asvd/microlight * Adapted from http://github.com/asvd/microlight
* @copyright 2016 asvd <heliosframework@gmail.com> * @copyright 2016 asvd <heliosframework@gmail.com>
@@ -722,6 +645,14 @@ export const shallowEqualKeys = (a,b, keys) => {
}) })
} }
export function sanitizeUrl(url) {
if(typeof url !== "string" || url === "") {
return ""
}
return braintreeSanitizeUrl(url)
}
export function getAcceptControllingResponse(responses) { export function getAcceptControllingResponse(responses) {
if(!Im.OrderedMap.isOrderedMap(responses)) { if(!Im.OrderedMap.isOrderedMap(responses)) {
// wrong type! // wrong type!

View File

@@ -0,0 +1,20 @@
export const UPDATE_CONFIGS = "configs_update"
export const TOGGLE_CONFIGS = "configs_toggle"
// Update the configs, with a merge ( not deep )
export function update(configName, configValue) {
return {
type: UPDATE_CONFIGS,
payload: {
[configName]: configValue
},
}
}
// Toggle's the config, by name
export function toggle(configName) {
return {
type: TOGGLE_CONFIGS,
payload: configName,
}
}

View File

@@ -1,56 +1,66 @@
import YAML from "js-yaml" import YAML from "js-yaml"
import yamlConfig from "../../../swagger-config.yaml" import yamlConfig from "../../../swagger-config.yaml"
import * as actions from "./actions"
import * as selectors from "./selectors"
import reducers from "./reducers"
const parseYamlConfig = (yaml, system) => { const parseYamlConfig = (yaml, system) => {
try { try {
return YAML.safeLoad(yaml) return YAML.safeLoad(yaml)
} catch(e) { } catch(e) {
if (system) { if (system) {
system.errActions.newThrownErr( new Error(e) ) system.errActions.newThrownErr( new Error(e) )
}
return {}
} }
return {}
}
} }
export default function configPlugin (toolbox) { const specActions = {
let { fn } = toolbox downloadConfig: (url) => ({fn}) => {
let {fetch} = fn
const actions = { return fetch(url)
downloadConfig: (url) => () => { },
let {fetch} = fn
return fetch(url)
},
getConfigByUrl: (configUrl, cb)=> ({ specActions }) => {
if (configUrl) {
return specActions.downloadConfig(configUrl).then(next, next)
}
function next(res) {
if (res instanceof Error || res.status >= 400) {
specActions.updateLoadingStatus("failedConfig")
specActions.updateLoadingStatus("failedConfig")
specActions.updateUrl("")
console.error(res.statusText + " " + configUrl)
cb(null)
} else {
cb(parseYamlConfig(res.text))
}
}
}
getConfigByUrl: (configUrl, cb)=> ({ specActions }) => {
if (configUrl) {
return specActions.downloadConfig(configUrl).then(next, next)
} }
const selectors = { function next(res) {
getLocalConfig: () => { if (res instanceof Error || res.status >= 400) {
return parseYamlConfig(yamlConfig) specActions.updateLoadingStatus("failedConfig")
} specActions.updateLoadingStatus("failedConfig")
} specActions.updateUrl("")
console.error(res.statusText + " " + configUrl)
return { cb(null)
statePlugins: { } else {
spec: { actions, selectors } cb(parseYamlConfig(res.text))
} }
} }
}
}
const specSelectors = {
getLocalConfig: () => {
return parseYamlConfig(yamlConfig)
}
}
export default function configsPlugin() {
return {
statePlugins: {
spec: {
actions: specActions,
selectors: specSelectors,
},
configs: {
reducers,
actions,
selectors,
}
}
}
} }

View File

@@ -0,0 +1,20 @@
import { fromJS } from "immutable"
import {
UPDATE_CONFIGS,
TOGGLE_CONFIGS,
} from "./actions"
export default {
[UPDATE_CONFIGS]: (state, action) => {
return state.merge(fromJS(action.payload))
},
[TOGGLE_CONFIGS]: (state, action) => {
const configName = action.payload
const oriVal = state.get(configName)
return state.set(configName, !oriVal)
},
}

View File

@@ -0,0 +1,4 @@
// Just get the config value ( it can possibly be an immutable object)
export const get = (state, path) => {
return state.getIn(Array.isArray(path) ? path : [path])
}

View File

@@ -16,7 +16,8 @@ import {
fromJSOrdered, fromJSOrdered,
getAcceptControllingResponse, getAcceptControllingResponse,
createDeepLinkPath, createDeepLinkPath,
escapeDeepLinkPath escapeDeepLinkPath,
sanitizeUrl
} from "core/utils" } from "core/utils"
import win from "core/window" import win from "core/window"
@@ -885,4 +886,44 @@ describe("utils", function() {
expect(result).toEqual("hello\\#world") expect(result).toEqual("hello\\#world")
}) })
}) })
describe("sanitizeUrl", function() {
it("should sanitize a `javascript:` url", function() {
const res = sanitizeUrl("javascript:alert('bam!')")
expect(res).toEqual("about:blank")
})
it("should sanitize a `data:` url", function() {
const res = sanitizeUrl(`data:text/html;base64,PHNjcmlwdD5hbGVydCgiSGV
sbG8iKTs8L3NjcmlwdD4=`)
expect(res).toEqual("about:blank")
})
it("should not modify a `http:` url", function() {
const res = sanitizeUrl(`http://swagger.io/`)
expect(res).toEqual("http://swagger.io/")
})
it("should not modify a `https:` url", function() {
const res = sanitizeUrl(`https://swagger.io/`)
expect(res).toEqual("https://swagger.io/")
})
it("should gracefully handle empty strings", function() {
expect(sanitizeUrl("")).toEqual("")
})
it("should gracefully handle non-string values", function() {
expect(sanitizeUrl(123)).toEqual("")
expect(sanitizeUrl(null)).toEqual("")
expect(sanitizeUrl(undefined)).toEqual("")
expect(sanitizeUrl([])).toEqual("")
expect(sanitizeUrl({})).toEqual("")
})
})
}) })