diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index d25be6bd6c..ecffcd3a38 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -72,7 +72,7 @@ public class ScmConfiguration implements Configuration { /** * SCM Manager release feed url */ - public static final String RELEASE_FEED_URL = + public static final String DEFAULT_RELEASE_FEED_URL = "https://www.scm-manager.org/download/rss.xml"; /** @@ -146,6 +146,9 @@ public class ScmConfiguration implements Configuration { @XmlElement(name = "plugin-url") private String pluginUrl = DEFAULT_PLUGINURL; + @XmlElement(name = "release-feed-url") + private String releaseFeedUrl = DEFAULT_RELEASE_FEED_URL; + /** * Login attempt timeout. * @@ -223,6 +226,7 @@ public class ScmConfiguration implements Configuration { this.enabledXsrfProtection = other.enabledXsrfProtection; this.namespaceStrategy = other.namespaceStrategy; this.loginInfoUrl = other.loginInfoUrl; + this.releaseFeedUrl = other.releaseFeedUrl; } /** @@ -278,6 +282,15 @@ public class ScmConfiguration implements Configuration { return pluginUrl; } + /** + * Returns the url of the rss release feed. + * + * @return the rss release feed url. + */ + public String getReleaseFeedUrl() { + return releaseFeedUrl; + } + /** * Returns a set of glob patterns for urls which should excluded from * proxy settings. @@ -330,6 +343,7 @@ public class ScmConfiguration implements Configuration { /** * Returns {@code true} if anonymous mode is enabled. + * * @return {@code true} if anonymous mode is enabled * @deprecated since 2.4.0 use {@link ScmConfiguration#getAnonymousMode} instead */ @@ -385,6 +399,7 @@ public class ScmConfiguration implements Configuration { /** * Enables the anonymous access at protocol level. + * * @param anonymousAccessEnabled enable or disables the anonymous access * @deprecated since 2.4.0 use {@link ScmConfiguration#setAnonymousMode(AnonymousMode)} instead */ @@ -399,8 +414,8 @@ public class ScmConfiguration implements Configuration { /** * Configures the anonymous mode. - * @param mode type of anonymous mode * + * @param mode type of anonymous mode * @since 2.4.0 */ public void setAnonymousMode(AnonymousMode mode) { @@ -452,6 +467,10 @@ public class ScmConfiguration implements Configuration { this.pluginUrl = pluginUrl; } + public void setReleaseFeedUrl(String releaseFeedUrl) { + this.releaseFeedUrl = releaseFeedUrl; + } + /** * Set glob patterns for urls which are should be excluded from proxy * settings. diff --git a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedDto.java b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedDto.java new file mode 100644 index 0000000000..98892cbe04 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedDto.java @@ -0,0 +1,88 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.admin; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.time.Instant; +import java.util.List; + +@XmlRootElement +@XmlAccessorType(XmlAccessType.FIELD) +@AllArgsConstructor +public final class ReleaseFeedDto { + + @XmlElement(name = "rss") + private final RSS rss; + + public RSS getRSS() { + return rss; + } + + @XmlRootElement(name = "rss") + @XmlAccessorType(XmlAccessType.FIELD) + @AllArgsConstructor + public static class RSS { + + @XmlElement(name = "channel") + private final Channel channel; + + public Channel getChannel() { + return channel; + } + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "channel") + @Getter + @AllArgsConstructor + public static class Channel { + + private final String title; + private final String description; + private final String link; + private final String generator; + private final Instant lastBuildDate; + @XmlElement(name = "releases") + private final List releases; + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlRootElement(name = "conditions") + @Getter + @AllArgsConstructor + public static class Release { + private final String title; + private final String description; + private final String link; + private final String guid; + private final Instant pubDate; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedReader.java b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedReader.java new file mode 100644 index 0000000000..da22f04f8c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedReader.java @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.admin; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.net.ahc.AdvancedHttpClient; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.Comparator; +import java.util.Optional; + +public class ReleaseFeedReader { + + private static final Logger LOG = LoggerFactory.getLogger(ReleaseFeedReader.class); + + private final AdvancedHttpClient client; + + @Inject + public ReleaseFeedReader(AdvancedHttpClient client) { + this.client = client; + } + + Optional findLatestRelease(String url) throws IOException { + LOG.info("Search for newer versions of SCM-Manager"); + ReleaseFeedDto releaseFeed = client.get(url).request().contentFromXml(ReleaseFeedDto.class); + Optional latestRelease = filterForLatestRelease(releaseFeed); + if (latestRelease.isPresent()) { + ReleaseFeedDto.Release release = latestRelease.get(); + return Optional.of(new ReleaseInfo(release.getTitle(), release.getLink(), release.getPubDate())); + } + return Optional.empty(); + } + + private Optional filterForLatestRelease(ReleaseFeedDto releaseFeed) { + return releaseFeed.getRSS().getChannel().getReleases() + .stream() + .max(Comparator.comparing(ReleaseFeedDto.Release::getPubDate)); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseInfo.java b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseInfo.java new file mode 100644 index 0000000000..fc0cc8b6ac --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseInfo.java @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.admin; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.Instant; + +@AllArgsConstructor +@Getter +public class ReleaseInfo { + private final String title; + private final String link; + private final Instant releaseDate; +} diff --git a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseVersionChecker.java b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseVersionChecker.java new file mode 100644 index 0000000000..b5bac61e31 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseVersionChecker.java @@ -0,0 +1,68 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.admin; + +import sonia.scm.SCMContextProvider; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.version.Version; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.Optional; + +public class ReleaseVersionChecker { + + private final ReleaseFeedReader releaseFeedReader; + private final ScmConfiguration scmConfiguration; + private final SCMContextProvider scmContextProvider; + + @Inject + public ReleaseVersionChecker(ReleaseFeedReader releaseFeedReader, ScmConfiguration scmConfiguration, SCMContextProvider scmContextProvider) { + this.releaseFeedReader = releaseFeedReader; + this.scmConfiguration = scmConfiguration; + this.scmContextProvider = scmContextProvider; + } + + Optional checkForNewerVersion() { + try { + String releaseFeedUrl = scmConfiguration.getReleaseFeedUrl(); + Optional latestRelease = releaseFeedReader.findLatestRelease(releaseFeedUrl); + if (latestRelease.isPresent()) { + if (isNewerVersion(latestRelease.get())) { + return latestRelease; + } + } + } catch (IOException e) { + // This is an silent action. We don't want the user to get any kind of error for this. + return Optional.empty(); + } + return Optional.empty(); + } + + private boolean isNewerVersion(ReleaseInfo releaseInfo) { + Version versionFromReleaseFeed = Version.parse(releaseInfo.getTitle()); + return versionFromReleaseFeed.isNewer(scmContextProvider.getVersion()); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedReaderTest.java b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedReaderTest.java new file mode 100644 index 0000000000..0f88839da5 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedReaderTest.java @@ -0,0 +1,78 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.admin; + +import com.google.common.collect.ImmutableList; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.net.ahc.AdvancedHttpClient; + +import java.io.IOException; +import java.time.Instant; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ReleaseFeedReaderTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + AdvancedHttpClient client; + + @InjectMocks + ReleaseFeedReader releaseFeedReader; + + @Test + void shouldFindLatestRelease() throws IOException { + String url = "https://www.scm-manager.org/download/rss.xml"; + + when(client.get(url).request().contentFromXml(ReleaseFeedDto.class)).thenReturn(createReleaseFeedDto()); + + Optional release = releaseFeedReader.findLatestRelease(url); + + assertThat(release).isPresent(); + assertThat(release.get().getTitle()).isEqualTo("3"); + assertThat(release.get().getTitle()).isEqualTo("download-3"); + } + + private ReleaseFeedDto createReleaseFeedDto() { + ReleaseFeedDto.Release release1 = createRelease("1", "download-1", 1000000000L); + ReleaseFeedDto.Release release2 = createRelease("2", "download-2", 2000000000L); + ReleaseFeedDto.Release release3 = createRelease("3", "download-3", 3000000000L); + ReleaseFeedDto.Channel channel = new ReleaseFeedDto.Channel("scm", "scm releases", "scm-download", "gatsby", Instant.now(), ImmutableList.of(release1, release2, release3)); + ReleaseFeedDto.RSS rss = new ReleaseFeedDto.RSS(channel); + return new ReleaseFeedDto(rss); + } + + private ReleaseFeedDto.Release createRelease(String version, String link, long date) { + return new ReleaseFeedDto.Release(version, version, link, version, Instant.ofEpochMilli(date)); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/admin/ReleaseVersionCheckerTest.java b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseVersionCheckerTest.java new file mode 100644 index 0000000000..e9dd8360b2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseVersionCheckerTest.java @@ -0,0 +1,31 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.admin; + +import static org.junit.jupiter.api.Assertions.*; + +class ReleaseVersionCheckerTest { + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java index 40b6bb837e..cef98c4062 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java @@ -83,7 +83,6 @@ public class ScmConfigurationToConfigDtoMapperTest { public void shouldMapFields() { ScmConfiguration config = createConfiguration(); - when(subject.isPermitted("configuration:write:global")).thenReturn(true); ConfigDto dto = mapper.map(config); @@ -106,6 +105,7 @@ public class ScmConfigurationToConfigDtoMapperTest { assertTrue(dto.isEnabledXsrfProtection()); assertEquals("username", dto.getNamespaceStrategy()); assertEquals("https://scm-manager.org/login-info", dto.getLoginInfoUrl()); + assertEquals("https://www.scm-manager.org/download/rss.xml", dto.getReleaseFeedUrl()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); @@ -159,6 +159,7 @@ public class ScmConfigurationToConfigDtoMapperTest { config.setEnabledXsrfProtection(true); config.setNamespaceStrategy("username"); config.setLoginInfoUrl("https://scm-manager.org/login-info"); + config.setReleaseFeedUrl("https://www.scm-manager.org/download/rss.xml"); return config; }