diff --git a/gradle/changelog/unicode_name_validation.yaml b/gradle/changelog/unicode_name_validation.yaml new file mode 100644 index 0000000000..d495a14c2b --- /dev/null +++ b/gradle/changelog/unicode_name_validation.yaml @@ -0,0 +1,2 @@ +- type: changed + description: Allow all UTF-8 characters except URL identifiers as user and group names and for namespaces. ([#1600](https://github.com/scm-manager/scm-manager/pull/1600)) 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 fce742f4fa..5382d28a75 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -243,7 +243,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per */ @Override public boolean isValid() { - return ValidationUtil.isRepositoryNameValid(namespace) + return ValidationUtil.isNameValid(namespace) && ValidationUtil.isRepositoryNameValid(name) && Util.isNotEmpty(type) && ((Util.isEmpty(contact)) || ValidationUtil.isMailAddressValid(contact)); diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java b/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java index e3cd03a8cd..6693f29f52 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java @@ -21,11 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; import sonia.scm.repository.Repository; import sonia.scm.repository.api.ScmProtocol; +import sonia.scm.util.HttpUtil; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -51,7 +52,7 @@ public abstract class HttpScmProtocol implements ScmProtocol { @Override public String getUrl() { - return URI.create(basePath + "/").resolve(String.format("repo/%s/%s", repository.getNamespace(), repository.getName())).toASCIIString(); + return URI.create(basePath + "/").resolve(String.format("repo/%s/%s", HttpUtil.encode(repository.getNamespace()), HttpUtil.encode(repository.getName()))).toASCIIString(); } public final void serve(HttpServletRequest request, HttpServletResponse response, ServletConfig config) throws ServletException, IOException { 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 730a08c545..a06572c885 100644 --- a/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/ValidationUtil.java @@ -21,106 +21,69 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.util; -//~--- non-JDK imports -------------------------------------------------------- +package sonia.scm.util; import sonia.scm.Validateable; import java.util.regex.Pattern; -//~--- JDK imports ------------------------------------------------------------ +public final class ValidationUtil { -/** - * - * @author Sebastian Sdorra - */ -public final class ValidationUtil -{ + private static final String REGEX_MAIL = "^[A-Za-z0-9][\\w.-]*@[A-Za-z0-9][\\w\\-\\.]*\\.[A-Za-z0-9][A-Za-z0-9-]+$"; - /** Field description */ - private static final String REGEX_MAIL = - "^[A-Za-z0-9][\\w.-]*@[A-Za-z0-9][\\w\\-\\.]*\\.[A-Za-z0-9][A-Za-z0-9-]+$"; - - /** Field description */ - public static final String REGEX_NAME = - "^[A-Za-z0-9\\.\\-_][A-Za-z0-9\\.\\-_@]*$"; + public static final String REGEX_NAME = "^(?:(?:[^:/?#;&=\\s@%\\\\][^:/?#;&=%\\\\]*[^:/?#;&=\\s%\\\\])|(?:[^:/?#;&=\\s@%\\\\]))$"; public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])(?!.*[.]git$)^[A-Za-z0-9\\.][A-Za-z0-9\\.\\-_]*$"; - /** Field description */ private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME); - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - */ - private ValidationUtil() {} - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param value - * - * @return - */ - public static boolean isFilenameValid(String value) - { - return Util.isNotEmpty(value) && isNotContaining(value, "/", "\\", ":"); + private ValidationUtil() { } /** - * Method description + * Returns {@code true} if the filename is valid. * - * - * @param value - * - * @return + * @param filename filename to be validated + * @return {@code true} if filename is valid */ - public static boolean isMailAddressValid(String value) - { - return Util.isNotEmpty(value) && value.matches(REGEX_MAIL); + public static boolean isFilenameValid(String filename) { + return Util.isNotEmpty(filename) && isNotContaining(filename, "/", "\\", ":"); } /** - * Method description + * Returns {@code true} if the mail is valid. * - * - * @param name - * - * @return + * @param mail email-address to be validated + * @return {@code true} if mail is valid */ - public static boolean isNameValid(String name) - { - return Util.isNotEmpty(name) && name.matches(REGEX_NAME); + public static boolean isMailAddressValid(String mail) { + return Util.isNotEmpty(mail) && mail.matches(REGEX_MAIL); } /** - * Method description + * Returns {@code true} if the name is valid. * - * - * @param value - * @param notAllowedStrings - * - * @return + * @param name name to be validated + * @return {@code true} if name is valid */ - public static boolean isNotContaining(String value, - String... notAllowedStrings) - { + public static boolean isNameValid(String name) { + return Util.isNotEmpty(name) && name.matches(REGEX_NAME) && !name.equals(".."); + } + + /** + * Returns {@code true} if the object is valid. + * + * @param value value to be checked + * @param notAllowedStrings one or more strings which should not be included in value + * @return {@code true} if string has no not allowed strings else false + */ + public static boolean isNotContaining(String value, String... notAllowedStrings) { boolean result = Util.isNotEmpty(value); - if (result && (notAllowedStrings != null)) - { - for (String nas : notAllowedStrings) - { - if (value.indexOf(nas) >= 0) - { + if (result && (notAllowedStrings != null)) { + for (String nas : notAllowedStrings) { + if (value.contains(nas)) { result = false; break; @@ -135,24 +98,20 @@ public final class ValidationUtil * Returns {@code true} if the repository name is valid. * * @param name repository name - * @since 1.9 - * * @return {@code true} if repository name is valid + * @since 1.9 */ public static boolean isRepositoryNameValid(String name) { return PATTERN_REPOSITORYNAME.matcher(name).matches(); } /** - * Method description + * Returns {@code true} if the object is valid. * - * - * @param validateable - * - * @return + * @param validatable object to be validated + * @return {@code true} if object is valid */ - public static boolean isValid(Validateable validateable) - { - return (validateable != null) && validateable.isValid(); + public static boolean isValid(Validateable validatable) { + return (validatable != null) && validatable.isValid(); } } diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java index 4b6d94b670..57609b4785 100644 --- a/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java @@ -21,10 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository.spi; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import sonia.scm.repository.Repository; @@ -37,16 +40,44 @@ import static org.assertj.core.api.Assertions.assertThat; class HttpScmProtocolTest { - @TestFactory - Stream shouldCreateCorrectUrlsWithContextPath() { - return Stream.of("http://localhost/scm", "http://localhost/scm/") - .map(url -> assertResultingUrl(url, "http://localhost/scm/repo/space/name")); + private String namespace; + private String name; + + @Nested + class WithSimpleNamespaceAndName { + + @BeforeEach + void setNamespaceAndName() { + namespace = "space"; + name = "name"; + } + + @TestFactory + Stream shouldCreateCorrectUrlsWithContextPath() { + return Stream.of("http://localhost/scm", "http://localhost/scm/") + .map(url -> assertResultingUrl(url, "http://localhost/scm/repo/space/name")); + } + + @TestFactory + Stream shouldCreateCorrectUrlsWithPort() { + return Stream.of("http://localhost:8080", "http://localhost:8080/") + .map(url -> assertResultingUrl(url, "http://localhost:8080/repo/space/name")); + } } - @TestFactory - Stream shouldCreateCorrectUrlsWithPort() { - return Stream.of("http://localhost:8080", "http://localhost:8080/") - .map(url -> assertResultingUrl(url, "http://localhost:8080/repo/space/name")); + @Nested + class WithComplexNamespaceAndName{ + + @BeforeEach + void setNamespaceAndName() { + namespace = "name space"; + name = "name"; + } + + @Test + void shouldCreateCorrectUrlsWithContextPath() { + assertResultingUrl("http://localhost/scm", "http://localhost/scm/repo/name%20space/name"); + } } DynamicTest assertResultingUrl(String baseUrl, String expectedUrl) { @@ -55,7 +86,7 @@ class HttpScmProtocolTest { } private HttpScmProtocol createInstanceOfHttpScmProtocol(String baseUrl) { - return new HttpScmProtocol(new Repository("", "", "space", "name"), baseUrl) { + return new HttpScmProtocol(new Repository("", "", namespace, name), baseUrl) { @Override protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) { } diff --git a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java index a5f81a7150..e430ae334b 100644 --- a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java @@ -21,108 +21,119 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.util; -//~--- non-JDK imports -------------------------------------------------------- +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; -import org.junit.Test; - -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /** - * * @author Sebastian Sdorra */ -public class ValidationUtilTest -{ +class ValidationUtilTest { - /** - * Method description - * - */ - @Test - public void testIsFilenameValid() - { - - // true - assertTrue(ValidationUtil.isFilenameValid("test")); - assertTrue(ValidationUtil.isFilenameValid("test 123")); - - // false - assertFalse(ValidationUtil.isFilenameValid("../../")); - assertFalse(ValidationUtil.isFilenameValid("test/../..")); - assertFalse(ValidationUtil.isFilenameValid("\\ka")); - assertFalse(ValidationUtil.isFilenameValid("ka:on")); + @ParameterizedTest + @ValueSource(strings = { + "test", + "test 123" + }) + void shouldAcceptFilename(String value) { + assertTrue(ValidationUtil.isFilenameValid(value)); } - /** - * Method description - * - */ - @Test - public void testIsMailAddressValid() - { - - // true - assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@ostfalia.de")); - assertTrue(ValidationUtil.isMailAddressValid("sdorra@ostfalia.de")); - assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@hbk-bs.de")); - assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@gmail.com")); - assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@t.co")); - assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@ucla.college")); - assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@example.xn--p1ai")); - - // issue 909 - assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@scm.solutions")); - - // false - assertFalse(ValidationUtil.isMailAddressValid("ostfalia.de")); - assertFalse(ValidationUtil.isMailAddressValid("@ostfalia.de")); - assertFalse(ValidationUtil.isMailAddressValid("s.sdorra@")); - assertFalse(ValidationUtil.isMailAddressValid("s.sdorra@ostfalia")); - assertFalse(ValidationUtil.isMailAddressValid("s.sdorra@@ostfalia.de")); - assertFalse(ValidationUtil.isMailAddressValid("s.sdorra@ ostfalia.de")); - assertFalse(ValidationUtil.isMailAddressValid("s.sdorra @ostfalia.de")); + @ParameterizedTest + @ValueSource(strings = { + "../../", + "test/../..", + "\\ka, \"ka:on\"" + }) + void shouldRejectFilename(String value) { + assertFalse(ValidationUtil.isFilenameValid(value)); } - /** - * Method description - * - */ - @Test - public void testIsNameValid() - { - - // true - assertTrue(ValidationUtil.isNameValid("test")); - assertTrue(ValidationUtil.isNameValid("test.git")); - assertTrue(ValidationUtil.isNameValid("Test123.git")); - assertTrue(ValidationUtil.isNameValid("Test123-git")); - assertTrue(ValidationUtil.isNameValid("Test_user-123.git")); - assertTrue(ValidationUtil.isNameValid("test@scm-manager.de")); - assertTrue(ValidationUtil.isNameValid("t")); - - // false - assertFalse(ValidationUtil.isNameValid("test 123")); - assertFalse(ValidationUtil.isNameValid(" test 123")); - assertFalse(ValidationUtil.isNameValid(" test 123 ")); - assertFalse(ValidationUtil.isNameValid("test 123 ")); - assertFalse(ValidationUtil.isNameValid("test/123")); - assertFalse(ValidationUtil.isNameValid("test%123")); - assertFalse(ValidationUtil.isNameValid("test:123")); - assertFalse(ValidationUtil.isNameValid("t ")); - assertFalse(ValidationUtil.isNameValid(" t")); - assertFalse(ValidationUtil.isNameValid(" t ")); + @ParameterizedTest + @ValueSource(strings = { + "s.sdorra@ostfalia.de", + "sdorra@ostfalia.de", + "s.sdorra@hbk-bs.de", + "s.sdorra@gmail.com", + "s.sdorra@t.co", + "s.sdorra@ucla.college", + "s.sdorra@example.xn--p1ai", + "s.sdorra@scm.solutions" // issue 909 + }) + void shouldAcceptMailAddress(String value) { + assertTrue(ValidationUtil.isMailAddressValid(value)); + } + + @ParameterizedTest + @ValueSource(strings = { + "ostfalia.de", + "@ostfalia.de", + "s.sdorra@", + "s.sdorra@ostfalia", + "s.sdorra@@ostfalia.de", + "s.sdorra@ ostfalia.de", + "s.sdorra @ostfalia.de" + }) + void shouldRejectMailAddress(String value) { + assertFalse(ValidationUtil.isMailAddressValid(value)); + } + + @ParameterizedTest + @ValueSource(strings = { + "test", + "test.git", + "Test123.git", + "Test123-git", + "Test_user-123.git", + "test@scm-manager.de", + "t", + "Лорем-ипсум", + "Λορεμ.ιπσθμ", + "լոռեմիպսում", + "ლორემიფსუმ", + "प्रमान", + "詳性約", + "隠サレニ", + "법률", + "المدن", + "אחד", + "Hu-rëm" + }) + void shouldAcceptName(String value) { + assertTrue(ValidationUtil.isNameValid(value)); + } + + @ParameterizedTest + @ValueSource(strings = { + "@", + "@test", + " test123", + "test/123", + "test:123", + "test#123", + "test%123", + "test&123", + "test?123", + "test=123", + "test;123", + "@test123", + "t ", + " t", + " t ", + ".." + }) + void shouldRejectName(String value) { + assertFalse(ValidationUtil.isNameValid(value)); } - /** - * Method description - * - */ @Test - public void testIsNotContaining() - { + void testIsNotContaining() { // true assertTrue(ValidationUtil.isNotContaining("test", "abc")); @@ -134,9 +145,8 @@ public class ValidationUtilTest assertFalse(ValidationUtil.isNotContaining("test", "t")); } - @Test - public void testIsRepositoryNameValid() { - String[] validPaths = { + @ParameterizedTest + @ValueSource(strings = { "scm", "scm-", "scm_", @@ -150,69 +160,66 @@ public class ValidationUtilTest "..c", "d..", "a..c" - }; + }) + void shouldAcceptRepositoryName(String path) { + assertTrue(ValidationUtil.isRepositoryNameValid(path)); + } - // issue 142, 144 and 148 - String[] 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", - "-scm", - "scm.git", - "scm.git.git" - }; - - for (String path : validPaths) { - assertTrue(ValidationUtil.isRepositoryNameValid(path)); - } - - for (String path : invalidPaths) { - assertFalse(ValidationUtil.isRepositoryNameValid(path)); - } + @ParameterizedTest + @ValueSource(strings = { + ".", + "/", + "//", + "..", + "/.", + "/..", + "./", + "../", + "/../", + "/./", + "/...", + "/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", + "-scm", + "scm.git", + "scm.git.git" + }) + void shouldRejectRepositoryName(String path) { + assertFalse(ValidationUtil.isRepositoryNameValid(path)); } } diff --git a/scm-core/src/test/java/sonia/scm/util/ValidationUtil_IllegalCharactersTest.java b/scm-core/src/test/java/sonia/scm/util/ValidationUtil_IllegalCharactersTest.java deleted file mode 100644 index a36e157e7b..0000000000 --- a/scm-core/src/test/java/sonia/scm/util/ValidationUtil_IllegalCharactersTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.util; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.util.Collection; -import java.util.List; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -import static java.util.Arrays.asList; -import static org.junit.Assert.assertFalse; -import static sonia.scm.util.ValidationUtil.REGEX_NAME; - -@RunWith(Parameterized.class) -public class ValidationUtil_IllegalCharactersTest { - - private static final List ACCEPTED_CHARS = asList('@', '_', '-', '.'); - - private final Pattern userGroupPattern=Pattern.compile(REGEX_NAME); - - private final String expression; - - public ValidationUtil_IllegalCharactersTest(String expression) { - this.expression = expression; - } - - @Parameterized.Parameters(name = "{0}") - public static Collection createParameters() { - return Stream.concat(IntStream.range(0x20, 0x2f).mapToObj(i -> (char) i), // chars before '0' - Stream.concat(IntStream.range(0x3a, 0x40).mapToObj(i -> (char) i), // chars between '9' and 'A' - Stream.concat(IntStream.range(0x5b, 0x60).mapToObj(i -> (char) i), // chars between 'Z' and 'a' - IntStream.range(0x7b, 0xff).mapToObj(i -> (char) i)))) // chars after 'z' - .filter(c -> !ACCEPTED_CHARS.contains(c)) - .flatMap(c -> Stream.of("abc" + c + "xyz", "@" + c, c + "tail")) - .map(c -> new String[] {c}) - .collect(Collectors.toList()); - } - - @Test - public void shouldNotAcceptSpecialCharacters() { - assertFalse(userGroupPattern.matcher(expression).matches()); - } -} diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 699095fe05..1452832896 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -46426,7 +46426,6 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Default 1`] = ` value="" /> -
-