diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2019-03-27 13:19:14 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2019-03-29 09:44:41 +0100 |
commit | 2beaf73c2d10dcaaf3949889af53579e7d5aba13 (patch) | |
tree | a2f61cae2f94ba9b33b3be1392c1027e5d0b5d80 /server | |
parent | 8e0777254fb78aaba0e1c0645ad945da1c2095f5 (diff) | |
download | sonarqube-2beaf73c2d10dcaaf3949889af53579e7d5aba13.tar.gz sonarqube-2beaf73c2d10dcaaf3949889af53579e7d5aba13.zip |
SONAR-11867, SSF-74 Fix XSS in project links on account/projects
Diffstat (limited to 'server')
8 files changed, 110 insertions, 69 deletions
diff --git a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx index b2cef470d7a..879d5c2b522 100644 --- a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx +++ b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx @@ -18,14 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { sortBy } from 'lodash'; import { Link } from 'react-router'; import DateFromNow from '../../../components/intl/DateFromNow'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import Level from '../../../components/ui/Level'; -import ProjectLinkIcon from '../../../components/icons-components/ProjectLinkIcon'; import Tooltip from '../../../components/controls/Tooltip'; +import MetaLink from '../../overview/meta/MetaLink'; +import { orderLinks } from '../../projectLinks/utils'; import { translateWithParameters, translate } from '../../../helpers/l10n'; interface Props { @@ -33,7 +33,20 @@ interface Props { } export default function ProjectCard({ project }: Props) { - const links = sortBy(project.links, 'type'); + const { links } = project; + + const orderedLinks: T.ProjectLink[] = orderLinks( + links.map((link, i) => { + const { href, name, type } = link; + return { + id: `link-${i}`, + name, + type, + url: href + }; + }) + ); + const { lastAnalysisDate } = project; return ( @@ -72,20 +85,11 @@ export default function ProjectCard({ project }: Props) { <Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link> </h3> - {links.length > 0 && ( + {orderedLinks.length > 0 && ( <div className="account-project-links"> <ul className="list-inline"> - {links.map(link => ( - <li key={link.type}> - <a - className="link-with-icon" - href={link.href} - rel="nofollow" - target="_blank" - title={link.name}> - <ProjectLinkIcon type={link.type} /> - </a> - </li> + {orderedLinks.map(link => ( + <MetaLink iconOnly={true} key={link.id} link={link} /> ))} </ul> </div> diff --git a/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.tsx b/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.tsx index 97114637371..e74288226c1 100644 --- a/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.tsx +++ b/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.tsx @@ -19,68 +19,60 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import { Link } from 'react-router'; import ProjectCard from '../ProjectCard'; -import Level from '../../../../components/ui/Level'; - -const BASE = { key: 'key', links: [], name: 'name' }; it('should render key and name', () => { - const project = { ...BASE }; - const output = shallow(<ProjectCard project={project} />); - expect(output.find('.account-project-key').text()).toBe('key'); + const wrapper = shallowRender(); + expect(wrapper.find('.account-project-key').text()).toBe('key'); expect( - output + wrapper .find('.account-project-name') - .find(Link) + .find('Link') .prop('children') ).toBe('name'); }); it('should render description', () => { - const project = { ...BASE, description: 'bla' }; - const output = shallow(<ProjectCard project={project} />); - expect(output.find('.account-project-description').text()).toBe('bla'); + const wrapper = shallowRender({ description: 'bla' }); + expect(wrapper.find('.account-project-description').text()).toBe('bla'); }); it('should not render optional fields', () => { - const project = { ...BASE }; - const output = shallow(<ProjectCard project={project} />); - expect(output.find('.account-project-description').length).toBe(0); - expect(output.find('.account-project-quality-gate').length).toBe(0); - expect(output.find('.account-project-links').length).toBe(0); + const wrapper = shallowRender(); + expect(wrapper.find('.account-project-description').length).toBe(0); + expect(wrapper.find('.account-project-quality-gate').length).toBe(0); + expect(wrapper.find('.account-project-links').length).toBe(0); }); it('should render analysis date', () => { - const project = { ...BASE, lastAnalysisDate: '2016-05-17' }; - const output = shallow(<ProjectCard project={project} />); - expect(output.find('.account-project-analysis DateFromNow')).toHaveLength(1); + const wrapper = shallowRender({ lastAnalysisDate: '2016-05-17' }); + expect(wrapper.find('.account-project-analysis DateFromNow')).toHaveLength(1); }); it('should not render analysis date', () => { - const project = { ...BASE }; - const output = shallow(<ProjectCard project={project} />); - expect(output.find('.account-project-analysis').text()).toContain( + const wrapper = shallowRender(); + expect(wrapper.find('.account-project-analysis').text()).toContain( 'my_account.projects.never_analyzed' ); }); it('should render quality gate status', () => { - const project = { ...BASE, qualityGate: 'ERROR' }; - const output = shallow(<ProjectCard project={project} />); + const wrapper = shallowRender({ qualityGate: 'ERROR' }); expect( - output + wrapper .find('.account-project-quality-gate') - .find(Level) + .find('Level') .prop('level') ).toBe('ERROR'); }); it('should render links', () => { - const project = { - ...BASE, - links: [{ name: 'n', type: 't', href: 'h' }] - }; - const output = shallow(<ProjectCard project={project} />); - expect(output.find('.account-project-links').find('li').length).toBe(1); + const wrapper = shallowRender({ + links: [{ name: 'name', type: 'type', href: 'href' }] + }); + expect(wrapper.find('MetaLink').length).toBe(1); }); + +function shallowRender(project: Partial<T.MyProject> = {}) { + return shallow(<ProjectCard project={{ key: 'key', links: [], name: 'name', ...project }} />); +} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.css b/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.css new file mode 100644 index 00000000000..132735649ba --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.css @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +.copy-paste-link .overview-key { + width: 80%; +} + +.copy-paste-link .close { + color: #000; + border-bottom: 0; + height: 100%; + display: inline-block; + margin-left: 5px; + box-sizing: border-box; +} + +.copy-paste-link .close svg { + vertical-align: sub; +} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx index 95b4609c6d0..ad4e6965729 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx @@ -22,8 +22,10 @@ import { getLinkName } from '../../projectLinks/utils'; import ProjectLinkIcon from '../../../components/icons-components/ProjectLinkIcon'; import isValidUri from '../../../app/utils/isValidUri'; import ClearIcon from '../../../components/icons-components/ClearIcon'; +import './MetaLink.css'; interface Props { + iconOnly?: boolean; link: T.ProjectLink; } @@ -42,7 +44,8 @@ export default class MetaLink extends React.PureComponent<Props, State> { }; render() { - const { link } = this.props; + const { iconOnly, link } = this.props; + const linkTitle = getLinkName(link); return ( <li> <a @@ -50,9 +53,10 @@ export default class MetaLink extends React.PureComponent<Props, State> { href={link.url} onClick={!isValidUri(link.url) ? this.handleClick : undefined} rel="nofollow noreferrer noopener" - target="_blank"> + target="_blank" + title={linkTitle}> <ProjectLinkIcon className="little-spacer-right" type={link.type} /> - {getLinkName(link)} + {!iconOnly && linkTitle} </a> {this.state.expanded && ( <div className="little-spacer-top copy-paste-link"> diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx index 13f5584c9f1..bff5b122cb8 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx @@ -31,6 +31,7 @@ it('should match snapshot', () => { }; expect(shallow(<MetaLink link={link} />)).toMatchSnapshot(); + expect(shallow(<MetaLink iconOnly={true} link={link} />)).toMatchSnapshot(); }); it('should render dangerous links as plaintext', () => { diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap index a8f7dbb5bba..256301c711f 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap @@ -7,6 +7,7 @@ exports[`should expand and collapse link 1`] = ` href="scm:git:git@github.com" rel="nofollow noreferrer noopener" target="_blank" + title="Foo" > <ProjectLinkIcon className="little-spacer-right" @@ -24,6 +25,7 @@ exports[`should expand and collapse link 2`] = ` href="scm:git:git@github.com" rel="nofollow noreferrer noopener" target="_blank" + title="Foo" > <ProjectLinkIcon className="little-spacer-right" @@ -41,6 +43,7 @@ exports[`should expand and collapse link 3`] = ` href="scm:git:git@github.com" rel="nofollow noreferrer noopener" target="_blank" + title="Foo" > <ProjectLinkIcon className="little-spacer-right" @@ -58,6 +61,7 @@ exports[`should match snapshot 1`] = ` href="http://example.com" rel="nofollow noreferrer noopener" target="_blank" + title="Foo" > <ProjectLinkIcon className="little-spacer-right" @@ -68,6 +72,23 @@ exports[`should match snapshot 1`] = ` </li> `; +exports[`should match snapshot 2`] = ` +<li> + <a + className="link-with-icon" + href="http://example.com" + rel="nofollow noreferrer noopener" + target="_blank" + title="Foo" + > + <ProjectLinkIcon + className="little-spacer-right" + type="foo" + /> + </a> +</li> +`; + exports[`should render dangerous links as plaintext 1`] = ` <li> <a @@ -76,6 +97,7 @@ exports[`should render dangerous links as plaintext 1`] = ` onClick={[Function]} rel="nofollow noreferrer noopener" target="_blank" + title="Dangerous" > <ProjectLinkIcon className="little-spacer-right" diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css index 4ce5c0b5c70..3e552f55f3c 100644 --- a/server/sonar-web/src/main/js/apps/overview/styles.css +++ b/server/sonar-web/src/main/js/apps/overview/styles.css @@ -474,23 +474,6 @@ background-color: transparent !important; } -.copy-paste-link .overview-key { - width: 90%; -} - -.copy-paste-link .close { - color: #000; - border-bottom: 0; - height: 100%; - display: inline-block; - margin-left: 5px; - box-sizing: border-box; -} - -.copy-paste-link .close svg { - vertical-align: sub; -} - .overview-deleted-profile, .overview-deprecated-rules { margin: 4px -6px 4px; diff --git a/server/sonar-web/src/main/js/apps/projectLinks/utils.ts b/server/sonar-web/src/main/js/apps/projectLinks/utils.ts index 9c09e1339cd..e2587c7efd5 100644 --- a/server/sonar-web/src/main/js/apps/projectLinks/utils.ts +++ b/server/sonar-web/src/main/js/apps/projectLinks/utils.ts @@ -31,7 +31,7 @@ export function orderLinks<T extends NameAndType>(links: T[]) { const [provided, unknown] = partition<T>(links, isProvided); return [ ...sortBy(provided, link => PROVIDED_TYPES.indexOf(link.type)), - ...sortBy(unknown, link => link.name!.toLowerCase()) + ...sortBy(unknown, link => link.name && link.name.toLowerCase()) ]; } |