From 61676c6dd4d792444ee82716ffd104eae5e55456 Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Fri, 9 Sep 2022 08:19:04 +0200 Subject: [PATCH] Handle missing encoding of square brackets in filenames (#2117) Due to unexpected and largely unchangeable behavior by both react-router and the browser, square brackets are not correctly encoded in the url when clicking a file link in the source view where the filename contains either of these characters. The source view then tries to use the useSources hook to get the file content but fails, because the path param for the file path it gets from the url has unencoded square brackets in them which are illegal in urls except for declaring IPv6 addresses. We have created a catch for exactly this scenario at the latest possible point before the actual http request is fired, which is in the useSources hook. It seems like the square brackets are the only affected special characters so we force encoding on them specifically. Only the path portion of the URL is checked so the host portion of the url may still contain unencoded square brackets which are left untouched. --- .../square_brackets_in_filename.yaml | 2 ++ scm-ui/ui-api/src/sources.test.ts | 25 +++++++++++-------- scm-ui/ui-api/src/sources.ts | 4 ++- 3 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 gradle/changelog/square_brackets_in_filename.yaml diff --git a/gradle/changelog/square_brackets_in_filename.yaml b/gradle/changelog/square_brackets_in_filename.yaml new file mode 100644 index 0000000000..82834ff095 --- /dev/null +++ b/gradle/changelog/square_brackets_in_filename.yaml @@ -0,0 +1,2 @@ +- type: fixed + description: Handle missing encoding of square brackets in filenames ([#2117](https://github.com/scm-manager/scm-manager/pull/2117)) diff --git a/scm-ui/ui-api/src/sources.test.ts b/scm-ui/ui-api/src/sources.test.ts index 0af56ad761..ba61af1426 100644 --- a/scm-ui/ui-api/src/sources.test.ts +++ b/scm-ui/ui-api/src/sources.test.ts @@ -124,6 +124,21 @@ describe("Test sources hooks", () => { }; describe("useSources tests", () => { + const testPath = async (path: string, expectedPath: string) => { + const queryClient = createInfiniteCachingClient(); + fetchMock.getOnce(`/api/v2/src/abc/${expectedPath}?collapse=true`, readmeMd); + const { result, waitFor } = renderHook(() => useSources(puzzle42, { revision: "abc", path }), { + wrapper: createWrapper(undefined, queryClient), + }); + await waitFor(() => !!result.current.data); + expect(result.current.data).toEqual(readmeMd); + }; + + it("should return file from url with revision and path", () => testPath("README.md", "README.md")); + it("should encode square brackets in path", () => + testPath("[...nextauth][...other].ts", "%5B...nextauth%5D%5B...other%5D.ts")); + it("should not double-encode path", () => testPath("%7BDatei%7D#42.txt", "%7BDatei%7D#42.txt")); + it("should return root directory", async () => { const queryClient = createInfiniteCachingClient(); fetchMock.getOnce("/api/v2/src?collapse=true", rootDirectory); @@ -134,16 +149,6 @@ describe("Test sources hooks", () => { expect(result.current.data).toEqual(rootDirectory); }); - it("should return file from url with revision and path", async () => { - const queryClient = createInfiniteCachingClient(); - fetchMock.getOnce("/api/v2/src/abc/README.md?collapse=true", readmeMd); - const { result, waitFor } = renderHook(() => useSources(puzzle42, { revision: "abc", path: "README.md" }), { - wrapper: createWrapper(undefined, queryClient), - }); - await waitFor(() => !!result.current.data); - expect(result.current.data).toEqual(readmeMd); - }); - it("should fetch next page", async () => { const queryClient = createInfiniteCachingClient(); fetchMock.getOnce("/api/v2/src?collapse=true", mainDirectoryTruncated); diff --git a/scm-ui/ui-api/src/sources.ts b/scm-ui/ui-api/src/sources.ts index 8095406326..5972168944 100644 --- a/scm-ui/ui-api/src/sources.ts +++ b/scm-ui/ui-api/src/sources.ts @@ -93,7 +93,7 @@ const createSourcesLink = (repository: Repository, options: UseSourcesOptions) = link = urls.concat(link, encodeURIComponent(options.revision)); if (options.path) { - link = urls.concat(link, options.path); + link = urls.concat(link, encodeInvalidCharacters(options.path)); } } if (options.collapse) { @@ -102,6 +102,8 @@ const createSourcesLink = (repository: Repository, options: UseSourcesOptions) = return link; }; +const encodeInvalidCharacters = (input: string) => input.replace(/\[/g, "%5B").replace(/]/g, "%5D"); + const merge = (files?: File[]): File | undefined => { if (!files || files.length === 0) { return;