Skip to content

Commit 07ea3af

Browse files
fix: [M3-8289] - Properly encode URL and filenames of files (#12077)
* fix: [M3-8289] - Properly encode URL and filenames of files * No need for e2e since we're encoding at the request --------- Co-authored-by: Jaalah Ramos <jaalah.ramos@gmail.com>
1 parent 0a323a6 commit 07ea3af

5 files changed

Lines changed: 44 additions & 23 deletions

File tree

packages/manager/src/components/Link.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,24 @@ import {
88
import * as React from 'react';
99
// eslint-disable-next-line no-restricted-imports
1010
import { Link as RouterLink } from 'react-router-dom';
11+
import type { LinkProps as _LinkProps } from 'react-router-dom';
1112

1213
import ExternalLinkIcon from 'src/assets/icons/external-link.svg';
1314
import { useStyles } from 'src/components/Link.styles';
1415

1516
import type { LinkProps as TanStackLinkProps } from '@tanstack/react-router';
16-
import type { LinkProps as _LinkProps } from 'react-router-dom';
1717

1818
export interface LinkProps extends Omit<_LinkProps, 'to'> {
1919
/**
2020
* This property can override the value of the copy passed by default to the aria label from the children.
2121
* This is useful when the text of the link is unavailable, not descriptive enough, or a single icon is used as the child.
2222
*/
2323
accessibleAriaLabel?: string;
24+
/**
25+
* Optional prop to bypass URL sanitization. Use with caution.
26+
* @default false
27+
*/
28+
bypassSanitization?: boolean;
2429
/**
2530
* Optional prop to render the link as an external link, which features an external link icon, opens in a new tab<br />
2631
* and provides by default "noopener noreferrer" attributes to prevent security vulnerabilities.
@@ -47,7 +52,7 @@ export interface LinkProps extends Omit<_LinkProps, 'to'> {
4752
* @example "/profile/display"
4853
* @example "https://linode.com"
4954
*/
50-
to: Exclude<TanStackLinkProps['to'] | (string & {}), null | undefined>;
55+
to: Exclude<(string & {}) | TanStackLinkProps['to'], null | undefined>;
5156
}
5257

5358
/**
@@ -78,14 +83,15 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
7883
children,
7984
className,
8085
external,
86+
bypassSanitization,
8187
forceCopyColor,
8288
hideIcon,
8389
onClick,
8490
to,
8591
} = props;
8692
const { classes, cx } = useStyles();
87-
const sanitizedUrl = () => sanitizeUrl(to);
88-
const shouldOpenInNewTab = opensInNewTab(sanitizedUrl());
93+
const processedUrl = () => (bypassSanitization ? to : sanitizeUrl(to));
94+
const shouldOpenInNewTab = opensInNewTab(processedUrl());
8995
const childrenAsAriaLabel = flattenChildrenIntoAriaLabel(children);
9096
const externalNotice = '- link opens in a new tab';
9197
const ariaLabel = accessibleAriaLabel
@@ -108,16 +114,16 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
108114

109115
return shouldOpenInNewTab ? (
110116
<a
117+
aria-label={ariaLabel}
111118
className={cx(
112119
classes.root,
113120
{
114121
[classes.forceCopyColor]: forceCopyColor,
115122
},
116123
className
117124
)}
118-
aria-label={ariaLabel}
119125
data-testid={external ? 'external-site-link' : 'external-link'}
120-
href={sanitizedUrl()}
126+
href={processedUrl()}
121127
onClick={onClick}
122128
ref={ref}
123129
rel="noopener noreferrer"

packages/manager/src/components/Uploaders/ObjectUploader/ObjectUploader.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -300,16 +300,15 @@ export const ObjectUploader = React.memo((props: Props) => {
300300
);
301301
});
302302

303-
export const onUploadProgressFactory = (
304-
dispatch: (value: ObjectUploaderAction) => void,
305-
fileName: string
306-
) => (progressEvent: AxiosProgressEvent) => {
307-
dispatch({
308-
data: {
309-
percentComplete:
310-
(progressEvent.loaded / (progressEvent.total ?? 1)) * 100,
311-
},
312-
filesToUpdate: [fileName],
313-
type: 'UPDATE_FILES',
314-
});
315-
};
303+
export const onUploadProgressFactory =
304+
(dispatch: (value: ObjectUploaderAction) => void, fileName: string) =>
305+
(progressEvent: AxiosProgressEvent) => {
306+
dispatch({
307+
data: {
308+
percentComplete:
309+
(progressEvent.loaded / (progressEvent.total ?? 1)) * 100,
310+
},
311+
filesToUpdate: [fileName],
312+
type: 'UPDATE_FILES',
313+
});
314+
};

packages/manager/src/features/ObjectStorage/BucketDetail/ObjectDetailsDrawer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const ObjectDetailsDrawer = React.memo(
8585

8686
{url ? (
8787
<StyledLinkContainer>
88-
<Link external to={url}>
88+
<Link bypassSanitization external to={url}>
8989
{truncateMiddle(url, 50)}
9090
</Link>
9191
<StyledCopyTooltip sx={{ marginLeft: 4 }} text={url} />

packages/manager/src/features/ObjectStorage/utilities.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import { ObjectStorageObject } from '@linode/api-v4/lib/object-storage';
2-
31
import {
42
basename,
53
confirmObjectStorage,
64
displayName,
75
extendObject,
86
firstSubfolder,
7+
generateObjectUrl,
98
isFile,
109
isFolder,
1110
prefixArrayToString,
1211
tableUpdateAction,
1312
} from './utilities';
1413

14+
import type { ObjectStorageObject } from '@linode/api-v4/lib/object-storage';
15+
1516
const folder: ObjectStorageObject = {
1617
etag: null,
1718
last_modified: null,
@@ -41,7 +42,22 @@ const object3: ObjectStorageObject = {
4142
size: 0,
4243
};
4344

45+
const mockHostname = 'my-bucket.linodeobjects.com';
46+
4447
describe('Object Storage utilities', () => {
48+
describe('generateObjectUrl', () => {
49+
it('returns the correct URL', () => {
50+
expect(generateObjectUrl(mockHostname, 'my-object')).toBe(
51+
'https://my-bucket.linodeobjects.com/my-object'
52+
);
53+
});
54+
it('encodes the URL for special characters', () => {
55+
expect(generateObjectUrl(mockHostname, 'my-object?')).toBe(
56+
'https://my-bucket.linodeobjects.com/my-object%3F'
57+
);
58+
});
59+
});
60+
4561
describe('isFolder', () => {
4662
it('returns `true` if the object has null for fields except `name`', () => {
4763
expect(isFolder(folder)).toBe(true);

packages/manager/src/features/ObjectStorage/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { ObjectStorageEndpoint } from '@linode/api-v4/lib/object-storage';
66
import type { FormikProps } from 'formik';
77

88
export const generateObjectUrl = (hostname: string, objectName: string) => {
9-
return `https://${hostname}/${objectName}`;
9+
return `https://${hostname}/${encodeURIComponent(objectName)}`;
1010
};
1111

1212
// Objects ending with a / and having a size of 0 are often used to represent

0 commit comments

Comments
 (0)