diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java
index 665f63487d..5bb50db06f 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Repository.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java
@@ -248,7 +248,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
/**
* Returns true if the {@link Repository} is valid.
*
- * - The name is not empty and contains only A-z, 0-9, _, -, /
+ * - The namespace is valid
+ * - The name is valid
* - The type is not empty
* - The contact is empty or contains a valid email address
*
@@ -257,9 +258,10 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
*/
@Override
public boolean isValid() {
- return ValidationUtil.isRepositoryNameValid(name) && Util.isNotEmpty(type)
- && ((Util.isEmpty(contact))
- || ValidationUtil.isMailAddressValid(contact));
+ return ValidationUtil.isRepositoryNameValid(namespace)
+ && ValidationUtil.isRepositoryNameValid(name)
+ && Util.isNotEmpty(type)
+ && ((Util.isEmpty(contact)) || ValidationUtil.isMailAddressValid(contact));
}
/**
diff --git a/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java b/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java
index 2dbf55287d..e354b7efc6 100644
--- a/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java
+++ b/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java
@@ -35,14 +35,12 @@ package sonia.scm.util;
//~--- non-JDK imports --------------------------------------------------------
-import com.google.common.base.Splitter;
-
import sonia.scm.Validateable;
-//~--- JDK imports ------------------------------------------------------------
-
import java.util.regex.Pattern;
+//~--- JDK imports ------------------------------------------------------------
+
/**
*
* @author Sebastian Sdorra
@@ -58,10 +56,10 @@ public final class ValidationUtil
private static final String REGEX_NAME =
"^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$";
+ public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_]*$";
+
/** Field description */
- private static final Pattern REGEX_REPOSITORYNAME = Pattern.compile(
- "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_]*$"
- );
+ private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME);
//~--- constructors ---------------------------------------------------------
@@ -151,7 +149,7 @@ public final class ValidationUtil
* @return {@code true} if repository name is valid
*/
public static boolean isRepositoryNameValid(String name) {
- return REGEX_REPOSITORYNAME.matcher(name).matches();
+ return PATTERN_REPOSITORYNAME.matcher(name).matches();
}
/**
diff --git a/scm-ui/src/repos/components/form/repositoryValidation.js b/scm-ui/src/repos/components/form/repositoryValidation.js
index c7f6fdb8b2..cdd180ad47 100644
--- a/scm-ui/src/repos/components/form/repositoryValidation.js
+++ b/scm-ui/src/repos/components/form/repositoryValidation.js
@@ -1,8 +1,10 @@
// @flow
import { validation } from "@scm-manager/ui-components";
+const nameRegex = /(?!^\.\.$)(?!^\.$)(?!.*[\\\[\]])^[A-z0-9\.][A-z0-9\.\-_]*$/;
+
export const isNameValid = (name: string) => {
- return validation.isNameValid(name);
+ return nameRegex.test(name);
};
export function isContactValid(mail: string) {
diff --git a/scm-ui/src/repos/components/form/repositoryValidation.test.js b/scm-ui/src/repos/components/form/repositoryValidation.test.js
index bcb29f3ef7..f0b524f2a9 100644
--- a/scm-ui/src/repos/components/form/repositoryValidation.test.js
+++ b/scm-ui/src/repos/components/form/repositoryValidation.test.js
@@ -11,6 +11,81 @@ describe("repository name validation", () => {
expect(validator.isNameValid("scm/manager")).toBe(false);
expect(validator.isNameValid("scm/ma/nager")).toBe(false);
});
+
+ it("should allow same names as the backend", () => {
+ const validPaths = [
+ "scm",
+ "s",
+ "sc",
+ ".hiddenrepo",
+ "b.",
+ "...",
+ "..c",
+ "d..",
+ "a..c"
+ ];
+
+ validPaths.forEach((path) =>
+ expect(validator.isNameValid(path)).toBe(true)
+ );
+ });
+
+ it("should deny same names as the backend", () => {
+ const invalidPaths = [
+ ".",
+ "/",
+ "//",
+ "..",
+ "/.",
+ "/..",
+ "./",
+ "../",
+ "/../",
+ "/./",
+ "/...",
+ "/abc",
+ ".../",
+ "/sdf/",
+ "asdf/",
+ "./b",
+ "scm/plugins/.",
+ "scm/../plugins",
+ "scm/main/",
+ "/scm/main/",
+ "scm/./main",
+ "scm//main",
+ "scm\\main",
+ "scm/main-$HOME",
+ "scm/main-${HOME}-home",
+ "scm/main-%HOME-home",
+ "scm/main-%HOME%-home",
+ "abc$abc",
+ "abc%abc",
+ "abcabc",
+ "abc#abc",
+ "abc+abc",
+ "abc{abc",
+ "abc}abc",
+ "abc(abc",
+ "abc)abc",
+ "abc[abc",
+ "abc]abc",
+ "abc|abc",
+ "scm/main",
+ "scm/plugins/git-plugin",
+ ".scm/plugins",
+ "a/b..",
+ "a/..b",
+ "scm/main",
+ "scm/plugins/git-plugin",
+ "scm/plugins/git-plugin"
+ ];
+
+ invalidPaths.forEach((path) =>
+ expect(validator.isNameValid(path)).toBe(false)
+ );
+ });
});
describe("repository contact validation", () => {
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java
index 8b48311bba..f3f28191ae 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java
@@ -9,6 +9,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
+import sonia.scm.util.ValidationUtil;
import javax.validation.constraints.Pattern;
import java.time.Instant;
@@ -25,8 +26,9 @@ public class RepositoryDto extends HalRepresentation {
private List healthCheckFailures;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Instant lastModified;
+ // we could not validate the namespace, this must be done by the namespace strategy
private String namespace;
- @Pattern(regexp = "^[A-z0-9\\-_]+$")
+ @Pattern(regexp = ValidationUtil.REGEX_REPOSITORYNAME)
private String name;
private boolean archived = false;
@NotEmpty