diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-03-06 17:41:23 +0100 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-03-26 20:20:57 +0200 |
commit | cb0a23c978efcc296cf29837c9e6e1a657403404 (patch) | |
tree | 7e408a33f49254e99e47753c9d7b272c269c53c6 | |
parent | b4125add7a55db6d2dc71a1bd0b2cadbe5ff7887 (diff) | |
download | sonarqube-cb0a23c978efcc296cf29837c9e6e1a657403404.tar.gz sonarqube-cb0a23c978efcc296cf29837c9e6e1a657403404.zip |
VSTS-141 Add VSTS Quality widget
26 files changed, 2025 insertions, 245 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java b/server/sonar-server/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java index f644738ddf2..23c31776d68 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java @@ -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 diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java b/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java index 27632c7c8b6..ab4990322f6 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java @@ -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); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/web/SecurityServletFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/web/SecurityServletFilterTest.java index 6c40c7d2279..4faf044eadb 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/web/SecurityServletFilterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/web/SecurityServletFilterTest.java @@ -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; } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java index 55080c092cb..75b11a726ad 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java @@ -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)); } diff --git a/server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html b/server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html deleted file mode 100644 index 379f83f53c7..00000000000 --- a/server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html +++ /dev/null @@ -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> diff --git a/server/sonar-web/config/utils.js b/server/sonar-web/config/utils.js new file mode 100644 index 00000000000..e2fef069675 --- /dev/null +++ b/server/sonar-web/config/utils.js @@ -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 +}; diff --git a/server/sonar-web/config/vsts.webpack.config.js b/server/sonar-web/config/vsts.webpack.config.js new file mode 100644 index 00000000000..dfab0102c08 --- /dev/null +++ b/server/sonar-web/config/vsts.webpack.config.js @@ -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) +}); diff --git a/server/sonar-web/config/webpack.config.js b/server/sonar-web/config/webpack.config.js index 3461d359ecb..04139c2acf0 100644 --- a/server/sonar-web/config/webpack.config.js +++ b/server/sonar-web/config/webpack.config.js @@ -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` diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 0bc51ddff1b..7bfa1b7d2dc 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -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}'", diff --git a/server/sonar-web/public/integration/vsts/index.html b/server/sonar-web/public/integration/vsts/index.html new file mode 100644 index 00000000000..90f0799d8ad --- /dev/null +++ b/server/sonar-web/public/integration/vsts/index.html @@ -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> diff --git a/server/sonar-web/scripts/start.js b/server/sonar-web/scripts/start.js index 53969869f33..96ceaa8ed9d 100644 --- a/server/sonar-web/scripts/start.js +++ b/server/sonar-web/scripts/start.js @@ -108,7 +108,8 @@ function runDevServer(compiler, host, port, protocol) { '/api': proxy, '/fonts': proxy, '/images': proxy, - '/static': proxy + '/static': proxy, + '/integration': proxy } }); diff --git a/server/sonar-web/scripts/vsts.build.js b/server/sonar-web/scripts/vsts.build.js new file mode 100644 index 00000000000..45a7a8f1f79 --- /dev/null +++ b/server/sonar-web/scripts/vsts.build.js @@ -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(); diff --git a/server/sonar-web/scripts/vsts.start.js b/server/sonar-web/scripts/vsts.start.js new file mode 100644 index 00000000000..a228c3d4a44 --- /dev/null +++ b/server/sonar-web/scripts/vsts.start.js @@ -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(); + }); +} diff --git a/server/sonar-web/src/main/js/api/measures.ts b/server/sonar-web/src/main/js/api/measures.ts index 40d219fcfa3..b5d1e649525 100644 --- a/server/sonar-web/src/main/js/api/measures.ts +++ b/server/sonar-web/src/main/js/api/measures.ts @@ -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[]; diff --git a/server/sonar-web/src/main/js/app/integration/vsts/components/Configuration.tsx b/server/sonar-web/src/main/js/app/integration/vsts/components/Configuration.tsx new file mode 100644 index 00000000000..838114c4cbd --- /dev/null +++ b/server/sonar-web/src/main/js/app/integration/vsts/components/Configuration.tsx @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/app/integration/vsts/components/QGWidget.tsx b/server/sonar-web/src/main/js/app/integration/vsts/components/QGWidget.tsx new file mode 100644 index 00000000000..d7e3910f9e1 --- /dev/null +++ b/server/sonar-web/src/main/js/app/integration/vsts/components/QGWidget.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/app/integration/vsts/components/Widget.tsx b/server/sonar-web/src/main/js/app/integration/vsts/components/Widget.tsx new file mode 100644 index 00000000000..98ac6c85aa7 --- /dev/null +++ b/server/sonar-web/src/main/js/app/integration/vsts/components/Widget.tsx @@ -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} />; + } +} diff --git a/server/sonar-web/src/main/js/app/integration/vsts/index.js b/server/sonar-web/src/main/js/app/integration/vsts/index.js new file mode 100644 index 00000000000..433334447b6 --- /dev/null +++ b/server/sonar-web/src/main/js/app/integration/vsts/index.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/app/integration/vsts/vsts.css b/server/sonar-web/src/main/js/app/integration/vsts/vsts.css new file mode 100644 index 00000000000..8c0b5a62242 --- /dev/null +++ b/server/sonar-web/src/main/js/app/integration/vsts/vsts.css @@ -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%; +} diff --git a/server/sonar-web/src/main/js/app/styles/components/spinner.css b/server/sonar-web/src/main/js/app/styles/components/spinner.css new file mode 100644 index 00000000000..4482584983f --- /dev/null +++ b/server/sonar-web/src/main/js/app/styles/components/spinner.css @@ -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); + } +} diff --git a/server/sonar-web/src/main/js/app/styles/init/icons.css b/server/sonar-web/src/main/js/app/styles/init/icons.css index b6c9cdb078b..25b007c1ed6 100644 --- a/server/sonar-web/src/main/js/app/styles/init/icons.css +++ b/server/sonar-web/src/main/js/app/styles/init/icons.css @@ -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); - } -} diff --git a/server/sonar-web/src/main/js/app/styles/sonar.css b/server/sonar-web/src/main/js/app/styles/sonar.css index 714ed9d74b2..5b6284ebeed 100644 --- a/server/sonar-web/src/main/js/app/styles/sonar.css +++ b/server/sonar-web/src/main/js/app/styles/sonar.css @@ -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'; diff --git a/server/sonar-web/src/main/js/libs/third-party/VSS.SDK.min.js b/server/sonar-web/src/main/js/libs/third-party/VSS.SDK.min.js new file mode 100644 index 00000000000..5f6a76663bd --- /dev/null +++ b/server/sonar-web/src/main/js/libs/third-party/VSS.SDK.min.js @@ -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 = {})); diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index a10da4a267b..906f3b75881 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -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" diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java b/sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java index 69e3050a698..5da7fdb1c12 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java @@ -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<>(); diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java index 6bc34cdaeb5..6d98e78e2b0 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java @@ -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", |