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 e94fabfa60..5f4c7cf3d3 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -184,8 +184,8 @@ public class ScmConfiguration implements Configuration { @XmlElement(name = "xsrf-protection") private boolean enabledXsrfProtection = true; - @XmlElement(name = "default-namespace-strategy") - private String defaultNamespaceStrategy = "sonia.scm.repository.DefaultNamespaceStrategy"; + @XmlElement(name = "namespace-strategy") + private String namespaceStrategy = "UsernameNamespaceStrategy"; /** @@ -227,7 +227,7 @@ public class ScmConfiguration implements Configuration { this.loginAttemptLimit = other.loginAttemptLimit; this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout; this.enabledXsrfProtection = other.enabledXsrfProtection; - this.defaultNamespaceStrategy = other.defaultNamespaceStrategy; + this.namespaceStrategy = other.namespaceStrategy; } public Set getAdminGroups() { @@ -366,8 +366,8 @@ public class ScmConfiguration implements Configuration { return loginAttemptLimit > 0; } - public String getDefaultNamespaceStrategy() { - return defaultNamespaceStrategy; + public String getNamespaceStrategy() { + return namespaceStrategy; } @@ -501,8 +501,8 @@ public class ScmConfiguration implements Configuration { this.enabledXsrfProtection = enabledXsrfProtection; } - public void setDefaultNamespaceStrategy(String defaultNamespaceStrategy) { - this.defaultNamespaceStrategy = defaultNamespaceStrategy; + public void setNamespaceStrategy(String namespaceStrategy) { + this.namespaceStrategy = namespaceStrategy; } @Override 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. * @@ -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 09032e93e5..bc710d9a50 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 @@ -52,15 +50,16 @@ public final class ValidationUtil /** Field description */ private static final String REGEX_MAIL = - "^[A-z0-9][\\w.-]*@[A-z0-9][\\w\\-\\.]*\\.[A-z0-9][A-z0-9-]+$"; + "^[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_NAME = - "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$"; + "^[A-Za-z0-9\\.\\-_@]|[^ ]([A-Za-z0-9\\.\\-_@ ]*[A-Za-z0-9\\.\\-_@]|[^ ])?$"; + + public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-Za-z0-9\\.][A-Za-z0-9\\.\\-_]*$"; /** Field description */ - private static final String REGEX_REPOSITORYNAME = - "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_/]*$"; + private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME); //~--- constructors --------------------------------------------------------- @@ -142,37 +141,15 @@ public final class ValidationUtil } /** - * Method description + * Returns {@code true} if the repository name is valid. * - * - * @param name + * @param name repository name * @since 1.9 * - * @return + * @return {@code true} if repository name is valid */ - public static boolean isRepositoryNameValid(String name) - { - Pattern pattern = Pattern.compile(REGEX_REPOSITORYNAME); - boolean result = true; - - if (Util.isNotEmpty(name)) - { - for (String p : Splitter.on('/').split(name)) - { - if (!pattern.matcher(p).matches()) - { - result = false; - - break; - } - } - } - else - { - result = false; - } - - return result; + public static boolean isRepositoryNameValid(String name) { + return PATTERN_REPOSITORYNAME.matcher(name).matches(); } /** diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 19859b876b..d0a64b17ec 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -46,6 +46,8 @@ public class VndMediaType { public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX; public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX; + public static final String NAMESPACE_STRATEGIES = PREFIX + "namespaceStrategies" + SUFFIX; + public static final String ME = PREFIX + "me" + SUFFIX; public static final String SOURCE = PREFIX + "source" + SUFFIX; public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX; 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 3eaddf2d36..e62f208e58 100644 --- a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java @@ -143,51 +143,21 @@ public class ValidationUtilTest assertFalse(ValidationUtil.isNotContaining("test", "t")); } - /** - * Method description - * - */ @Test - public void testIsRepositoryNameValid() - { - assertTrue(ValidationUtil.isRepositoryNameValid("scm")); - assertTrue(ValidationUtil.isRepositoryNameValid("scm/main")); - assertTrue(ValidationUtil.isRepositoryNameValid("scm/plugins/git-plugin")); - assertTrue(ValidationUtil.isRepositoryNameValid("s")); - assertTrue(ValidationUtil.isRepositoryNameValid("sc")); - assertTrue(ValidationUtil.isRepositoryNameValid(".scm/plugins")); - - // issue 142 - assertFalse(ValidationUtil.isRepositoryNameValid(".")); - assertFalse(ValidationUtil.isRepositoryNameValid("/")); - assertFalse(ValidationUtil.isRepositoryNameValid("scm/plugins/.")); - assertFalse(ValidationUtil.isRepositoryNameValid("scm/../plugins")); - assertFalse(ValidationUtil.isRepositoryNameValid("scm/main/")); - assertFalse(ValidationUtil.isRepositoryNameValid("/scm/main/")); - - // issue 144 - assertFalse(ValidationUtil.isRepositoryNameValid("scm/./main")); - assertFalse(ValidationUtil.isRepositoryNameValid("scm//main")); - - // issue 148 - //J- + public void testIsRepositoryNameValid() { String[] validPaths = { "scm", - "scm/main", - "scm/plugins/git-plugin", "s", "sc", - ".scm/plugins", ".hiddenrepo", "b.", "...", "..c", "d..", - "a/b..", - "a/..b", - "a..c", + "a..c" }; - + + // issue 142, 144 and 148 String[] invalidPaths = { ".", "/", @@ -228,17 +198,22 @@ public class ValidationUtilTest "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" }; - //J+ - for (String path : validPaths) - { + for (String path : validPaths) { assertTrue(ValidationUtil.isRepositoryNameValid(path)); } - for (String path : invalidPaths) - { + for (String path : invalidPaths) { assertFalse(ValidationUtil.isRepositoryNameValid(path)); } } diff --git a/scm-ui-components/packages/ui-types/src/Config.js b/scm-ui-components/packages/ui-types/src/Config.js index 916cf9f509..4eee3e47d5 100644 --- a/scm-ui-components/packages/ui-types/src/Config.js +++ b/scm-ui-components/packages/ui-types/src/Config.js @@ -22,6 +22,6 @@ export type Config = { pluginUrl: string, loginAttemptLimitTimeout: number, enabledXsrfProtection: boolean, - defaultNamespaceStrategy: string, + namespaceStrategy: string, _links: Links }; diff --git a/scm-ui-components/packages/ui-types/src/NamespaceStrategies.js b/scm-ui-components/packages/ui-types/src/NamespaceStrategies.js new file mode 100644 index 0000000000..ec53e6a7db --- /dev/null +++ b/scm-ui-components/packages/ui-types/src/NamespaceStrategies.js @@ -0,0 +1,9 @@ +// @flow + +import type { Links } from "./hal"; + +export type NamespaceStrategies = { + current: string, + available: string[], + _links: Links +}; diff --git a/scm-ui-components/packages/ui-types/src/index.js b/scm-ui-components/packages/ui-types/src/index.js index f7b375ac98..02e88f12e1 100644 --- a/scm-ui-components/packages/ui-types/src/index.js +++ b/scm-ui-components/packages/ui-types/src/index.js @@ -26,3 +26,5 @@ export type { SubRepository, File } from "./Sources"; export type { SelectValue, AutocompleteObject } from "./Autocomplete"; export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions"; + +export type { NamespaceStrategies } from "./NamespaceStrategies"; diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index b67bf90262..fd19e83c65 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -57,7 +57,7 @@ "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", "plugin-url": "Plugin URL", "enabled-xsrf-protection": "XSRF Protection aktivieren", - "default-namespace-strategy": "Default Namespace Strategie" + "namespace-strategy": "Namespace Strategie" }, "validation": { "date-format-invalid": "Das Datumsformat ist ungültig", @@ -87,6 +87,6 @@ "proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.", "proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.", "enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.", - "defaultNameSpaceStrategyHelpText": "Die Standardstrategie für Namespaces." + "nameSpaceStrategyHelpText": "Strategie für Namespaces." } } diff --git a/scm-ui/public/locales/de/repos.json b/scm-ui/public/locales/de/repos.json index 922a2b270f..141d58d1d2 100644 --- a/scm-ui/public/locales/de/repos.json +++ b/scm-ui/public/locales/de/repos.json @@ -1,5 +1,6 @@ { "repository": { + "namespace": "Namespace", "name": "Name", "type": "Typ", "contact": "Kontakt", @@ -8,10 +9,12 @@ "lastModified": "Zuletzt bearbeitet" }, "validation": { + "namespace-invalid": "Der Namespace des Repository ist ungültig", "name-invalid": "Der Name des Repository ist ungültig", "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein" }, "help": { + "namespaceHelpText": "Der Namespace des Repository. Dieser wird Teil der URL des Repository sein.", "nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.", "typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).", "contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.", diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 1b42878015..61670f0b2c 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -57,7 +57,7 @@ "skip-failed-authenticators": "Skip Failed Authenticators", "plugin-url": "Plugin URL", "enabled-xsrf-protection": "Enabled XSRF Protection", - "default-namespace-strategy": "Default Namespace Strategy" + "namespace-strategy": "Namespace Strategy" }, "validation": { "date-format-invalid": "The date format is not valid", @@ -87,6 +87,6 @@ "proxyUserHelpText": "The username for the proxy server authentication.", "proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.", "enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.", - "defaultNameSpaceStrategyHelpText": "The default namespace strategy." + "nameSpaceStrategyHelpText": "The namespace strategy." } } diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index ebc0adfd34..f10c771d96 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -1,5 +1,6 @@ { "repository": { + "namespace": "Namespace", "name": "Name", "type": "Type", "contact": "Contact", @@ -8,10 +9,12 @@ "lastModified": "Last Modified" }, "validation": { + "namespace-invalid": "The repository namespace is invalid", "name-invalid": "The repository name is invalid", "contact-invalid": "Contact must be a valid mail address" }, "help": { + "namespaceHelpText": "The namespace of the repository. This name will be part of the repository url.", "nameHelpText": "The name of the repository. This name will be part of the repository url.", "typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).", "contactHelpText": "Email address of the person who is responsible for this repository.", diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index 7b650ccbfd..1fda491c2e 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -2,7 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; import { SubmitButton, Notification } from "@scm-manager/ui-components"; -import type { Config } from "@scm-manager/ui-types"; +import type { NamespaceStrategies, Config } from "@scm-manager/ui-types"; import ProxySettings from "./ProxySettings"; import GeneralSettings from "./GeneralSettings"; import BaseUrlSettings from "./BaseUrlSettings"; @@ -13,9 +13,11 @@ type Props = { submitForm: Config => void, config?: Config, loading?: boolean, - t: string => string, configReadPermission: boolean, - configUpdatePermission: boolean + configUpdatePermission: boolean, + namespaceStrategies?: NamespaceStrategies, + // context props + t: string => string, }; type State = { @@ -54,7 +56,7 @@ class ConfigForm extends React.Component { pluginUrl: "", loginAttemptLimitTimeout: 0, enabledXsrfProtection: true, - defaultNamespaceStrategy: "", + namespaceStrategy: "", _links: {} }, showNotification: false, @@ -88,6 +90,7 @@ class ConfigForm extends React.Component { const { loading, t, + namespaceStrategies, configReadPermission, configUpdatePermission } = this.props; @@ -118,6 +121,7 @@ class ConfigForm extends React.Component {
{noPermissionNotification} { skipFailedAuthenticators={config.skipFailedAuthenticators} pluginUrl={config.pluginUrl} enabledXsrfProtection={config.enabledXsrfProtection} - defaultNamespaceStrategy={config.defaultNamespaceStrategy} + namespaceStrategy={config.namespaceStrategy} onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name) } diff --git a/scm-ui/src/config/components/form/GeneralSettings.js b/scm-ui/src/config/components/form/GeneralSettings.js index d2f891fd06..b221eb4d6a 100644 --- a/scm-ui/src/config/components/form/GeneralSettings.js +++ b/scm-ui/src/config/components/form/GeneralSettings.js @@ -1,7 +1,9 @@ // @flow import React from "react"; import { translate } from "react-i18next"; -import { Checkbox, InputField } from "@scm-manager/ui-components"; +import { Checkbox, InputField} from "@scm-manager/ui-components"; +import type { NamespaceStrategies } from "@scm-manager/ui-types"; +import NamespaceStrategySelect from "./NamespaceStrategySelect"; type Props = { realmDescription: string, @@ -12,13 +14,16 @@ type Props = { skipFailedAuthenticators: boolean, pluginUrl: string, enabledXsrfProtection: boolean, - defaultNamespaceStrategy: string, - t: string => string, + namespaceStrategy: string, + namespaceStrategies?: NamespaceStrategies, onChange: (boolean, any, string) => void, - hasUpdatePermission: boolean + hasUpdatePermission: boolean, + // context props + t: string => string }; class GeneralSettings extends React.Component { + render() { const { t, @@ -30,8 +35,9 @@ class GeneralSettings extends React.Component { skipFailedAuthenticators, pluginUrl, enabledXsrfProtection, - defaultNamespaceStrategy, - hasUpdatePermission + namespaceStrategy, + hasUpdatePermission, + namespaceStrategies } = this.props; return ( @@ -67,13 +73,14 @@ class GeneralSettings extends React.Component { />
- +
@@ -146,19 +153,17 @@ class GeneralSettings extends React.Component { handleAnonymousAccessEnabledChange = (value: string) => { this.props.onChange(true, value, "anonymousAccessEnabled"); }; - handleSkipFailedAuthenticatorsChange = (value: string) => { this.props.onChange(true, value, "skipFailedAuthenticators"); }; handlePluginUrlChange = (value: string) => { this.props.onChange(true, value, "pluginUrl"); }; - handleEnabledXsrfProtectionChange = (value: boolean) => { this.props.onChange(true, value, "enabledXsrfProtection"); }; - handleDefaultNamespaceStrategyChange = (value: string) => { - this.props.onChange(true, value, "defaultNamespaceStrategy"); + handleNamespaceStrategyChange = (value: string) => { + this.props.onChange(true, value, "namespaceStrategy"); }; } diff --git a/scm-ui/src/config/components/form/NamespaceStrategySelect.js b/scm-ui/src/config/components/form/NamespaceStrategySelect.js new file mode 100644 index 0000000000..71f54de247 --- /dev/null +++ b/scm-ui/src/config/components/form/NamespaceStrategySelect.js @@ -0,0 +1,63 @@ +//@flow +import React from "react"; +import { translate, type TFunction } from "react-i18next"; +import { Select } from "@scm-manager/ui-components"; +import type { NamespaceStrategies } from "@scm-manager/ui-types"; + +type Props = { + namespaceStrategies: NamespaceStrategies, + label: string, + value?: string, + disabled?: boolean, + helpText?: string, + onChange: (value: string, name?: string) => void, + // context props + t: TFunction +}; + +class NamespaceStrategySelect extends React.Component { + createNamespaceOptions = () => { + const { namespaceStrategies, t } = this.props; + let available = []; + if (namespaceStrategies && namespaceStrategies.available) { + available = namespaceStrategies.available; + } + + return available.map(ns => { + const key = "namespaceStrategies." + ns; + let label = t(key); + if (label === key) { + label = ns; + } + return { + value: ns, + label: label + }; + }); + }; + + findSelected = () => { + const { namespaceStrategies, value } = this.props; + if (namespaceStrategies.available.indexOf(value) < 0) { + return namespaceStrategies.current; + } + return value; + }; + + render() { + const { label, helpText, disabled, onChange } = this.props; + const nsOptions = this.createNamespaceOptions(); + return ( +