Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle encoding of dynamic params in descendant routes #9589

Merged
merged 6 commits into from Nov 18, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/pretty-dolls-bathe.md
@@ -0,0 +1,6 @@
---
"react-router": patch
"react-router-dom": patch
---

Fix issues with encoded characters in descendant routes
8 changes: 8 additions & 0 deletions packages/react-router-dom-v5-compat/lib/components.tsx
Expand Up @@ -81,6 +81,14 @@ export function StaticRouter({
createHref(to: To) {
return typeof to === "string" ? to : createPath(to);
},
encodeLocation(to: To) {
let path = typeof to === "string" ? parsePath(to) : to;
return {
pathname: path.pathname || "",
search: path.search || "",
hash: path.hash || "",
};
},
push(to: To) {
throw new Error(
`You cannot use navigator.push() on the server because it is a stateless ` +
Expand Down
39 changes: 39 additions & 0 deletions packages/react-router-dom/__tests__/special-characters-test.tsx
Expand Up @@ -221,6 +221,17 @@ describe("special character tests", () => {
path="/reset"
element={<Link to={navigatePath}>Link to path</Link>}
/>
<Route
path="/descendant/:param/*"
element={
<Routes>
<Route
path="match"
element={<Comp heading="Descendant Route" />}
/>
</Routes>
}
/>
<Route path="/*" element={<Comp heading="Root Splat Route" />} />
</>
);
Expand Down Expand Up @@ -487,6 +498,34 @@ describe("special character tests", () => {
}
});

it("handles special chars in descendant routes paths", async () => {
for (let charDef of specialChars) {
let { char, pathChar } = charDef;

await testParamValues(
`/descendant/${char}/match`,
"Descendant Route",
{
pathname: `/descendant/${pathChar}/match`,
search: "",
hash: "",
},
{ param: char, "*": "match" }
);

await testParamValues(
`/descendant/foo${char}bar/match`,
"Descendant Route",
{
pathname: `/descendant/foo${pathChar}bar/match`,
search: "",
hash: "",
},
{ param: `foo${char}bar`, "*": "match" }
);
}
});

it("handles special chars in search params", async () => {
for (let charDef of specialChars) {
let { char, searchChar } = charDef;
Expand Down
25 changes: 19 additions & 6 deletions packages/react-router-dom/server.tsx
@@ -1,5 +1,6 @@
import * as React from "react";
import type {
Path,
RevalidationState,
Router as RemixRouter,
StaticHandlerContext,
Expand Down Expand Up @@ -141,9 +142,8 @@ export function unstable_StaticRouterProvider({

function getStatelessNavigator() {
return {
createHref(to: To) {
return typeof to === "string" ? to : createPath(to);
},
createHref,
encodeLocation,
push(to: To) {
throw new Error(
`You cannot use navigator.push() on the server because it is a stateless ` +
Expand Down Expand Up @@ -230,9 +230,8 @@ export function unstable_createStaticRouter(
revalidate() {
throw msg("revalidate");
},
createHref() {
throw msg("createHref");
},
createHref,
encodeLocation,
getFetcher() {
return IDLE_FETCHER;
},
Expand All @@ -246,3 +245,17 @@ export function unstable_createStaticRouter(
_internalActiveDeferreds: new Map(),
};
}

function createHref(to: To) {
return typeof to === "string" ? to : createPath(to);
}

function encodeLocation(to: To): Path {
// Locations should already be encoded on the server, so just return as-is
let path = typeof to === "string" ? parsePath(to) : to;
return {
pathname: path.pathname || "",
search: path.search || "",
hash: path.hash || "",
};
}
1 change: 1 addition & 0 deletions packages/react-router/lib/components.tsx
Expand Up @@ -69,6 +69,7 @@ export function RouterProvider({
let navigator = React.useMemo((): Navigator => {
return {
createHref: router.createHref,
encodeLocation: router.encodeLocation,
go: (n) => router.navigate(n),
push: (to, state, opts) =>
router.navigate(to, {
Expand Down
1 change: 1 addition & 0 deletions packages/react-router/lib/context.ts
Expand Up @@ -107,6 +107,7 @@ export interface NavigateOptions {
*/
export interface Navigator {
createHref: History["createHref"];
encodeLocation: History["encodeLocation"];
go: History["go"];
push(to: To, state?: any, opts?: NavigateOptions): void;
replace(to: To, state?: any, opts?: NavigateOptions): void;
Expand Down
13 changes: 11 additions & 2 deletions packages/react-router/lib/hooks.tsx
Expand Up @@ -310,6 +310,7 @@ export function useRoutes(
`useRoutes() may be used only in the context of a <Router> component.`
);

let { navigator } = React.useContext(NavigationContext);
let dataRouterStateContext = React.useContext(DataRouterStateContext);
let { matches: parentMatches } = React.useContext(RouteContext);
let routeMatch = parentMatches[parentMatches.length - 1];
Expand Down Expand Up @@ -401,11 +402,19 @@ export function useRoutes(
matches.map((match) =>
Object.assign({}, match, {
params: Object.assign({}, parentParams, match.params),
pathname: joinPaths([parentPathnameBase, match.pathname]),
pathname: joinPaths([
parentPathnameBase,
// Re-encode pathnames that were decoded inside matchRoutes
navigator.encodeLocation(match.pathname).pathname,
]),
pathnameBase:
match.pathnameBase === "/"
? parentPathnameBase
: joinPaths([parentPathnameBase, match.pathnameBase]),
: joinPaths([
parentPathnameBase,
// Re-encode pathnames that were decoded inside matchRoutes
navigator.encodeLocation(match.pathnameBase).pathname,
]),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need these to be re-encoded so that our pathname.slice(parentPathnameBase.length) call up on line 382 is correct. It's slicing from an encoded location.pathname so needs to be using the length of an encoded parentPathnameBase

})
),
parentMatches,
Expand Down
20 changes: 12 additions & 8 deletions packages/router/history.ts
Expand Up @@ -127,12 +127,12 @@ export interface History {

/**
* Encode a location the same way window.history would do (no-op for memory
* history) so we ensure our PUSH/REPLAC e navigations for data routers
* history) so we ensure our PUSH/REPLACE navigations for data routers
* behave the same as POP
*
* @param location The incoming location from router.navigate()
* @param to Unencoded path
*/
encodeLocation(location: Location): Location;
encodeLocation(to: To): Path;

/**
* Pushes a new location onto the history stack, increasing its length by one.
Expand Down Expand Up @@ -268,8 +268,13 @@ export function createMemoryHistory(
createHref(to) {
return typeof to === "string" ? to : createPath(to);
},
encodeLocation(location) {
return location;
encodeLocation(to: To) {
let path = typeof to === "string" ? parsePath(to) : to;
return {
pathname: path.pathname || "",
search: path.search || "",
hash: path.hash || "",
};
},
push(to, state) {
action = Action.Push;
Expand Down Expand Up @@ -636,11 +641,10 @@ function getUrlBasedHistory(
createHref(to) {
return createHref(window, to);
},
encodeLocation(location) {
encodeLocation(to) {
// Encode a Location the same way window.location would
let url = createURL(createPath(location));
let url = createURL(typeof to === "string" ? to : createPath(to));
return {
...location,
pathname: url.pathname,
search: url.search,
hash: url.hash,
Expand Down
18 changes: 16 additions & 2 deletions packages/router/router.ts
@@ -1,4 +1,4 @@
import type { History, Location, To } from "./history";
import type { History, Location, Path, To } from "./history";
import {
Action as HistoryAction,
createLocation,
Expand Down Expand Up @@ -154,6 +154,16 @@ export interface Router {
*/
createHref(location: Location | URL): string;

/**
* @internal
* PRIVATE - DO NOT USE
*
* Utility function to URL encode a destination path according to the internal
* history implementation
* @param to
*/
encodeLocation(to: To): Path;

/**
* @internal
* PRIVATE - DO NOT USE
Expand Down Expand Up @@ -773,7 +783,10 @@ export function createRouter(init: RouterInit): Router {
// remains the same as POP and non-data-router usages. new URL() does all
// the same encoding we'd get from a history.pushState/window.location read
// without having to touch history
location = init.history.encodeLocation(location);
location = {
...location,
...init.history.encodeLocation(location),
};

let historyAction =
(opts && opts.replace) === true || submission != null
Expand Down Expand Up @@ -1825,6 +1838,7 @@ export function createRouter(init: RouterInit): Router {
// Passthrough to history-aware createHref used by useHref so we get proper
// hash-aware URLs in DOM paths
createHref: (to: To) => init.history.createHref(to),
encodeLocation: (to: To) => init.history.encodeLocation(to),
getFetcher,
deleteFetcher,
dispose,
Expand Down