@@ -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 |
@@ -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); | |||
} | |||
} | |||
@@ -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; | |||
} | |||
} |
@@ -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)); | |||
} | |||
@@ -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> |
@@ -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 | |||
}; |
@@ -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) | |||
}); |
@@ -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` |
@@ -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}'", |
@@ -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> |
@@ -108,7 +108,8 @@ function runDevServer(compiler, host, port, protocol) { | |||
'/api': proxy, | |||
'/fonts': proxy, | |||
'/images': proxy, | |||
'/static': proxy | |||
'/static': proxy, | |||
'/integration': proxy | |||
} | |||
}); | |||
@@ -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(); |
@@ -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(); | |||
}); | |||
} |
@@ -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[]; |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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} />; | |||
} | |||
} |
@@ -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(); | |||
}); |
@@ -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%; | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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'; |
@@ -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 = {})); |
@@ -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" | |||
@@ -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<>(); |
@@ -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", |