Browse Source

VSTS-141 Add VSTS Quality widget

tags/7.5
Grégoire Aubert 6 years ago
parent
commit
cb0a23c978
26 changed files with 2025 additions and 245 deletions
  1. 5
    1
      server/sonar-server/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java
  2. 25
    15
      server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java
  3. 45
    12
      server/sonar-server/src/test/java/org/sonar/server/platform/web/SecurityServletFilterTest.java
  4. 32
    87
      server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java
  5. 0
    14
      server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html
  6. 65
    0
      server/sonar-web/config/utils.js
  7. 110
    0
      server/sonar-web/config/vsts.webpack.config.js
  8. 2
    39
      server/sonar-web/config/webpack.config.js
  9. 7
    1
      server/sonar-web/package.json
  10. 26
    0
      server/sonar-web/public/integration/vsts/index.html
  11. 2
    1
      server/sonar-web/scripts/start.js
  12. 97
    0
      server/sonar-web/scripts/vsts.build.js
  13. 113
    0
      server/sonar-web/scripts/vsts.start.js
  14. 1
    1
      server/sonar-web/src/main/js/api/measures.ts
  15. 147
    0
      server/sonar-web/src/main/js/app/integration/vsts/components/Configuration.tsx
  16. 57
    0
      server/sonar-web/src/main/js/app/integration/vsts/components/QGWidget.tsx
  17. 106
    0
      server/sonar-web/src/main/js/app/integration/vsts/components/Widget.tsx
  18. 42
    0
      server/sonar-web/src/main/js/app/integration/vsts/index.js
  19. 89
    0
      server/sonar-web/src/main/js/app/integration/vsts/vsts.css
  20. 82
    0
      server/sonar-web/src/main/js/app/styles/components/spinner.css
  21. 0
    67
      server/sonar-web/src/main/js/app/styles/init/icons.css
  22. 1
    0
      server/sonar-web/src/main/js/app/styles/sonar.css
  23. 957
    0
      server/sonar-web/src/main/js/libs/third-party/VSS.SDK.min.js
  24. 2
    2
      server/sonar-web/yarn.lock
  25. 1
    1
      sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java
  26. 11
    4
      sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java

+ 5
- 1
server/sonar-server/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java View File

@@ -59,7 +59,11 @@ public class SecurityServletFilter implements Filter {

// Clickjacking protection
// See https://www.owasp.org/index.php/Clickjacking_Protection_for_Java_EE
httpResponse.addHeader("X-Frame-Options", "SAMEORIGIN");
// The protection is disabled on purpose for integration in external systems like VSTS (/integration/vsts/index.html).
String path = httpRequest.getRequestURI().replaceFirst(httpRequest.getContextPath(), "");
if (!path.startsWith("/integration/")) {
httpResponse.addHeader("X-Frame-Options", "SAMEORIGIN");
}

// Cross-site scripting
// See https://www.owasp.org/index.php/List_of_useful_HTTP_headers

+ 25
- 15
server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java View File

@@ -19,7 +19,12 @@
*/
package org.sonar.server.platform.web;

import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
@@ -47,15 +52,24 @@ public class WebPagesFilter implements Filter {

private static final String CACHE_CONTROL_HEADER = "Cache-Control";
private static final String CACHE_CONTROL_VALUE = "no-cache, no-store, must-revalidate";

private static final String CONTEXT_PLACEHOLDER = "%WEB_CONTEXT%";
private static final String WEB_CONTEXT_PLACEHOLDER = "%WEB_CONTEXT%";
private static final String DEFAULT_HTML_PATH = "/index.html";
// all the html files to be loaded from disk
private static final Set<String> HTML_PATHS = ImmutableSet.of(DEFAULT_HTML_PATH, "/integration/vsts/index.html");
private static final Map<String, String> HTML_CONTENTS_BY_PATH = new HashMap<>();

private static final ServletFilter.UrlPattern URL_PATTERN = ServletFilter.UrlPattern
.builder()
.excludes(staticResourcePatterns())
.build();

private String indexDotHtml;
@Override
public void init(FilterConfig filterConfig) {
HTML_PATHS.forEach(path -> {
ServletContext servletContext = filterConfig.getServletContext();
HTML_CONTENTS_BY_PATH.put(path, loadHtmlFile(servletContext, path));
});
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
@@ -69,21 +83,17 @@ public class WebPagesFilter implements Filter {
httpServletResponse.setContentType(HTML);
httpServletResponse.setCharacterEncoding(UTF_8.name().toLowerCase(ENGLISH));
httpServletResponse.setHeader(CACHE_CONTROL_HEADER, CACHE_CONTROL_VALUE);
write(indexDotHtml, httpServletResponse.getOutputStream(), UTF_8);
}

@Override
public void init(FilterConfig filterConfig) {
String context = filterConfig.getServletContext().getContextPath();
String indexFile = readIndexFile(filterConfig.getServletContext());
this.indexDotHtml = indexFile.replaceAll(CONTEXT_PLACEHOLDER, context);
String htmlPath = HTML_PATHS.contains(path) ? path : DEFAULT_HTML_PATH;
String htmlContent = requireNonNull(HTML_CONTENTS_BY_PATH.get(htmlPath));
write(htmlContent, httpServletResponse.getOutputStream(), UTF_8);
}

private static String readIndexFile(ServletContext servletContext) {
try {
return IOUtils.toString(requireNonNull(servletContext.getResource("/index.html")), UTF_8);
private static String loadHtmlFile(ServletContext context, String path) {
try (InputStream input = context.getResourceAsStream(path)) {
String template = IOUtils.toString(requireNonNull(input), UTF_8);
return template.replaceAll(WEB_CONTEXT_PLACEHOLDER, context.getContextPath());
} catch (Exception e) {
throw new IllegalStateException("Fail to provide index file", e);
throw new IllegalStateException("Fail to load file " + path, e);
}
}


+ 45
- 12
server/sonar-server/src/test/java/org/sonar/server/platform/web/SecurityServletFilterTest.java View File

@@ -26,21 +26,19 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Test;
import org.sonar.server.platform.web.SecurityServletFilter;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class SecurityServletFilterTest {

SecurityServletFilter underTest = new SecurityServletFilter();
HttpServletResponse response = mock(HttpServletResponse.class);
FilterChain chain = mock(FilterChain.class);
private SecurityServletFilter underTest = new SecurityServletFilter();
private HttpServletResponse response = mock(HttpServletResponse.class);
private FilterChain chain = mock(FilterChain.class);

@Test
public void allow_GET_method() throws IOException, ServletException {
@@ -63,7 +61,7 @@ public class SecurityServletFilterTest {
}

private void assertThatMethodIsAllowed(String httpMethod) throws IOException, ServletException {
HttpServletRequest request = newRequest(httpMethod);
HttpServletRequest request = newRequest(httpMethod, "/");
underTest.doFilter(request, response, chain);
verify(response, never()).setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
verify(chain).doFilter(request, response);
@@ -80,25 +78,60 @@ public class SecurityServletFilterTest {
}

private void assertThatMethodIsDenied(String httpMethod) throws IOException, ServletException {
underTest.doFilter(newRequest(httpMethod), response, chain);
underTest.doFilter(newRequest(httpMethod, "/"), response, chain);
verify(response).setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}

@Test
public void set_secured_headers() throws ServletException, IOException {
public void set_security_headers() throws Exception {
underTest.init(mock(FilterConfig.class));
HttpServletRequest request = newRequest("GET");
HttpServletRequest request = newRequest("GET", "/");

underTest.doFilter(request, response, chain);

verify(response, times(3)).addHeader(startsWith("X-"), anyString());
verify(response).addHeader("X-Frame-Options", "SAMEORIGIN");
verify(response).addHeader("X-XSS-Protection", "1; mode=block");
verify(response).addHeader("X-Content-Type-Options", "nosniff");

underTest.destroy();
}

private HttpServletRequest newRequest(String httpMethod) {
@Test
public void do_not_set_frame_protection_on_integration_resources() throws Exception {
underTest.init(mock(FilterConfig.class));
HttpServletRequest request = newRequest("GET", "/integration/vsts/index.html");

underTest.doFilter(request, response, chain);

verify(response, never()).addHeader(eq("X-Frame-Options"), anyString());
verify(response).addHeader("X-XSS-Protection", "1; mode=block");
verify(response).addHeader("X-Content-Type-Options", "nosniff");

underTest.destroy();
}

@Test
public void do_not_set_frame_protection_on_integration_resources_with_context() throws Exception {
underTest.init(mock(FilterConfig.class));
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getMethod()).thenReturn("GET");
when(request.getRequestURI()).thenReturn("/sonarqube/integration/vsts/index.html");
when(request.getContextPath()).thenReturn("/sonarqube");

underTest.doFilter(request, response, chain);

verify(response, never()).addHeader(eq("X-Frame-Options"), anyString());
verify(response).addHeader("X-XSS-Protection", "1; mode=block");
verify(response).addHeader("X-Content-Type-Options", "nosniff");

underTest.destroy();
}

private static HttpServletRequest newRequest(String httpMethod, String path) {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getMethod()).thenReturn(httpMethod);
when(req.getRequestURI()).thenReturn(path);
when(req.getContextPath()).thenReturn("");
return req;
}
}

+ 32
- 87
server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java View File

@@ -19,8 +19,7 @@
*/
package org.sonar.server.platform.web;

import java.io.IOException;
import java.net.MalformedURLException;
import java.io.InputStream;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
@@ -28,131 +27,77 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.stubbing.Answer;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.RETURNS_MOCKS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

public class WebPagesFilterTest {

private static final String TEST_CONTEXT = "/sonarqube";
@Rule
public ExpectedException expectedException = ExpectedException.none();

private HttpServletRequest request = mock(HttpServletRequest.class);
private HttpServletResponse response = mock(HttpServletResponse.class);
private FilterChain chain = mock(FilterChain.class);
private ServletContext servletContext = mock(ServletContext.class);
private ServletContext servletContext = mock(ServletContext.class, RETURNS_MOCKS);
private FilterConfig filterConfig = mock(FilterConfig.class);
private StringOutputStream outputStream = new StringOutputStream();

private WebPagesFilter underTest = new WebPagesFilter();

@Before
public void setUp() throws Exception {
when(servletContext.getContextPath()).thenReturn(TEST_CONTEXT);
when(servletContext.getResourceAsStream(anyString())).thenAnswer((Answer<InputStream>) invocationOnMock -> {
String path = invocationOnMock.getArgument(0);
return IOUtils.toInputStream("Content of " + path + " with context [%WEB_CONTEXT%]", UTF_8);
});
when(filterConfig.getServletContext()).thenReturn(servletContext);
when(response.getOutputStream()).thenReturn(outputStream);
}

@Test
public void verify_paths() throws Exception {
mockIndexFile();
verifyPathIsHandled("/");
verifyPathIsHandled("/issues");
verifyPathIsHandled("/foo");
}

@Test
public void return_index_file_content() throws Exception {
mockIndexFile();
mockPath("/foo", "");
underTest.init(filterConfig);
underTest.doFilter(request, response, chain);

assertThat(outputStream.toString()).contains("<head>");
verify(response).setContentType("text/html");
verify(response).setCharacterEncoding("utf-8");
verify(response).setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
verify(response).getOutputStream();
verifyNoMoreInteractions(response);
}

@Test
public void return_index_file_content_with_default_web_context() throws Exception {
mockIndexFile();
mockPath("/foo", "");
underTest.init(filterConfig);
underTest.doFilter(request, response, chain);

assertThat(outputStream.toString()).contains("href=\"/sonar.css\"");
assertThat(outputStream.toString()).contains("<script src=\"/sonar.js\"></script>");
assertThat(outputStream.toString()).doesNotContain("%WEB_CONTEXT%");
public void return_base_index_dot_html_if_not_static_resource() throws Exception {
verifyDefaultHtml("/index.html");
verifyDefaultHtml("/foo");
verifyDefaultHtml("/foo.html");
}

@Test
public void return_index_file_content_with_web_context() throws Exception {
mockIndexFile();
mockPath("/foo", "/web");
underTest.init(filterConfig);
underTest.doFilter(request, response, chain);

assertThat(outputStream.toString()).contains("href=\"/web/sonar.css\"");
assertThat(outputStream.toString()).contains("<script src=\"/web/sonar.js\"></script>");
assertThat(outputStream.toString()).doesNotContain("%WEB_CONTEXT%");
public void return_integration_html() throws Exception {
verifyHtml("/integration/vsts/index.html", "Content of /integration/vsts/index.html with context [" + TEST_CONTEXT + "]");
}

@Test
public void fail_when_index_is_not_found() throws Exception {
mockPath("/foo", "");
when(servletContext.getResource("/index.html")).thenReturn(null);

expectedException.expect(IllegalStateException.class);
underTest.init(filterConfig);
}

private void mockIndexFile() throws MalformedURLException {
when(servletContext.getResource("/index.html")).thenReturn(getClass().getResource("WebPagesFilterTest/index.html"));
private void verifyDefaultHtml(String path) throws Exception {
verifyHtml(path, "Content of /index.html with context [" + TEST_CONTEXT + "]");
}

private void mockPath(String path, String context) {
private void verifyHtml(String path, String expectedContent) throws Exception {
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getRequestURI()).thenReturn(path);
when(request.getContextPath()).thenReturn(context);
when(servletContext.getContextPath()).thenReturn(context);
}

private void verifyPathIsHandled(String path) throws Exception {
mockPath(path, "");
underTest.init(filterConfig);

underTest.doFilter(request, response, chain);

verify(response).getOutputStream();
verify(response).setContentType(anyString());
reset(response);
when(request.getContextPath()).thenReturn(TEST_CONTEXT);
HttpServletResponse response = mock(HttpServletResponse.class);
StringOutputStream outputStream = new StringOutputStream();
when(response.getOutputStream()).thenReturn(outputStream);
}

private void verifyPthIsIgnored(String path) throws Exception {
mockPath(path, "");
underTest.init(filterConfig);
FilterChain chain = mock(FilterChain.class);

underTest.doFilter(request, response, chain);

verifyZeroInteractions(response);
reset(response);
when(response.getOutputStream()).thenReturn(outputStream);
verify(response).setContentType("text/html");
verify(response).setCharacterEncoding("utf-8");
verify(response).setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
assertThat(outputStream.toString()).isEqualTo(expectedContent);
}

class StringOutputStream extends ServletOutputStream {
private StringBuffer buf = new StringBuffer();
private final StringBuilder buf = new StringBuilder();

StringOutputStream() {
}
@@ -176,7 +121,7 @@ public class WebPagesFilterTest {
}

public void write(int b) {
byte[] bytes = new byte[] {(byte) b};
byte[] bytes = new byte[]{(byte) b};
this.buf.append(new String(bytes));
}


+ 0
- 14
server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="%WEB_CONTEXT%/favicon.ico" rel="shortcut icon" type="image/x-icon">
<link href="%WEB_CONTEXT%/sonar.css" rel="stylesheet">
<title>SonarQube</title>
</head>
<body>
<div id="content"></div>
<script>window.baseUrl = '%WEB_CONTEXT%';</script>
<script src="%WEB_CONTEXT%/sonar.js"></script>
</body>
</html>

+ 65
- 0
server/sonar-web/config/utils.js View File

@@ -0,0 +1,65 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
const cssMinimizeOptions = {
discardComments: { removeAll: true }
};

const cssLoader = ({ production }) => ({
loader: 'css-loader',
options: {
importLoaders: 1,
minimize: production && cssMinimizeOptions,
url: false
}
});

const postcssLoader = () => ({
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [
require('autoprefixer'),
require('postcss-custom-properties')({
variables: require('../src/main/js/app/theme')
}),
require('postcss-calc')
]
}
});

const minifyParams = ({ production }) =>
production && {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
};

module.exports = {
cssLoader,
postcssLoader,
minifyParams
};

+ 110
- 0
server/sonar-web/config/vsts.webpack.config.js View File

@@ -0,0 +1,110 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
/* eslint-disable import/no-extraneous-dependencies */
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const webpack = require('webpack');
const paths = require('./paths');
const utils = require('./utils');

module.exports = ({ production = true, fast = false }) => ({
bail: production,

devtool: production ? (fast ? false : 'source-map') : 'cheap-module-source-map',
resolve: {
// Add '.ts' and '.tsx' as resolvable extensions.
extensions: ['.ts', '.tsx', '.js', '.json']
},
entry: {
vsts: [
!production && require.resolve('react-dev-utils/webpackHotDevClient'),
!production && require.resolve('react-error-overlay'),
'react',
'react-dom',
'./src/main/js/app/integration/vsts/index.js'
].filter(Boolean)
},
output: {
path: paths.vstsBuild,
pathinfo: !production,
publicPath: '/integration/vsts/',
filename: production ? 'js/[name].[chunkhash:8].js' : 'js/[name].js',
chunkFilename: production ? 'js/[name].[chunkhash:8].chunk.js' : 'js/[name].chunk.js'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /(node_modules|libs)/
},
{
test: /\.tsx?$/,
use: [
{
loader: 'awesome-typescript-loader',
options: {
transpileOnly: true,
useBabel: true,
useCache: true
}
}
]
},
{
test: /\.css$/,
use: ['style-loader', utils.cssLoader({ production, fast }), utils.postcssLoader()]
}
].filter(Boolean)
},
plugins: [
!production && new InterpolateHtmlPlugin({ WEB_CONTEXT: '' }),

new HtmlWebpackPlugin({
inject: false,
template: paths.vstsHtml,
minify: utils.minifyParams({ production, fast })
}),

new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(production ? 'production' : 'development')
}),

new CopyWebpackPlugin([
{
from: './src/main/js/libs/third-party/VSS.SDK.min.js',
to: 'js/'
}
]),

production &&
!fast &&
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: { screw_ie8: true, warnings: false },
mangle: { screw_ie8: true },
output: { comments: false, screw_ie8: true }
}),

!production && new webpack.HotModuleReplacementPlugin()
].filter(Boolean)
});

+ 2
- 39
server/sonar-web/config/webpack.config.js View File

@@ -26,33 +26,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const InterpolateHtmlPlugin = require('./InterpolateHtmlPlugin');
const paths = require('./paths');

const cssMinimizeOptions = {
discardComments: { removeAll: true }
};

const cssLoader = ({ production }) => ({
loader: 'css-loader',
options: {
importLoaders: 1,
minimize: production && cssMinimizeOptions,
url: false
}
});

const postcssLoader = () => ({
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [
require('autoprefixer'),
require('postcss-custom-properties')({
variables: require('../src/main/js/app/theme')
}),
require('postcss-calc')
]
}
});
const utils = require('./utils');

module.exports = ({ production = true }) => ({
mode: production ? 'production' : 'development',
@@ -122,18 +96,7 @@ module.exports = ({ production = true }) => ({
new HtmlWebpackPlugin({
inject: false,
template: paths.appHtml,
minify: production && {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
}
minify: utils.minifyParams({ production })
}),

// keep `InterpolateHtmlPlugin` after `HtmlWebpackPlugin`

+ 7
- 1
server/sonar-web/package.json View File

@@ -106,7 +106,13 @@
},
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"start-vsts": "node scripts/vsts.start.js",
"build": "yarn build-core && yarn build-vsts",
"build-fast": "yarn build-core-fast && yarn build-vsts-fast",
"build-core": "node scripts/build.js",
"build-core-fast": "node scripts/build.js --fast",
"build-vsts": "node scripts/vsts.build.js",
"build-vsts-fast": "node scripts/vsts.build.js --fast",
"test": "node scripts/test.js",
"coverage": "npm test -- --coverage",
"format": "prettier --write --list-different 'src/main/js/!(libs)/**/*.{js,ts,tsx,css}'",

+ 26
- 0
server/sonar-web/public/integration/vsts/index.html View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<meta name="application-name" content="SonarQube VSTS Widget" />

<script src="%WEB_CONTEXT%/integration/vsts/js/VSS.SDK.min.js"></script>
<title>SonarQube VSTS Widget</title>
</head>

<body>
<div id="content">
<div class="vsts-loading">
<i class="spinner global-loading-spinner"></i>
</div>
</div>
<script>window.baseUrl = '%WEB_CONTEXT%';</script>
<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
<script src="%WEB_CONTEXT%<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
<% } %>
</body>

</html>

+ 2
- 1
server/sonar-web/scripts/start.js View File

@@ -108,7 +108,8 @@ function runDevServer(compiler, host, port, protocol) {
'/api': proxy,
'/fonts': proxy,
'/images': proxy,
'/static': proxy
'/static': proxy,
'/integration': proxy
}
});


+ 97
- 0
server/sonar-web/scripts/vsts.build.js View File

@@ -0,0 +1,97 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
/* eslint-disable no-console*/
process.env.NODE_ENV = 'production';

const chalk = require('chalk');
const fs = require('fs-extra');
const rimrafSync = require('rimraf').sync;
const webpack = require('webpack');
const paths = require('../config/paths');
const formatSize = require('./utils/formatSize');
const getConfig = require('../config/vsts.webpack.config');

const fast = process.argv.some(arg => arg.indexOf('--fast') > -1);

const config = getConfig({ fast, production: true });

function clean() {
// Remove all content but keep the directory so that
// if you're in it, you don't end up in Trash
console.log(chalk.cyan.bold('Cleaning output directories and files...'));

console.log(paths.vstsBuild + '/*');
rimrafSync(paths.vstsBuild + '/*');

console.log();
}

function build() {
if (fast) {
console.log(chalk.magenta.bold('Running fast build...'));
} else {
console.log(chalk.cyan.bold('Creating optimized production build...'));
}
console.log();

webpack(config, (err, stats) => {
if (err) {
console.log(chalk.red.bold('Failed to create a production build!'));
console.log(chalk.red(err.message || err));
process.exit(1);
}

if (stats.compilation.errors && stats.compilation.errors.length) {
console.log(chalk.red.bold('Failed to create a production build!'));
stats.compilation.errors.forEach(err => console.log(chalk.red(err.message || err)));
process.exit(1);
}

const jsonStats = stats.toJson();

console.log('Assets:');
const assets = jsonStats.assets.slice();
assets.sort((a, b) => b.size - a.size);
assets.forEach(asset => {
let sizeLabel = formatSize(asset.size);
const leftPadding = ' '.repeat(Math.max(0, 8 - sizeLabel.length));
sizeLabel = leftPadding + sizeLabel;
console.log('', chalk.yellow(sizeLabel), asset.name);
});
console.log();

const seconds = jsonStats.time / 1000;
console.log('Duration: ' + seconds.toFixed(2) + 's');
console.log();

console.log(chalk.green.bold('Compiled successfully!'));
});
}

function copyPublicFolder() {
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: file => file !== paths.appHtml
});
}

clean();
build();
copyPublicFolder();

+ 113
- 0
server/sonar-web/scripts/vsts.start.js View File

@@ -0,0 +1,113 @@
/*
* SonarQube
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact 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.
*/
/* eslint-disable no-console */
process.env.NODE_ENV = 'development';

const chalk = require('chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const errorOverlayMiddleware = require('react-error-overlay/middleware');
const getConfig = require('../config/vsts.webpack.config');
const paths = require('../config/paths');

const config = getConfig({ production: false });

const port = process.env.PORT || 3000;
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const host = process.env.HOST || 'localhost';
const proxy = process.env.PROXY || 'http://localhost:9000';

const compiler = setupCompiler(host, port, protocol);

runDevServer(compiler, host, port, protocol);

function setupCompiler(host, port, protocol) {
const compiler = webpack(config);

compiler.plugin('invalid', () => {
clearConsole();
console.log('Compiling...');
});

compiler.plugin('done', stats => {
clearConsole();

const jsonStats = stats.toJson({}, true);
const messages = formatWebpackMessages(jsonStats);
const seconds = jsonStats.time / 1000;
if (!messages.errors.length && !messages.warnings.length) {
console.log(chalk.green('Compiled successfully!'));
console.log('Duration: ' + seconds.toFixed(2) + 's');
console.log();
console.log('The app is running at:');
console.log();
console.log(' ' + chalk.cyan(protocol + '://' + host + ':' + port + '/'));
console.log();
}

if (messages.errors.length) {
console.log(chalk.red('Failed to compile.'));
console.log();
messages.errors.forEach(message => {
console.log(message);
console.log();
});
}
});

return compiler;
}

function runDevServer(compiler, host, port, protocol) {
const devServer = new WebpackDevServer(compiler, {
before(app) {
app.use(errorOverlayMiddleware());
},
compress: true,
clientLogLevel: 'none',
contentBase: paths.appPublic,
disableHostCheck: true,
hot: true,
publicPath: config.output.publicPath,
quiet: true,
watchOptions: {
ignored: /node_modules/
},
https: protocol === 'https',
host,
overlay: false,
proxy: {
'/': proxy
}
});

devServer.listen(port, err => {
if (err) {
console.log(err);
return;
}

clearConsole();
console.log(chalk.cyan('Starting the development server...'));
console.log();
});
}

+ 1
- 1
server/sonar-web/src/main/js/api/measures.ts View File

@@ -29,7 +29,7 @@ export function getMeasures(
return getJSON('/api/measures/component', data).then(r => r.component.measures, throwGlobalError);
}

interface MeasureComponent {
export interface MeasureComponent {
key: string;
description?: string;
measures: Measure[];

+ 147
- 0
server/sonar-web/src/main/js/app/integration/vsts/components/Configuration.tsx View File

@@ -0,0 +1,147 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
import * as React from 'react';
import { searchProjects } from '../../../../api/components';

interface Settings {
project: string;
}

interface Props {
widgetHelpers: any;
}

interface State {
loading: boolean;
organizations?: Array<{ key: string; name: string }>;
projects?: Array<{ label: string; value: string }>;
settings: Settings;
widgetConfigurationContext?: any;
}

declare const VSS: any;

export default class Configuration extends React.PureComponent<Props, State> {
mounted = false;
state: State = { loading: true, settings: { project: '' } };

componentDidMount() {
this.mounted = true;
this.props.widgetHelpers.IncludeWidgetConfigurationStyles();
VSS.register('e56c6ff0-c6f9-43d0-bdef-b3f1aa0dc6dd', () => {
return { load: this.load, onSave: this.onSave };
});
}

componentWillUnmount() {
this.mounted = false;
}

load = (widgetSettings: any, widgetConfigurationContext: any) => {
const settings: Settings = JSON.parse(widgetSettings.customSettings.data);
if (this.mounted) {
this.setState({ settings: settings || {}, widgetConfigurationContext });
this.fetchProjects();
}
return this.props.widgetHelpers.WidgetStatusHelper.Success();
};

onSave = () => {
if (!this.state.settings || !this.state.settings.project) {
return this.props.widgetHelpers.WidgetConfigurationSave.Invalid();
}
return this.props.widgetHelpers.WidgetConfigurationSave.Valid({
data: JSON.stringify(this.state.settings)
});
};

fetchProjects = (organization?: string) => {
this.setState({ loading: true });
searchProjects({ organization, ps: 100 }).then(
({ components }) => {
if (this.mounted) {
this.setState({
projects: components.map(c => ({ label: c.name, value: c.key })),
loading: false
});
}
},
() => {
this.setState({
projects: [],
loading: false
});
}
);
};

handleProjectChange = (
event: React.ChangeEvent<HTMLSelectElement> | React.FocusEvent<HTMLSelectElement>
) => {
const { value } = event.currentTarget;
this.setState(
({ settings }) => ({ settings: { ...settings, project: value } }),
this.notifyChange
);
};

notifyChange = ({ settings, widgetConfigurationContext } = this.state) => {
const { widgetHelpers } = this.props;
if (widgetConfigurationContext && widgetConfigurationContext.notify) {
const eventName = widgetHelpers.WidgetEvent.ConfigurationChange;
const eventArgs = widgetHelpers.WidgetEvent.Args({ data: JSON.stringify(settings) });
widgetConfigurationContext.notify(eventName, eventArgs);
}
};

render() {
const { projects, loading, settings } = this.state;
if (loading) {
return (
<div className="vsts-loading">
<i className="spinner global-loading-spinner" />
</div>
);
}
return (
<div className="widget-configuration">
<div className="dropdown" id="project">
<label>SonarCloud project</label>
<div className="wrapper">
<select
onBlur={this.handleProjectChange}
onChange={this.handleProjectChange}
value={settings.project}>
<option disabled={true} hidden={true} value="">
Select a project...
</option>
{projects &&
projects.map(project => (
<option key={project.value} value={project.value}>
{project.label}
</option>
))}
</select>
</div>
</div>
</div>
);
}
}

+ 57
- 0
server/sonar-web/src/main/js/app/integration/vsts/components/QGWidget.tsx View File

@@ -0,0 +1,57 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
import * as React from 'react';
import * as classNames from 'classnames';
import { MeasureComponent } from '../../../../api/measures';
import { Metric } from '../../../types';
import { getPathUrlAsString, getProjectUrl } from '../../../../helpers/urls';

interface Props {
component: MeasureComponent;
metrics: Metric[];
}

const QG_LEVELS: { [level: string]: string } = {
ERROR: 'Failed',
WARN: 'Warning',
OK: 'Passed',
NONE: 'None'
};

export default function QGWidget({ component, metrics }: Props) {
const qgMetric = metrics && metrics.find(m => m.key === 'alert_status');
const qgMeasure = component && component.measures.find(m => m.metric === 'alert_status');

if (!qgMeasure || !qgMeasure.value) {
return <p>Project Quality Gate not computed.</p>;
}

return (
<div className={classNames('widget dark-widget clickable', 'level-' + qgMeasure.value)}>
<a href={getPathUrlAsString(getProjectUrl(component.key))} target="_blank">
<h2 className="title truncated-text-ellipsis">{component.name}</h2>
<div className="big-value truncated-text-ellipsis">{QG_LEVELS[qgMeasure.value]}</div>
<div className="footer truncated-text-ellipsis">
{qgMetric ? qgMetric.name : 'Quality Gate'}
</div>
</a>
</div>
);
}

+ 106
- 0
server/sonar-web/src/main/js/app/integration/vsts/components/Widget.tsx View File

@@ -0,0 +1,106 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
import * as React from 'react';
import QGWidget from './QGWidget';
import { getMeasuresAndMeta, MeasureComponent } from '../../../../api/measures';
import { Metric } from '../../../types';

interface Props {
widgetHelpers: any;
}

interface State {
component?: MeasureComponent;
loading: boolean;
metrics?: Metric[];
}

declare const VSS: any;

export default class Widget extends React.PureComponent<Props, State> {
mounted = false;
state: State = { loading: true };

componentDidMount() {
this.mounted = true;
this.props.widgetHelpers.IncludeWidgetStyles();
VSS.register('3c598f25-01c1-4c09-97c6-926476882688', () => {
return { load: this.load, reload: this.load };
});
}

componentWillUnmount() {
this.mounted = false;
}

load = (widgetSettings: any) => {
const settings = JSON.parse(widgetSettings.customSettings.data);
if (this.mounted) {
if (settings && settings.project) {
this.fetchProjectMeasures(settings.project);
} else {
this.setState({ loading: false });
}
}
return this.props.widgetHelpers.WidgetStatusHelper.Success();
};

fetchProjectMeasures = (project: string) => {
this.setState({ loading: true });
getMeasuresAndMeta(project, ['alert_status'], { additionalFields: 'metrics' }).then(
({ component, metrics }) => {
if (this.mounted) {
this.setState({ component, loading: false, metrics });
}
},
() => {
this.setState({ loading: false });
}
);
};

render() {
const { component, loading, metrics } = this.state;
if (loading) {
return (
<div className="vsts-loading">
<i className="spinner global-loading-spinner" />
</div>
);
}

if (!component || !metrics) {
return (
<div className="vsts-widget-configure widget">
<h2 className="title">Quality Widget</h2>
<div className="content">
<div>Configure widget</div>
<img
alt=""
src="https://cdn.vsassets.io/v/20180301T143409/_content/Dashboards/unconfigured-small.png"
/>
</div>
</div>
);
}

return <QGWidget component={component} metrics={metrics} />;
}
}

+ 42
- 0
server/sonar-web/src/main/js/app/integration/vsts/index.js View File

@@ -0,0 +1,42 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
import { parse } from 'querystring';
import React from 'react';
import { render } from 'react-dom';
import Configuration from './components/Configuration';
import Widget from './components/Widget';
import './vsts.css';

VSS.init({
explicitNotifyLoaded: true,
usePlatformStyles: true
});

VSS.require('TFS/Dashboards/WidgetHelpers', widgetHelpers => {
const container = document.getElementById('content');
const query = parse(window.location.search.replace('?', ''));

if (query.type === 'configuration') {
render(<Configuration widgetHelpers={widgetHelpers} />, container);
} else {
render(<Widget widgetHelpers={widgetHelpers} />, container);
}
VSS.notifyLoadSucceeded();
});

+ 89
- 0
server/sonar-web/src/main/js/app/integration/vsts/vsts.css View File

@@ -0,0 +1,89 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/

@import '../../styles/components/spinner.css';
@import '../../styles/components/global-loading.css';

#content {
height: 100%;
}

.vsts-loading {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

.vsts-widget-configure {
display: block;
position: relative;
width: 100%;
height: 100%;
padding: 10px 14px;
font-size: 16px;
}

.vsts-widget-configure .title {
color: #333;
font-weight: normal;
}

.vsts-widget-configure .content {
padding-top: 10%;
text-align: center;
color: #666;
}

.vsts-widget-configure img {
height: 40px;
margin-top: 10px;
}

.widget.dark-widget.clickable > a {
color: white;
}

.big-value {
font-size: 36px;
line-height: 68px;
margin: 20px 0 10px 0;
font-weight: 300;
}

.level-OK {
background-color: var(--green);
}

.level-WARN {
background-color: var(--orange);
}

.level-ERROR {
background-color: var(--red);
}

.level-NONE {
background-color: var(--gray71);
}

.Select {
width: 100%;
}

+ 82
- 0
server/sonar-web/src/main/js/app/styles/components/spinner.css View File

@@ -0,0 +1,82 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
.spinner {
position: relative;
vertical-align: middle;
width: 16px;
height: 16px;
border: 2px solid var(--blue);
border-radius: 50%;
animation: spin 0.75s infinite linear;
}

.spinner-placeholder {
position: relative;
display: inline-block;
vertical-align: middle;
width: 16px;
height: 16px;
visibility: hidden;
}

.spinner:before,
.spinner:after {
left: -2px;
top: -2px;
display: none;
position: absolute;
content: '';
width: inherit;
height: inherit;
border: inherit;
border-radius: inherit;
}

.spinner,
.spinner:before,
.spinner:after {
display: inline-block;
box-sizing: border-box;
border-color: transparent;
border-top-color: var(--blue);
animation-duration: 1.2s;
}

.spinner:before {
transform: rotate(120deg);
}

.spinner:after {
transform: rotate(240deg);
}

.spinner-margin {
margin: 10px;
}

@keyframes spin {
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
}

+ 0
- 67
server/sonar-web/src/main/js/app/styles/init/icons.css View File

@@ -783,70 +783,3 @@ a:hover > .icon-radio {
background-image: url(data:image/svg+xml,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M2.977%2012.656c0%20.417-.142.745-.426.985-.283.24-.636.36-1.058.36-.552%200-1-.172-1.344-.516l.446-.687c.255.234.53.35.828.35.15%200%20.282-.036.394-.112.112-.075.168-.186.168-.332%200-.333-.273-.48-.82-.437l-.203-.438c.043-.052.127-.165.255-.34.127-.174.238-.315.332-.422.094-.106.19-.207.29-.3v-.008c-.084%200-.21.002-.38.008-.17.005-.296.007-.38.007v.415H.25V10h2.602v.688l-.743.898c.265.062.476.19.632.383.156.19.235.42.235.686zm.015-4.898V9H.164c-.03-.188-.047-.328-.047-.422%200-.265.06-.508.184-.726.123-.22.27-.396.442-.532.172-.135.344-.26.516-.37.172-.113.32-.226.44-.34.124-.115.185-.232.185-.352%200-.13-.038-.23-.113-.3-.076-.07-.18-.106-.31-.106-.24%200-.45.15-.632.453l-.664-.46c.125-.267.31-.474.56-.622.246-.15.52-.223.823-.223.38%200%20.7.108.96.324.26.216.39.51.39.88%200%20.26-.087.498-.264.714-.177.216-.373.384-.586.504-.214.12-.41.25-.59.394-.18.144-.272.28-.277.41h.992V7.76h.82zM14%2010.25v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074-.05-.05-.074-.108-.074-.176v-1.5c0-.073.023-.133.07-.18.047-.047.107-.07.18-.07h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176zM3%203.227V4H.383v-.773h.836c0-.214%200-.532.003-.954l.004-.945v-.094H1.21c-.04.09-.17.23-.39.422l-.554-.593L1.328.07h.828v3.157H3zM14%206.25v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074C4.024%207.876%204%207.818%204%207.75v-1.5c0-.073.023-.133.07-.18.047-.047.107-.07.18-.07h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176zm0-4v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074C4.024%203.876%204%203.818%204%203.75v-1.5c0-.068.025-.126.074-.176.05-.05.108-.074.176-.074h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176z%22%20fill%3D%22%23236A97%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E);
background-repeat: no-repeat;
}

/*
* Spinner
*/
.spinner {
position: relative;
vertical-align: middle;
width: 16px;
height: 16px;
border: 2px solid var(--blue);
border-radius: 50%;
animation: spin 0.75s infinite linear;
}

.spinner-placeholder {
position: relative;
display: inline-block;
vertical-align: middle;
width: 16px;
height: 16px;
visibility: hidden;
}

.spinner:before,
.spinner:after {
left: -2px;
top: -2px;
display: none;
position: absolute;
content: '';
width: inherit;
height: inherit;
border: inherit;
border-radius: inherit;
}

.spinner,
.spinner:before,
.spinner:after {
display: inline-block;
box-sizing: border-box;
border-color: transparent;
border-top-color: var(--blue);
animation-duration: 1.2s;
}

.spinner:before {
transform: rotate(120deg);
}

.spinner:after {
transform: rotate(240deg);
}

.spinner-margin {
margin: 10px;
}

@keyframes spin {
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
}

+ 1
- 0
server/sonar-web/src/main/js/app/styles/sonar.css View File

@@ -27,6 +27,7 @@
@import './init/misc.css';

@import './components/ui.css';
@import './components/spinner.css';
@import './components/global-loading.css';
@import './components/bubble-popup.css';
@import './components/modals.css';

+ 957
- 0
server/sonar-web/src/main/js/libs/third-party/VSS.SDK.min.js View File

@@ -0,0 +1,957 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
// Copyright (C) Microsoft Corporation. All rights reserved.
var XDM, VSS;
(function(n) {
function u() {
return new o();
}
function s() {
return (
Math.floor(Math.random() * (f - t) + t).toString(36) +
Math.floor(Math.random() * (f - t) + t).toString(36)
);
}
var i, r, e;
n.createDeferred = u;
var o = (function() {
function n() {
var n = this;
this._resolveCallbacks = [];
this._rejectCallbacks = [];
this._isResolved = !1;
this._isRejected = !1;
this.resolve = function(t) {
n._resolve(t);
};
this.reject = function(t) {
n._reject(t);
};
this.promise = {};
this.promise.then = function(t, i) {
return n._then(t, i);
};
}
return (
(n.prototype._then = function(t, i) {
var u = this,
r;
return (!t && !i) || (this._isResolved && !t) || (this._isRejected && !i)
? this.promise
: ((r = new n()),
this._resolveCallbacks.push(function(n) {
u._wrapCallback(t, n, r, !1);
}),
this._rejectCallbacks.push(function(n) {
u._wrapCallback(i, n, r, !0);
}),
this._isResolved
? this._resolve(this._resolvedValue)
: this._isRejected && this._reject(this._rejectValue),
r.promise);
}),
(n.prototype._wrapCallback = function(n, t, i, r) {
if (!n) {
r ? i.reject(t) : i.resolve(t);
return;
}
var u;
try {
u = n(t);
} catch (f) {
i.reject(f);
return;
}
u === undefined
? i.resolve(t)
: u && typeof u.then == 'function'
? u.then(
function(n) {
i.resolve(n);
},
function(n) {
i.reject(n);
}
)
: i.resolve(u);
}),
(n.prototype._resolve = function(n) {
if (
(this._isRejected ||
this._isResolved ||
((this._isResolved = !0), (this._resolvedValue = n)),
this._isResolved && this._resolveCallbacks.length > 0)
) {
var t = this._resolveCallbacks.splice(0);
window.setTimeout(function() {
for (var i = 0, r = t.length; i < r; i++) t[i](n);
});
}
}),
(n.prototype._reject = function(n) {
if (
(this._isRejected ||
this._isResolved ||
((this._isRejected = !0),
(this._rejectValue = n),
this._rejectCallbacks.length === 0 &&
window.console &&
window.console.warn &&
(console.warn('Rejected XDM promise with no reject callbacks'),
n && console.warn(n))),
this._isRejected && this._rejectCallbacks.length > 0)
) {
var t = this._rejectCallbacks.splice(0);
window.setTimeout(function() {
for (var i = 0, r = t.length; i < r; i++) t[i](n);
});
}
}),
n
);
})(),
t = parseInt('10000000000', 36),
f = Number.MAX_SAFE_INTEGER || 9007199254740991;
i = (function() {
function n() {
this._registeredObjects = {};
}
return (
(n.prototype.register = function(n, t) {
this._registeredObjects[n] = t;
}),
(n.prototype.unregister = function(n) {
delete this._registeredObjects[n];
}),
(n.prototype.getInstance = function(n, t) {
var i = this._registeredObjects[n];
return i ? (typeof i == 'function' ? i(t) : i) : null;
}),
n
);
})();
n.XDMObjectRegistry = i;
n.globalObjectRegistry = new i();
r = (function() {
function t(n, r) {
r === void 0 && (r = null);
this._nextMessageId = 1;
this._deferreds = {};
this._nextProxyFunctionId = 1;
this._proxyFunctions = {};
this._postToWindow = n;
this._targetOrigin = r;
this._channelObjectRegistry = new i();
this._channelId = t._nextChannelId++;
this._targetOrigin || (this._handshakeToken = s());
}
return (
(t.prototype.getObjectRegistry = function() {
return this._channelObjectRegistry;
}),
(t.prototype.invokeRemoteMethod = function(n, t, i, r, f) {
var e = {
id: this._nextMessageId++,
methodName: n,
instanceId: t,
instanceContext: r,
params: this._customSerializeObject(i, f),
jsonrpc: '2.0',
serializationSettings: f
},
o;
return (
this._targetOrigin || (e.handshakeToken = this._handshakeToken),
(o = u()),
(this._deferreds[e.id] = o),
this._sendRpcMessage(e),
o.promise
);
}),
(t.prototype.getRemoteObjectProxy = function(n, t) {
return this.invokeRemoteMethod(null, n, null, t);
}),
(t.prototype.invokeMethod = function(n, t) {
var f = this,
r,
u,
i;
if (!t.methodName) {
this._success(t, n, t.handshakeToken);
return;
}
if (((r = n[t.methodName]), typeof r != 'function')) {
this._error(t, new Error('RPC method not found: ' + t.methodName), t.handshakeToken);
return;
}
try {
u = [];
t.params && (u = this._customDeserializeObject(t.params));
i = r.apply(n, u);
i && i.then && typeof i.then == 'function'
? i.then(
function(n) {
f._success(t, n, t.handshakeToken);
},
function(n) {
f._error(t, n, t.handshakeToken);
}
)
: this._success(t, i, t.handshakeToken);
} catch (e) {
this._error(t, e, t.handshakeToken);
}
}),
(t.prototype.getRegisteredObject = function(t, i) {
if (t === '__proxyFunctions') return this._proxyFunctions;
var r = this._channelObjectRegistry.getInstance(t, i);
return r || (r = n.globalObjectRegistry.getInstance(t, i)), r;
}),
(t.prototype.onMessage = function(n) {
var u = this,
t = n,
i,
r;
if (t.instanceId) {
if (((i = this.getRegisteredObject(t.instanceId, t.instanceContext)), !i)) return !1;
typeof i.then == 'function'
? i.then(
function(n) {
u.invokeMethod(n, t);
},
function(n) {
u._error(t, n, t.handshakeToken);
}
)
: this.invokeMethod(i, t);
} else {
if (((r = this._deferreds[t.id]), !r)) return !1;
t.error
? r.reject(this._customDeserializeObject([t.error])[0])
: r.resolve(this._customDeserializeObject([t.result])[0]);
delete this._deferreds[t.id];
}
return !0;
}),
(t.prototype.owns = function(n, t, i) {
var r = i;
if (this._postToWindow === n) {
if (this._targetOrigin)
return t
? t.toLowerCase() === 'null' ||
this._targetOrigin.toLowerCase().indexOf(t.toLowerCase()) === 0
: !1;
if (r.handshakeToken && r.handshakeToken === this._handshakeToken)
return (this._targetOrigin = t), !0;
}
return !1;
}),
(t.prototype.error = function(n, t) {
var i = n;
this._error(i, t, i.handshakeToken);
}),
(t.prototype._error = function(n, t, i) {
var r = {
id: n.id,
error: this._customSerializeObject([t], n.serializationSettings)[0],
jsonrpc: '2.0',
handshakeToken: i
};
this._sendRpcMessage(r);
}),
(t.prototype._success = function(n, t, i) {
var r = {
id: n.id,
result: this._customSerializeObject([t], n.serializationSettings)[0],
jsonrpc: '2.0',
handshakeToken: i
};
this._sendRpcMessage(r);
}),
(t.prototype._sendRpcMessage = function(n) {
var t = JSON.stringify(n);
this._postToWindow.postMessage(t, '*');
}),
(t.prototype._shouldSkipSerialization = function(n) {
for (var r, i = 0, u = t.WINDOW_TYPES_TO_SKIP_SERIALIZATION.length; i < u; i++)
if (((r = t.WINDOW_TYPES_TO_SKIP_SERIALIZATION[i]), window[r] && n instanceof window[r]))
return !0;
if (window.jQuery)
for (i = 0, u = t.JQUERY_TYPES_TO_SKIP_SERIALIZATION.length; i < u; i++)
if (
((r = t.JQUERY_TYPES_TO_SKIP_SERIALIZATION[i]),
window.jQuery[r] && n instanceof window.jQuery[r])
)
return !0;
return !1;
}),
(t.prototype._customSerializeObject = function(n, i, r, u, f) {
var h = this,
a,
o,
l,
v,
e,
c,
s;
if (
(r === void 0 && (r = null),
u === void 0 && (u = 1),
f === void 0 && (f = 1),
!n || f > t.MAX_XDM_DEPTH) ||
this._shouldSkipSerialization(n)
)
return null;
if (
((a = function(t, e, o) {
var s, c, l, a, v;
try {
s = t[o];
} catch (y) {}
((c = typeof s), c !== 'undefined') &&
((l = -1),
c === 'object' && (l = r.originalObjects.indexOf(s)),
l >= 0
? ((a = r.newObjects[l]),
a.__circularReferenceId || (a.__circularReferenceId = u++),
(e[o] = { __circularReference: a.__circularReferenceId }))
: c === 'function'
? ((v = h._nextProxyFunctionId++),
(e[o] = {
__proxyFunctionId: h._registerProxyFunction(s, n),
__channelId: h._channelId
}))
: c === 'object'
? (e[o] =
s && s instanceof Date
? { __proxyDate: s.getTime() }
: h._customSerializeObject(s, i, r, u, f + 1))
: o !== '__proxyFunctionId' && (e[o] = s));
}),
r || (r = { newObjects: [], originalObjects: [] }),
r.originalObjects.push(n),
n instanceof Array)
)
for (o = [], r.newObjects.push(o), e = 0, c = n.length; e < c; e++) a(n, o, e);
else {
o = {};
r.newObjects.push(o);
l = {};
try {
for (s in n) l[s] = !0;
for (v = Object.getOwnPropertyNames(n), e = 0, c = v.length; e < c; e++) l[v[e]] = !0;
} catch (y) {}
for (s in l) ((s && s[0] !== '_') || (i && i.includeUnderscoreProperties)) && a(n, o, s);
}
return r.originalObjects.pop(), r.newObjects.pop(), o;
}),
(t.prototype._registerProxyFunction = function(n, t) {
var i = this._nextProxyFunctionId++;
return (
(this._proxyFunctions['proxy' + i] = function() {
return n.apply(t, Array.prototype.slice.call(arguments, 0));
}),
i
);
}),
(t.prototype._customDeserializeObject = function(n, t) {
var e = this,
o = this,
r,
i,
u,
f;
if (!n) return null;
if (
(t || (t = {}),
(r = function(n, i) {
var r = n[i],
u = typeof r;
i === '__circularReferenceId' && u === 'number'
? ((t[r] = n), delete n[i])
: u === 'object' &&
r &&
(r.__proxyFunctionId
? (n[i] = function() {
return o.invokeRemoteMethod(
'proxy' + r.__proxyFunctionId,
'__proxyFunctions',
Array.prototype.slice.call(arguments, 0),
null,
{ includeUnderscoreProperties: !0 }
);
})
: r.__proxyDate
? (n[i] = new Date(r.__proxyDate))
: r.__circularReference
? (n[i] = t[r.__circularReference])
: e._customDeserializeObject(r, t));
}),
n instanceof Array)
)
for (i = 0, u = n.length; i < u; i++) r(n, i);
else if (typeof n == 'object') for (f in n) r(n, f);
return n;
}),
(t._nextChannelId = 1),
(t.MAX_XDM_DEPTH = 100),
(t.WINDOW_TYPES_TO_SKIP_SERIALIZATION = ['Node', 'Window', 'Event']),
(t.JQUERY_TYPES_TO_SKIP_SERIALIZATION = ['jQuery']),
t
);
})();
n.XDMChannel = r;
e = (function() {
function n() {
this._channels = [];
this._subscribe(window);
}
return (
(n.get = function() {
return this._default || (this._default = new n()), this._default;
}),
(n.prototype.addChannel = function(n, t) {
var i = new r(n, t);
return this._channels.push(i), i;
}),
(n.prototype.removeChannel = function(n) {
this._channels = this._channels.filter(function(t) {
return t !== n;
});
}),
(n.prototype._handleMessageReceived = function(n) {
var i, e, r, t, u, f;
if (typeof n.data == 'string')
try {
t = JSON.parse(n.data);
} catch (o) {}
if (t) {
for (u = !1, i = 0, e = this._channels.length; i < e; i++)
(r = this._channels[i]),
r.owns(n.source, n.origin, t) && ((f = r), (u = r.onMessage(t, n.origin) || u));
!f ||
u ||
(window.console &&
console.error('No handler found on any channel for message: ' + JSON.stringify(t)),
t.instanceId &&
f.error(t, 'The registered object ' + t.instanceId + ' could not be found.'));
}
}),
(n.prototype._subscribe = function(n) {
var t = this;
n.addEventListener
? n.addEventListener('message', function(n) {
t._handleMessageReceived(n);
})
: n.attachEvent('onmessage', function(n) {
t._handleMessageReceived(n);
});
}),
n
);
})();
n.XDMChannelManager = e;
})(XDM || (XDM = {})),
(function(n) {
function at() {
function r() {
n ||
(n = setTimeout(function() {
n = 0;
tt();
}, 50));
}
var n,
i = !1,
t;
try {
i = typeof document.cookie == 'string';
} catch (f) {}
i ||
Object.defineProperty(Document.prototype, 'cookie', {
get: function() {
return '';
},
set: function() {}
});
t = !1;
try {
t = !!window.localStorage;
} catch (f) {}
t ||
(delete window.localStorage,
(u = new g(r)),
Object.defineProperty(window, 'localStorage', { value: u }),
delete window.sessionStorage,
Object.defineProperty(window, 'sessionStorage', { value: new g() }));
}
function nt(f) {
r = f || {};
e = r.usePlatformScripts;
a = r.usePlatformStyles;
window.setTimeout(function() {
var f = {
notifyLoadSucceeded: !r.explicitNotifyLoaded,
extensionReusedCallback: r.extensionReusedCallback,
vssSDKVersion: n.VssSDKVersion
};
i.invokeRemoteMethod('initialHandshake', 'VSS.HostControl', [f]).then(function(n) {
var f, r, o, h, l, s, v, i;
if (
((t = n.pageContext),
(b = t.webContext),
(k = n.initialConfig || {}),
(d = n.contribution),
(c = n.extensionContext),
n.sandboxedStorage)
) {
if (((f = !1), u))
if (n.sandboxedStorage.localStorage) {
for (
r = n.sandboxedStorage.localStorage, o = 0, h = Object.keys(u);
o < h.length;
o++
)
(i = h[o]), (l = u.getItem(i)), l !== r[i] && ((r[i] = l), (f = !0));
for (s = 0, v = Object.keys(r); s < v.length; s++) (i = v[s]), u.setItem(i, r[i]);
} else u.length > 0 && (f = !0);
lt = !0;
f && tt();
}
e || a ? ht() : w();
});
}, 0);
}
function tt() {
var n = { localStorage: JSON.stringify(u || {}) };
i.invokeRemoteMethod('updateSandboxedStorage', 'VSS.HostControl', [n]);
}
function pt(n, t) {
var i;
i = typeof n == 'string' ? [n] : n;
t || (t = function() {});
l
? it(i, t)
: (r ? e || ((e = !0), s && ((s = !1), ht())) : nt({ usePlatformScripts: !0 }),
rt(function() {
it(i, t);
}));
}
function it(n, i) {
t.diagnostics.bundlingEnabled
? window.require(['VSS/Bundling'], function(t) {
t.requireModules(n).spread(function() {
i.apply(this, arguments);
});
})
: window.require(n, i);
}
function rt(n) {
s ? window.setTimeout(n, 0) : (f || (f = []), f.push(n));
}
function wt() {
i.invokeRemoteMethod('notifyLoadSucceeded', 'VSS.HostControl');
}
function ut(n) {
i.invokeRemoteMethod('notifyLoadFailed', 'VSS.HostControl', [n]);
}
function ft() {
return b;
}
function bt() {
return k;
}
function et() {
return c;
}
function kt() {
return d;
}
function dt(n, t) {
return ot(n).then(function(n) {
return (
t || (t = {}),
t.webContext || (t.webContext = ft()),
t.extensionContext || (t.extensionContext = et()),
n.getInstance(n.id, t)
);
});
}
function ot(t) {
var r = XDM.createDeferred();
return (
n.ready(function() {
i
.invokeRemoteMethod('getServiceContribution', 'vss.hostManagement', [t])
.then(function(n) {
var t = n;
t.getInstance = function(t, i) {
return st(n, t, i);
};
r.resolve(t);
}, r.reject);
}),
r.promise
);
}
function gt(t) {
var r = XDM.createDeferred();
return (
n.ready(function() {
i
.invokeRemoteMethod('getContributionsForTarget', 'vss.hostManagement', [t])
.then(function(n) {
var t = [];
n.forEach(function(n) {
var i = n;
i.getInstance = function(t, i) {
return st(n, t, i);
};
t.push(i);
});
r.resolve(t);
}, r.reject);
}),
r.promise
);
}
function st(t, r, u) {
var f = XDM.createDeferred();
return (
n.ready(function() {
i
.invokeRemoteMethod('getBackgroundContributionInstance', 'vss.hostManagement', [
t,
r,
u
])
.then(f.resolve, f.reject);
}),
f.promise
);
}
function ni(n, t) {
i.getObjectRegistry().register(n, t);
}
function ti(n) {
i.getObjectRegistry().unregister(n);
}
function ii(n, t) {
return i.getObjectRegistry().getInstance(n, t);
}
function ri() {
return i.invokeRemoteMethod('getAccessToken', 'VSS.HostControl');
}
function ui() {
return i.invokeRemoteMethod('getAppToken', 'VSS.HostControl');
}
function fi(n, t) {
o || (o = document.getElementsByTagName('body').item(0));
var r = typeof n == 'number' ? n : o.scrollWidth,
u = typeof t == 'number' ? t : o.scrollHeight;
i.invokeRemoteMethod('resize', 'VSS.HostControl', [r, u]);
}
function ht() {
var i = si(t.webContext),
f,
g,
n,
s,
o,
b,
k,
nt,
tt,
d,
u;
if (
((window.__vssPageContext = t),
(window.__cultureInfo = t.microsoftAjaxConfig.cultureInfo),
a !== !1 &&
t.coreReferences.stylesheets &&
t.coreReferences.stylesheets.forEach(function(n) {
if (n.isCoreStylesheet) {
var t = document.createElement('link');
t.href = h(n.url, i);
t.rel = 'stylesheet';
p(t, 'head');
}
}),
!e)
) {
l = !0;
w();
return;
}
if (
((f = []),
(g = !1),
t.coreReferences.scripts &&
(t.coreReferences.scripts.forEach(function(n) {
if (n.isCoreModule) {
var r = !1,
t = window;
n.identifier === 'JQuery'
? (r = !!t.jQuery)
: n.identifier === 'JQueryUI'
? (r = !!(t.jQuery && t.jQuery.ui && t.jQuery.ui.version))
: n.identifier === 'AMDLoader' &&
(r = typeof t.define == 'function' && !!t.define.amd);
r ? (g = !0) : f.push({ source: h(n.url, i) });
}
}),
t.coreReferences.coreScriptsBundle &&
!g &&
(f = [{ source: h(t.coreReferences.coreScriptsBundle.url, i) }]),
t.coreReferences.extensionCoreReferences &&
f.push({ source: h(t.coreReferences.extensionCoreReferences.url, i) })),
(n = { baseUrl: c.baseUri, contributionPaths: null, paths: {}, shim: {} }),
r.moduleLoaderConfig &&
(r.moduleLoaderConfig.baseUrl && (n.baseUrl = r.moduleLoaderConfig.baseUrl),
oi(r.moduleLoaderConfig, n),
ct(r.moduleLoaderConfig, n)),
t.moduleLoaderConfig &&
(ct(t.moduleLoaderConfig, n), (s = t.moduleLoaderConfig.contributionPaths), s))
)
for (o in s)
if (
s.hasOwnProperty(o) &&
!n.paths[o] &&
((b = s[o].value),
(n.paths[o] = b.match('^https?://') ? b : i + b),
(k = t.moduleLoaderConfig.paths),
k)
) {
nt = o + '/';
tt = v(i, t.moduleLoaderConfig.baseUrl);
for (d in k)
ei(d, nt) &&
((u = k[d]),
u.match('^https?://') || (u = u[0] === '/' ? v(i, u) : v(tt, u)),
(n.paths[d] = u));
}
window.__vssModuleLoaderConfig = n;
f.push({ content: 'require.config(' + JSON.stringify(n) + ');' });
y(f, 0, function() {
l = !0;
w();
});
}
function ei(n, t) {
return n && n.length >= t.length ? n.substr(0, t.length).localeCompare(t) === 0 : !1;
}
function v(n, t) {
var i = n || '';
return i[i.length - 1] !== '/' && (i += '/'), t && (i += t[0] === '/' ? t.substr(1) : t), i;
}
function oi(n, t, i) {
var r, u;
if (n.paths) {
t.paths || (t.paths = {});
for (r in n.paths)
n.paths.hasOwnProperty(r) &&
((u = n.paths[r]), i && (u = i(r, n.paths[r])), u && (t.paths[r] = u));
}
}
function ct(n, t) {
if (n.shim) {
t.shim || (t.shim = {});
for (var i in n.shim) n.shim.hasOwnProperty(i) && (t.shim[i] = n.shim[i]);
}
}
function si(n) {
var r = n.account || n.host,
t = r.uri,
i = r.relativeUri;
return (
t &&
i &&
(t[t.length - 1] !== '/' && (t += '/'),
i[i.length - 1] !== '/' && (i += '/'),
(t = t.substr(0, t.length - i.length))),
t
);
}
function y(n, t, i) {
var f = this,
r,
u;
if (t >= n.length) {
i.call(this);
return;
}
r = document.createElement('script');
r.type = 'text/javascript';
n[t].source
? ((u = n[t].source),
(r.src = u),
r.addEventListener('load', function() {
y.call(f, n, t + 1, i);
}),
r.addEventListener('error', function() {
ut('Failed to load script: ' + u);
}),
p(r, 'head'))
: n[t].content && ((r.textContent = n[t].content), p(r, 'head'), y.call(this, n, t + 1, i));
}
function p(n, t) {
var i = document.getElementsByTagName(t)[0];
i || ((i = document.createElement(t)), document.appendChild(i));
i.appendChild(n);
}
function h(n, t) {
var i = (n || '').toLowerCase();
return (
i.substr(0, 2) !== '//' &&
i.substr(0, 5) !== 'http:' &&
i.substr(0, 6) !== 'https:' &&
(n = t + (i[0] === '/' ? '' : '/') + n),
n
);
}
function w() {
var t = this,
n;
s = !0;
f &&
((n = f),
(f = null),
n.forEach(function(n) {
n.call(t);
}));
}
var yt;
n.VssSDKVersion = 2;
n.VssSDKRestVersion = '4.0';
var o,
b,
t,
c,
k,
d,
r,
l = !1,
e,
a,
s = !1,
f,
i = XDM.XDMChannelManager.get().addChannel(window.parent),
u,
lt = !1,
g = (function() {
function n() {
t && t.call(this);
}
function i() {}
var t;
return (
Object.defineProperties(i.prototype, {
getItem: {
get: function() {
return function(n) {
var t = this['' + n];
return typeof t == 'undefined' ? null : t;
};
}
},
setItem: {
get: function() {
return function(t, i) {
t = '' + t;
var u = this[t],
r = '' + i;
u !== r && ((this[t] = r), n());
};
}
},
removeItem: {
get: function() {
return function(t) {
t = '' + t;
typeof this[t] != 'undefined' && (delete this[t], n());
};
}
},
clear: {
get: function() {
return function() {
var r = Object.keys(this),
t,
i,
u;
if (r.length > 0) {
for (t = 0, i = r; t < i.length; t++) (u = i[t]), delete this[u];
n();
}
};
}
},
key: {
get: function() {
return function(n) {
return Object.keys(this)[n];
};
}
},
length: {
get: function() {
return Object.keys(this).length;
}
}
}),
i
);
})();
if (!window.__vssNoSandboxShim)
try {
at();
} catch (vt) {
window.console &&
window.console.warn &&
window.console.warn(
'Failed to shim support for sandboxed properties: ' +
vt.message +
'. Set "window.__vssNoSandboxShim = true" in order to bypass the shim of sandboxed properties.'
);
}
(function(n) {
n.Dialog = 'ms.vss-web.dialog-service';
n.Navigation = 'ms.vss-web.navigation-service';
n.ExtensionData = 'ms.vss-web.data-service';
})((yt = n.ServiceIds || (n.ServiceIds = {})));
n.init = nt;
n.require = pt;
n.ready = rt;
n.notifyLoadSucceeded = wt;
n.notifyLoadFailed = ut;
n.getWebContext = ft;
n.getConfiguration = bt;
n.getExtensionContext = et;
n.getContribution = kt;
n.getService = dt;
n.getServiceContribution = ot;
n.getServiceContributions = gt;
n.register = ni;
n.unregister = ti;
n.getRegisteredObject = ii;
n.getAccessToken = ri;
n.getAppToken = ui;
n.resize = fi;
})(VSS || (VSS = {}));

+ 2
- 2
server/sonar-web/yarn.lock View File

@@ -7496,8 +7496,8 @@ sshpk@^1.7.0:
tweetnacl "~0.14.0"

ssri@^5.2.4:
version "5.3.0"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06"
version "5.2.4"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.2.4.tgz#9985e14041e65fc397af96542be35724ac11da52"
dependencies:
safe-buffer "^5.1.1"


+ 1
- 1
sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java View File

@@ -140,7 +140,7 @@ public abstract class ServletFilter implements Filter {
*/
public static class Builder {
private static final String WILDCARD_CHAR = "*";
private static final Collection<String> STATIC_RESOURCES = unmodifiableList(asList("/css/*", "/fonts/*", "/images/*", "/js/*", "/static/*",
private static final Collection<String> STATIC_RESOURCES = unmodifiableList(asList("*.css", "*.css.map", "*.ico", "*.png", "*.gif", "*.svg", "*.js", "*.js.map", "*.eot", "*.ttf", "*.woff", "/static/*",
"/robots.txt", "/favicon.ico", "/apple-touch-icon*", "/mstile*"));

private final Set<String> inclusions = new LinkedHashSet<>();

+ 11
- 4
sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java View File

@@ -217,10 +217,17 @@ public class ServletFilterTest {
@Test
public void test_staticResourcePatterns() {
assertThat(ServletFilter.UrlPattern.Builder.staticResourcePatterns()).containsOnly(
"/css/*",
"/fonts/*",
"/images/*",
"/js/*",
"*.css",
"*.css.map",
"*.ico",
"*.png",
"*.gif",
"*.svg",
"*.js",
"*.js.map",
"*.eot",
"*.ttf",
"*.woff",
"/static/*",
"/robots.txt",
"/favicon.ico",

Loading…
Cancel
Save