Fix routing for entity names with parenthesis (#1998)

If entities like users, groups or repository namespaces contains parenthesis the frontend router gets confused and doesn't work properly. To fix this issue we escape the chars in the url which may cause such problems because they are reserved by the http url schema.

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
Co-authored-by: Florian Scholdei <florian.scholdei@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2022-04-13 13:06:02 +02:00
committed by GitHub
parent 8ce0ab7b94
commit c8207d89da
13 changed files with 111 additions and 66 deletions

View File

@@ -0,0 +1,2 @@
- type: fixed
description: Escape parenthesis for entity names to fix routing ([#1998](https://github.com/scm-manager/scm-manager/pull/1998))

View File

@@ -108,3 +108,11 @@ export function matchedUrl(props: any) {
const match = props.match;
return matchedUrlFromMatch(match);
}
export function escapeUrlForRoute(url: string) {
return url.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
export function unescapeUrlForRoute(url: string) {
return url.replace(/\\/g, "");
}

View File

@@ -27,6 +27,7 @@ import { NavLink } from "../navigation";
import { Route } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { Repository, Links, Link } from "@scm-manager/ui-types";
import { urls } from "@scm-manager/ui-api";
type GlobalRouteProps = {
url: string;
@@ -49,7 +50,7 @@ class ConfigurationBinder {
route(path: string, Component: any) {
return (
<Route path={path} exact>
<Route path={urls.escapeUrlForRoute(path)} exact>
{Component}
</Route>
);
@@ -135,7 +136,7 @@ class ConfigurationBinder {
const link = repository._links[linkName];
if (link) {
return this.route(
url + "/settings" + to,
urls.unescapeUrlForRoute(url) + "/settings" + to,
<RepositoryComponent repository={repository} link={(link as Link).href} {...additionalProps} />
);
}

View File

@@ -22,6 +22,7 @@
* SOFTWARE.
*/
import { urls } from "@scm-manager/ui-api";
import { useLocation, useRouteMatch } from "react-router-dom";
import { RoutingProps } from "./RoutingProps";
@@ -33,8 +34,8 @@ const useActiveMatch = ({ to, activeOnlyWhenExact, activeWhenMatch }: RoutingPro
}
const match = useRouteMatch({
path,
exact: activeOnlyWhenExact,
path: urls.escapeUrlForRoute(path),
exact: activeOnlyWhenExact
});
const location = useLocation();
@@ -42,7 +43,7 @@ const useActiveMatch = ({ to, activeOnlyWhenExact, activeWhenMatch }: RoutingPro
const isActiveWhenMatch = () => {
if (activeWhenMatch) {
return activeWhenMatch({
location,
location
});
}
return false;

View File

@@ -47,19 +47,20 @@ const SingleRepositoryRole: FC = () => {
}
const url = urls.matchedUrlFromMatch(match);
const escapedUrl = urls.escapeUrlForRoute(url);
const extensionProps = {
role,
url
url: escapedUrl
};
return (
<>
<Title title={t("repositoryRole.title")} />
<Route path={`${url}/info`}>
<Route path={`${escapedUrl}/info`}>
<PermissionRoleDetail role={role} url={url} />
</Route>
<Route path={`${url}/edit`} exact>
<Route path={`${escapedUrl}/edit`} exact>
<EditRepositoryRole role={role} />
</Route>
<ExtensionPoint<extensionPoints.RolesRoute> name="roles.route" props={extensionProps} renderAll={true} />

View File

@@ -59,6 +59,7 @@ const SingleGroup: FC = () => {
}
const url = urls.matchedUrlFromMatch(match);
const escapedUrl = urls.escapeUrlForRoute(url);
const extensionProps = {
group,
@@ -70,16 +71,23 @@ const SingleGroup: FC = () => {
<Page title={group.name}>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Route path={url} exact>
<Route path={escapedUrl} exact>
<Details group={group} />
</Route>
<Route path={`${url}/settings/general`} exact>
<Route path={`${escapedUrl}/settings/general`} exact>
<EditGroup group={group} />
</Route>
<Route path={`${url}/settings/permissions`} exact>
<Route path={`${escapedUrl}/settings/permissions`} exact>
<SetGroupPermissions group={group} />
</Route>
<ExtensionPoint<extensionPoints.GroupRoute> name="group.route" props={extensionProps} renderAll={true} />
<ExtensionPoint<extensionPoints.GroupRoute>
name="group.route"
props={{
group,
url: escapedUrl
}}
renderAll={true}
/>
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("singleGroup.menu.navigationLabel")}>

View File

@@ -61,10 +61,12 @@ const BranchRoot: FC<Props> = ({ repository }) => {
return null;
}
const escapedUrl = urls.escapeUrlForRoute(url);
return (
<Switch>
<Redirect exact from={url} to={`${url}/info`} />
<Route path={`${url}/info`}>
<Redirect exact from={escapedUrl} to={`${url}/info`} />
<Route path={`${escapedUrl}/info`}>
<BranchView repository={repository} branch={branch} />
</Route>
</Switch>

View File

@@ -24,7 +24,7 @@
import React, { FC, ReactNode } from "react";
import styled from "styled-components";
import { useLocation } from "react-router-dom";
import { BranchSelector, devices, Level } from "@scm-manager/ui-components";
import { BranchSelector, devices, Level, urls } from "@scm-manager/ui-components";
import CodeViewSwitcher, { SwitchViewLink } from "./CodeViewSwitcher";
import { useTranslation } from "react-i18next";
import { Branch } from "@scm-manager/ui-types";
@@ -87,7 +87,9 @@ const CodeActionBar: FC<Props> = ({ selectedBranch, branches, onSelectBranch, sw
)
}
children={actions}
right={<CodeViewSwitcher currentUrl={location.pathname} switchViewLink={switchViewLink} />}
right={
<CodeViewSwitcher currentUrl={urls.escapeUrlForRoute(location.pathname)} switchViewLink={switchViewLink} />
}
/>
</ActionBar>
);

View File

@@ -26,7 +26,7 @@ import { Route, useLocation } from "react-router-dom";
import Sources from "../../sources/containers/Sources";
import ChangesetsRoot from "../../containers/ChangesetsRoot";
import { Branch, Repository } from "@scm-manager/ui-types";
import { ErrorPage, Loading } from "@scm-manager/ui-components";
import { ErrorPage, Loading, urls } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { useBranches } from "@scm-manager/ui-api";
import FileSearch from "./FileSearch";
@@ -84,24 +84,28 @@ type RoutingProps = {
selectedBranch?: string;
};
const CodeRouting: FC<RoutingProps> = ({ repository, baseUrl, branches, selectedBranch }) => (
const CodeRouting: FC<RoutingProps> = ({ repository, baseUrl, branches, selectedBranch }) => {
const escapedUrl = urls.escapeUrlForRoute(baseUrl);
return (
<>
<Route path={`${baseUrl}/sources`} exact={true}>
<Route path={`${escapedUrl}/sources`} exact={true}>
<Sources repository={repository} baseUrl={baseUrl} branches={branches} />
</Route>
<Route path={`${baseUrl}/sources/:revision/:path*`}>
<Route path={`${escapedUrl}/sources/:revision/:path*`}>
<Sources repository={repository} baseUrl={baseUrl} branches={branches} selectedBranch={selectedBranch} />
</Route>
<Route path={`${baseUrl}/changesets`}>
<Route path={`${escapedUrl}/changesets`}>
<ChangesetsRoot repository={repository} baseUrl={baseUrl} branches={branches} />
</Route>
<Route path={`${baseUrl}/branch/:branch/changesets/`}>
<Route path={`${escapedUrl}/branch/:branch/changesets/`}>
<ChangesetsRoot repository={repository} baseUrl={baseUrl} branches={branches} selectedBranch={selectedBranch} />
</Route>
<Route path={`${baseUrl}/search/:revision/`}>
<Route path={`${escapedUrl}/search/:revision/`}>
<FileSearch repository={repository} baseUrl={baseUrl} branches={branches} selectedBranch={selectedBranch} />
</Route>
</>
);
};
export default CodeOverview;

View File

@@ -43,7 +43,7 @@ const ChangesetRoot: FC<Props> = ({ repository, baseUrl, branches, selectedBranc
return null;
}
const url = urls.stripEndingSlash(match.url);
const url = urls.stripEndingSlash(urls.escapeUrlForRoute(match.url));
const defaultBranch = branches?.find(b => b.defaultBranch === true);
const isBranchAvailable = () => {

View File

@@ -229,6 +229,8 @@ const RepositoryRoot = () => {
/>
);
const escapedUrl = urls.escapeUrlForRoute(url);
return (
<StateMenuContextProvider>
<Page
@@ -247,57 +249,64 @@ const RepositoryRoot = () => {
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Switch>
<Redirect exact from={match.url} to={redirectedUrl} />
<Redirect exact from={urls.escapeUrlForRoute(match.url)} to={urls.escapeUrlForRoute(redirectedUrl)} />
{/* redirect pre 2.0.0-rc2 links */}
<Redirect from={`${url}/changeset/:id`} to={`${url}/code/changeset/:id`} />
<Redirect exact from={`${url}/sources`} to={`${url}/code/sources`} />
<Redirect from={`${url}/sources/:revision/:path*`} to={`${url}/code/sources/:revision/:path*`} />
<Redirect exact from={`${url}/changesets`} to={`${url}/code/changesets`} />
<Redirect from={`${url}/branch/:branch/changesets`} to={`${url}/code/branch/:branch/changesets/`} />
<Redirect from={`${escapedUrl}/changeset/:id`} to={`${url}/code/changeset/:id`} />
<Redirect exact from={`${escapedUrl}/sources`} to={`${url}/code/sources`} />
<Redirect from={`${escapedUrl}/sources/:revision/:path*`} to={`${url}/code/sources/:revision/:path*`} />
<Redirect exact from={`${escapedUrl}/changesets`} to={`${url}/code/changesets`} />
<Redirect
from={`${escapedUrl}/branch/:branch/changesets`}
to={`${url}/code/branch/:branch/changesets/`}
/>
<Route path={`${url}/info`} exact>
<Route path={`${escapedUrl}/info`} exact>
<RepositoryDetails repository={repository} />
</Route>
<Route path={`${url}/settings/general`}>
<Route path={`${escapedUrl}/settings/general`}>
<EditRepo repository={repository} />
</Route>
<Route path={`${url}/settings/permissions`}>
<Route path={`${escapedUrl}/settings/permissions`}>
<Permissions namespaceOrRepository={repository} />
</Route>
<Route exact path={`${url}/code/changeset/:id`}>
<Route exact path={`${escapedUrl}/code/changeset/:id`}>
<ChangesetView repository={repository} fileControlFactoryFactory={fileControlFactoryFactory} />
</Route>
<Route path={`${url}/code/sourceext/:extension`} exact={true}>
<Route path={`${escapedUrl}/code/sourceext/:extension`} exact={true}>
<SourceExtensions repository={repository} />
</Route>
<Route path={`${url}/code/sourceext/:extension/:revision/:path*`}>
<Route path={`${escapedUrl}/code/sourceext/:extension/:revision/:path*`}>
<SourceExtensions repository={repository} baseUrl={`${url}/code/sources`} />
</Route>
<Route path={`${url}/code`}>
<Route path={`${escapedUrl}/code`}>
<CodeOverview baseUrl={`${url}/code`} repository={repository} />
</Route>
<Route path={`${url}/branch/:branch`}>
<Route path={`${escapedUrl}/branch/:branch`}>
<BranchRoot repository={repository} />
</Route>
<Route path={`${url}/branches`} exact={true}>
<Route path={`${escapedUrl}/branches`} exact={true}>
<BranchesOverview repository={repository} baseUrl={`${url}/branch`} />
</Route>
<Route path={`${url}/branches/create`}>
<Route path={`${escapedUrl}/branches/create`}>
<CreateBranch repository={repository} />
</Route>
<Route path={`${url}/tag/:tag`}>
<Route path={`${escapedUrl}/tag/:tag`}>
<TagRoot repository={repository} baseUrl={`${url}/tag`} />
</Route>
<Route path={`${url}/tags`} exact={true}>
<Route path={`${escapedUrl}/tags`} exact={true}>
<TagsOverview repository={repository} baseUrl={`${url}/tag`} />
</Route>
<Route path={`${url}/compare/:sourceType/:sourceName`}>
<Route path={`${escapedUrl}/compare/:sourceType/:sourceName`}>
<CompareRoot repository={repository} baseUrl={`${url}/compare`} />
</Route>
<ExtensionPoint<extensionPoints.RepositoryRoute>
name="repository.route"
props={extensionProps}
props={{
repository,
url: urls.escapeUrlForRoute(url),
indexLinks
}}
renderAll={true}
/>
</Switch>

View File

@@ -52,10 +52,12 @@ const TagRoot: FC<Props> = ({ repository, baseUrl }) => {
const url = urls.matchedUrlFromMatch(match);
const escapedUrl = urls.escapeUrlForRoute(url);
return (
<Switch>
<Redirect exact from={url} to={`${url}/info`} />
<Route path={`${url}/info`}>
<Redirect exact from={escapedUrl} to={`${url}/info`} />
<Route path={`${escapedUrl}/info`}>
<TagView repository={repository} tag={tag} />
</Route>
</Switch>

View File

@@ -74,30 +74,35 @@ const SingleUser: FC = () => {
url
};
const escapedUrl = urls.escapeUrlForRoute(url);
return (
<StateMenuContextProvider>
<Page title={user.displayName}>
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Route path={url} exact>
<Route path={escapedUrl} exact>
<Details user={user} />
</Route>
<Route path={`${url}/settings/general`}>
<Route path={`${escapedUrl}/settings/general`}>
<EditUser user={user} />
</Route>
<Route path={`${url}/settings/password`}>
<Route path={`${escapedUrl}/settings/password`}>
<SetUserPassword user={user} />
</Route>
<Route path={`${url}/settings/permissions`}>
<Route path={`${escapedUrl}/settings/permissions`}>
<SetUserPermissions user={user} />
</Route>
<Route path={`${url}/settings/publickeys`}>
<Route path={`${escapedUrl}/settings/publickeys`}>
<SetPublicKeys user={user} />
</Route>
<Route path={`${url}/settings/apiKeys`}>
<Route path={`${escapedUrl}/settings/apiKeys`}>
<SetApiKeys user={user} />
</Route>
<ExtensionPoint<extensionPoints.UserRoute> name="user.route" props={extensionProps} renderAll={true} />
<ExtensionPoint<extensionPoints.UserRoute> name="user.route" props={{
user,
url: escapedUrl
}} renderAll={true} />
</PrimaryContentColumn>
<SecondaryNavigationColumn>
<SecondaryNavigation label={t("singleUser.menu.navigationLabel")}>