diff --git a/Jenkinsfile b/Jenkinsfile index c0ca5f33d0..e317ed77d4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -15,7 +15,7 @@ node('docker') { disableConcurrentBuilds() ]) - timeout(activity: true, time: 20, unit: 'MINUTES') { + timeout(activity: true, time: 30, unit: 'MINUTES') { catchError { diff --git a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java index dd87b77210..8702567880 100644 --- a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java +++ b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java @@ -6,6 +6,8 @@ import static java.util.Collections.unmodifiableList; public abstract class ExceptionWithContext extends RuntimeException { + private static final long serialVersionUID = 4327413456580409224L; + private final List context; public ExceptionWithContext(List context, String message) { diff --git a/scm-core/src/main/java/sonia/scm/NotFoundException.java b/scm-core/src/main/java/sonia/scm/NotFoundException.java index 69b9617e93..9c478da855 100644 --- a/scm-core/src/main/java/sonia/scm/NotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/NotFoundException.java @@ -7,6 +7,8 @@ import static java.util.stream.Collectors.joining; public class NotFoundException extends ExceptionWithContext { + private static final long serialVersionUID = 1710455380886499111L; + private static final String CODE = "AGR7UzkhA1"; public NotFoundException(Class type, String id) { diff --git a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java new file mode 100644 index 0000000000..a28812eb8a --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java @@ -0,0 +1,136 @@ +package sonia.scm; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; + +import static java.util.Collections.unmodifiableCollection; + +/** + * Use this exception to handle invalid input values that cannot be handled using + * JEE bean validation. + * Use the {@link Builder} to conditionally create a new exception: + *
+ * Builder
+ *   .doThrow()
+ *   .violation("name or alias must not be empty if not anonymous", "myParameter", "name")
+ *   .violation("name or alias must not be empty if not anonymous", "myParameter", "alias")
+ *   .when(myParameter.getName() == null && myParameter.getAlias() == null && !myParameter.isAnonymous())
+ *   .andThrow()
+ *   .violation("name must be empty if anonymous", "myParameter", "name")
+ *   .when(myParameter.getName() != null && myParameter.isAnonymous());
+ * 
+ * Mind that using this way you do not have to use if-else constructs. + */ +public class ScmConstraintViolationException extends RuntimeException implements Serializable { + + private static final long serialVersionUID = 6904534307450229887L; + + private final Collection violations; + + private final String furtherInformation; + + private ScmConstraintViolationException(Collection violations, String furtherInformation) { + this.violations = violations; + this.furtherInformation = furtherInformation; + } + + /** + * The violations that caused this exception. + */ + public Collection getViolations() { + return unmodifiableCollection(violations); + } + + /** + * An optional URL for more informations about this constraint violation. + */ + public String getUrl() { + return furtherInformation; + } + + /** + * Builder to conditionally create constraint violations. + */ + public static class Builder { + private final Collection violations = new ArrayList<>(); + private String furtherInformation; + + /** + * Use this to create a new builder instance. + */ + public static Builder doThrow() { + return new Builder(); + } + + /** + * Resets this builder to check for further violations. + * @return this builder instance. + */ + public Builder andThrow() { + this.violations.clear(); + this.furtherInformation = null; + return this; + } + + /** + * Describes the violation with a custom message and the affected property. When more than one property is affected, + * you can call this method multiple times. + * @param message The message describing the violation. + * @param pathElements The affected property denoted by the path to reach this property, + * eg. "someParameter", "complexProperty", "attribute" + * @return this builder instance. + */ + public Builder violation(String message, String... pathElements) { + this.violations.add(new ScmConstraintViolation(message, pathElements)); + return this; + } + + /** + * Use this to specify a URL with further information about this violation and hints how to solve this. + * This is optional. + * @return this builder instance. + */ + public Builder withFurtherInformation(String furtherInformation) { + this.furtherInformation = furtherInformation; + return this; + } + + /** + * When the given condition is true, a exception will be thrown. Otherwise this simply resets this + * builder and does nothing else. + * @param condition The condition that indicates a violation of this constraint. + * @return this builder instance. + */ + public Builder when(boolean condition) { + if (condition && !this.violations.isEmpty()) { + throw new ScmConstraintViolationException(violations, furtherInformation); + } + return andThrow(); + } + } + + /** + * A single constraint violation. + */ + public static class ScmConstraintViolation implements Serializable { + + private static final long serialVersionUID = -6900317468157084538L; + + private final String message; + private final String path; + + private ScmConstraintViolation(String message, String... pathElements) { + this.message = message; + this.path = String.join(".", pathElements); + } + + public String getMessage() { + return message; + } + + public String getPropertyPath() { + return path; + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/InitialRepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/InitialRepositoryLocationResolver.java index 54ef8875b0..23dbf85f24 100644 --- a/scm-core/src/main/java/sonia/scm/repository/InitialRepositoryLocationResolver.java +++ b/scm-core/src/main/java/sonia/scm/repository/InitialRepositoryLocationResolver.java @@ -1,8 +1,12 @@ package sonia.scm.repository; +import com.google.common.base.CharMatcher; + import java.nio.file.Path; import java.nio.file.Paths; +import static com.google.common.base.Preconditions.checkArgument; + /** * A Location Resolver for File based Repository Storage. *

@@ -19,6 +23,8 @@ public class InitialRepositoryLocationResolver { private static final String DEFAULT_REPOSITORY_PATH = "repositories"; + private static final CharMatcher ID_MATCHER = CharMatcher.anyOf("/\\."); + /** * Returns the initial path to repository. * @@ -26,7 +32,10 @@ public class InitialRepositoryLocationResolver { * * @return initial path of repository */ + @SuppressWarnings("squid:S2083") // path traversal is prevented with ID_MATCHER public Path getPath(String repositoryId) { + // avoid path traversal attacks + checkArgument(ID_MATCHER.matchesNoneOf(repositoryId), "repository id contains invalid characters"); return Paths.get(DEFAULT_REPOSITORY_PATH, repositoryId); } diff --git a/scm-core/src/test/java/sonia/scm/repository/InitialRepositoryLocationResolverTest.java b/scm-core/src/test/java/sonia/scm/repository/InitialRepositoryLocationResolverTest.java index 9af00ce183..e4cd22b060 100644 --- a/scm-core/src/test/java/sonia/scm/repository/InitialRepositoryLocationResolverTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/InitialRepositoryLocationResolverTest.java @@ -1,5 +1,6 @@ package sonia.scm.repository; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -12,12 +13,33 @@ import static org.assertj.core.api.Assertions.assertThat; @ExtendWith({MockitoExtension.class}) class InitialRepositoryLocationResolverTest { + private InitialRepositoryLocationResolver resolver = new InitialRepositoryLocationResolver(); + @Test void shouldComputeInitialPath() { - InitialRepositoryLocationResolver resolver = new InitialRepositoryLocationResolver(); Path path = resolver.getPath("42"); assertThat(path).isRelative(); assertThat(path.toString()).isEqualTo("repositories" + File.separator + "42"); } + + @Test + void shouldThrowIllegalArgumentExceptionIfIdHasASlash() { + Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath("../../../passwd")); + } + + @Test + void shouldThrowIllegalArgumentExceptionIfIdHasABackSlash() { + Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath("..\\..\\..\\users.ntlm")); + } + + @Test + void shouldThrowIllegalArgumentExceptionIfIdIsDotDot() { + Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath("..")); + } + + @Test + void shouldThrowIllegalArgumentExceptionIfIdIsDot() { + Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath(".")); + } } diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java index bddcdec570..70698aed59 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathDatabase.java @@ -61,7 +61,8 @@ class PathDatabase { private void ensureParentDirectoryExists() { Path parent = storePath.getParent(); - if (!Files.exists(parent)) { + // Files.exists is slow on java 8 + if (!parent.toFile().exists()) { try { Files.createDirectories(parent); } catch (IOException ex) { diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java index de51ebdef7..4987b269da 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java @@ -47,12 +47,11 @@ import sonia.scm.store.StoreConstants; import javax.inject.Inject; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.time.Clock; import java.util.Collection; -import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * @author Sebastian Sdorra @@ -69,49 +68,52 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO { private final InitialRepositoryLocationResolver locationResolver; private final FileSystem fileSystem; - @VisibleForTesting - Clock clock = Clock.systemUTC(); + private final Map pathById; + private final Map byId; + private final Map byNamespaceAndName; + + private final Clock clock; private Long creationTime; private Long lastModified; - private Map pathById; - private Map byId; - private Map byNamespaceAndName; - @Inject public XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem) { + this(context, locationResolver, fileSystem, Clock.systemUTC()); + } + + XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem, Clock clock) { this.context = context; this.locationResolver = locationResolver; this.fileSystem = fileSystem; + this.clock = clock; this.creationTime = clock.millis(); - this.pathById = new LinkedHashMap<>(); - this.byId = new LinkedHashMap<>(); - this.byNamespaceAndName = new LinkedHashMap<>(); + this.pathById = new ConcurrentHashMap<>(); + this.byId = new ConcurrentHashMap<>(); + this.byNamespaceAndName = new ConcurrentHashMap<>(); - pathDatabase = new PathDatabase(createStorePath()); + pathDatabase = new PathDatabase(resolveStorePath()); read(); } private void read() { - Path storePath = createStorePath(); + Path storePath = resolveStorePath(); - if (!Files.exists(storePath)) { - return; + // Files.exists is slow on java 8 + if (storePath.toFile().exists()) { + pathDatabase.read(this::onLoadDates, this::onLoadRepository); } - - pathDatabase.read(this::loadDates, this::loadRepository); } - private void loadDates(Long creationTime, Long lastModified) { + private void onLoadDates(Long creationTime, Long lastModified) { this.creationTime = creationTime; this.lastModified = lastModified; } - private void loadRepository(String id, Path repositoryPath) { - Path metadataPath = createMetadataPath(context.resolve(repositoryPath)); + private void onLoadRepository(String id, Path repositoryPath) { + Path metadataPath = resolveMetadataPath(context.resolve(repositoryPath)); Repository repository = metadataStore.read(metadataPath); @@ -121,7 +123,7 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO { } @VisibleForTesting - Path createStorePath() { + Path resolveStorePath() { return context.getBaseDirectory() .toPath() .resolve(StoreConstants.CONFIG_DIRECTORY_NAME) @@ -130,7 +132,7 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO { @VisibleForTesting - Path createMetadataPath(Path repositoryPath) { + Path resolveMetadataPath(Path repositoryPath) { return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION)); } @@ -159,7 +161,7 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO { try { fileSystem.create(resolvedPath.toFile()); - Path metadataPath = createMetadataPath(resolvedPath); + Path metadataPath = resolveMetadataPath(resolvedPath); metadataStore.write(metadataPath, repository); synchronized (this) { @@ -227,7 +229,7 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO { } Path repositoryPath = context.resolve(getPath(repository.getId())); - Path metadataPath = createMetadataPath(repositoryPath); + Path metadataPath = resolveMetadataPath(repositoryPath); metadataStore.write(metadataPath, clone); } diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java b/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java index 30972210fb..d812eedc35 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java +++ b/scm-dao-xml/src/main/java/sonia/scm/xml/XmlStreams.java @@ -13,6 +13,7 @@ import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -44,7 +45,7 @@ public final class XmlStreams { } public static XMLStreamReader createReader(Path path) throws IOException, XMLStreamException { - return createReader(Files.newBufferedReader(path, Charsets.UTF_8)); + return createReader(Files.newBufferedReader(path, StandardCharsets.UTF_8)); } public static XMLStreamReader createReader(File file) throws IOException, XMLStreamException { @@ -57,7 +58,7 @@ public final class XmlStreams { public static IndentXMLStreamWriter createWriter(Path path) throws IOException, XMLStreamException { - return createWriter(Files.newBufferedWriter(path, Charsets.UTF_8)); + return createWriter(Files.newBufferedWriter(path, StandardCharsets.UTF_8)); } public static IndentXMLStreamWriter createWriter(File file) throws IOException, XMLStreamException { diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index 3e51d011d7..6330db56a0 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -67,11 +67,10 @@ class XmlRepositoryDAOTest { } private XmlRepositoryDAO createDAO() { - XmlRepositoryDAO dao = new XmlRepositoryDAO(context, locationResolver, fileSystem); - Clock clock = mock(Clock.class); when(clock.millis()).then(ic -> atomicClock.incrementAndGet()); - dao.clock = clock; + + XmlRepositoryDAO dao = new XmlRepositoryDAO(context, locationResolver, fileSystem, clock); return dao; } @@ -83,8 +82,8 @@ class XmlRepositoryDAOTest { @Test void shouldReturnCreationTimeAfterCreation() { - long now = System.currentTimeMillis(); - assertThat(dao.getCreationTime()).isBetween(now - 200, now + 200); + long now = atomicClock.get(); + assertThat(dao.getCreationTime()).isEqualTo(now); } @Test @@ -286,7 +285,7 @@ class XmlRepositoryDAOTest { Repository heartOfGold = createHeartOfGold(); dao.add(heartOfGold); - Path storePath = dao.createStorePath(); + Path storePath = dao.resolveStorePath(); assertThat(storePath).isRegularFile(); String content = content(storePath); @@ -305,7 +304,7 @@ class XmlRepositoryDAOTest { dao.add(heartOfGold); Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId()); - Path metadataPath = dao.createMetadataPath(repositoryDirectory); + Path metadataPath = dao.resolveMetadataPath(repositoryDirectory); assertThat(metadataPath).isRegularFile(); @@ -324,7 +323,7 @@ class XmlRepositoryDAOTest { dao.modify(heartOfGold); Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId()); - Path metadataPath = dao.createMetadataPath(repositoryDirectory); + Path metadataPath = dao.resolveMetadataPath(repositoryDirectory); String content = content(metadataPath); assertThat(content).contains("Awesome Spaceship"); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java index 533adbac82..c2c0439fc1 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgRepositoryHandler.java @@ -367,27 +367,6 @@ public class HgRepositoryHandler //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @param file - */ - private void createNewFile(File file) - { - try - { - if (!file.createNewFile() && logger.isErrorEnabled()) - { - logger.error("could not create file {}", file); - } - } - catch (IOException ex) - { - logger.error("could not create file {}".concat(file.getPath()), ex); - } - } - /** * Method description * diff --git a/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java b/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java index 5dbd672b98..82c6e24108 100644 --- a/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java +++ b/scm-test/src/main/java/sonia/scm/repository/RepositoryTestData.java @@ -33,6 +33,9 @@ package sonia.scm.repository; public final class RepositoryTestData { + public static final String NAMESPACE = "hitchhiker"; + public static final String MAIL_DOMAIN = "@hitchhiker.com"; + private RepositoryTestData() { } @@ -43,9 +46,9 @@ public final class RepositoryTestData { public static Repository create42Puzzle(String type) { return new RepositoryBuilder() .type(type) - .contact("douglas.adams@hitchhiker.com") + .contact("douglas.adams" + MAIL_DOMAIN) .name("42Puzzle") - .namespace("hitchhiker") + .namespace(NAMESPACE) .description("The 42 Puzzle") .build(); } @@ -58,9 +61,9 @@ public final class RepositoryTestData { public static Repository createHappyVerticalPeopleTransporter(String type) { return new RepositoryBuilder() .type(type) - .contact("zaphod.beeblebrox@hitchhiker.com") + .contact("zaphod.beeblebrox" + MAIL_DOMAIN) .name("happyVerticalPeopleTransporter") - .namespace("hitchhiker") + .namespace(NAMESPACE) .description("Happy Vertical People Transporter") .build(); } @@ -72,9 +75,9 @@ public final class RepositoryTestData { public static Repository createHeartOfGold(String type) { return new RepositoryBuilder() .type(type) - .contact("zaphod.beeblebrox@hitchhiker.com") + .contact("zaphod.beeblebrox" + MAIL_DOMAIN) .name("HeartOfGold") - .namespace("hitchhiker") + .namespace(NAMESPACE) .description( "Heart of Gold is the first prototype ship to successfully utilise the revolutionary Infinite Improbability Drive") .build(); @@ -88,9 +91,9 @@ public final class RepositoryTestData { public static Repository createRestaurantAtTheEndOfTheUniverse(String type) { return new RepositoryBuilder() .type(type) - .contact("douglas.adams@hitchhiker.com") + .contact("douglas.adams" + MAIL_DOMAIN) .name("RestaurantAtTheEndOfTheUniverse") - .namespace("hitchhiker") + .namespace(NAMESPACE) .description("The Restaurant at the End of the Universe") .build(); } diff --git a/scm-ui-components/packages/ui-components/src/StatePaginator.js b/scm-ui-components/packages/ui-components/src/StatePaginator.js new file mode 100644 index 0000000000..04f70ead52 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/StatePaginator.js @@ -0,0 +1,138 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { PagedCollection } from "@scm-manager/ui-types"; +import { Button } from "./index"; + +type Props = { + collection: PagedCollection, + page: number, + updatePage: number => void, + + // context props + t: string => string +}; + +class StatePaginator extends React.Component { + renderFirstButton() { + return ( +