aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2019-03-27 13:19:14 +0100
committersonartech <sonartech@sonarsource.com>2019-03-29 09:44:41 +0100
commit2beaf73c2d10dcaaf3949889af53579e7d5aba13 (patch)
treea2f61cae2f94ba9b33b3be1392c1027e5d0b5d80 /server
parent8e0777254fb78aaba0e1c0645ad945da1c2095f5 (diff)
downloadsonarqube-2beaf73c2d10dcaaf3949889af53579e7d5aba13.tar.gz
sonarqube-2beaf73c2d10dcaaf3949889af53579e7d5aba13.zip
SONAR-11867, SSF-74 Fix XSS in project links on account/projects
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.tsx58
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaLink.css35
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap22
-rw-r--r--server/sonar-web/src/main/js/apps/overview/styles.css17
-rw-r--r--server/sonar-web/src/main/js/apps/projectLinks/utils.ts2
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())
];
}