Skip to content

Commit 7666aee

Browse files
authored
Merge pull request pterodactyl#2956 from pterodactyl/fix/files-urlencoding
fix urlencoding in the file manager
2 parents b352c04 + 4fd2af0 commit 7666aee

File tree

7 files changed

+35
-47
lines changed

7 files changed

+35
-47
lines changed

app/Transformers/Daemon/FileObjectTransformer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class FileObjectTransformer extends BaseDaemonTransformer
2323
public function transform(array $item)
2424
{
2525
return [
26-
'name' => rawurlencode(Arr::get($item, 'name')),
26+
'name' => Arr::get($item, 'name'),
2727
'mode' => Arr::get($item, 'mode'),
2828
'mode_bits' => Arr::get($item, 'mode_bits'),
2929
'size' => Arr::get($item, 'size'),

resources/scripts/api/server/files/loadDirectory.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ export interface FileObject {
1818

1919
export default async (uuid: string, directory?: string): Promise<FileObject[]> => {
2020
const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, {
21-
// At this point the directory is still encoded so we need to decode it since axios
22-
// will automatically re-encode this value before sending it along in the request.
2321
params: { directory: directory ?? '/' },
2422
});
2523

resources/scripts/components/server/files/FileEditContainer.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import modes from '@/modes';
1717
import useFlash from '@/plugins/useFlash';
1818
import { ServerContext } from '@/state/server';
1919
import ErrorBoundary from '@/components/elements/ErrorBoundary';
20+
import { encodePathSegments, hashToPath } from '@/helpers';
21+
import { dirname } from 'path';
2022

2123
const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor'));
2224

@@ -43,8 +45,9 @@ export default () => {
4345

4446
setError('');
4547
setLoading(true);
46-
setDirectory(hash.replace(/^#/, '').split('/').filter(v => !!v).slice(0, -1).join('/'));
47-
getFileContents(uuid, hash.replace(/^#/, ''))
48+
const path = hashToPath(hash);
49+
setDirectory(dirname(path));
50+
getFileContents(uuid, path)
4851
.then(setContent)
4952
.catch(error => {
5053
console.error(error);
@@ -61,10 +64,10 @@ export default () => {
6164
setLoading(true);
6265
clearFlashes('files:view');
6366
fetchFileContent()
64-
.then(content => saveFileContents(uuid, name || decodeURI(hash.replace(/^#/, '')), content))
67+
.then(content => saveFileContents(uuid, name || hashToPath(hash), content))
6568
.then(() => {
6669
if (name) {
67-
history.push(`/server/${id}/files/edit#/${name}`);
70+
history.push(`/server/${id}/files/edit#/${encodePathSegments(name)}`);
6871
return;
6972
}
7073

resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useEffect, useState } from 'react';
22
import { ServerContext } from '@/state/server';
33
import { NavLink, useLocation } from 'react-router-dom';
4-
import { cleanDirectoryPath } from '@/helpers';
4+
import { encodePathSegments, hashToPath } from '@/helpers';
55
import tw from 'twin.macro';
66

77
interface Props {
@@ -17,22 +17,10 @@ export default ({ renderLeft, withinFileEditor, isNewFile }: Props) => {
1717
const { hash } = useLocation();
1818

1919
useEffect(() => {
20-
let pathHash = cleanDirectoryPath(hash);
21-
try {
22-
pathHash = decodeURI(pathHash);
23-
} catch (e) {
24-
console.warn('Error decoding URL parts in hash:', e);
25-
}
20+
const path = hashToPath(hash);
2621

2722
if (withinFileEditor && !isNewFile) {
28-
let name = pathHash.split('/').pop() || null;
29-
if (name) {
30-
try {
31-
name = decodeURIComponent(name);
32-
} catch (e) {
33-
console.warn('Error decoding filename:', e);
34-
}
35-
}
23+
const name = path.split('/').pop() || null;
3624
setFile(name);
3725
}
3826
}, [ withinFileEditor, isNewFile, hash ]);
@@ -62,14 +50,14 @@ export default ({ renderLeft, withinFileEditor, isNewFile }: Props) => {
6250
crumb.path ?
6351
<React.Fragment key={index}>
6452
<NavLink
65-
to={`/server/${id}/files#${crumb.path}`}
53+
to={`/server/${id}/files#${encodePathSegments(crumb.path)}`}
6654
css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`}
6755
>
68-
{decodeURIComponent(crumb.name)}
56+
{crumb.name}
6957
</NavLink>/
7058
</React.Fragment>
7159
:
72-
<span key={index} css={tw`px-1 text-neutral-300`}>{decodeURIComponent(crumb.name)}</span>
60+
<span key={index} css={tw`px-1 text-neutral-300`}>{crumb.name}</span>
7361
))
7462
}
7563
{file &&

resources/scripts/components/server/files/FileManagerContainer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import ServerContentBlock from '@/components/elements/ServerContentBlock';
1919
import { useStoreActions } from '@/state/hooks';
2020
import ErrorBoundary from '@/components/elements/ErrorBoundary';
2121
import { FileActionCheckbox } from '@/components/server/files/SelectFileCheckbox';
22+
import { hashToPath } from '@/helpers';
2223

2324
const sortFiles = (files: FileObject[]): FileObject[] => {
2425
return files.sort((a, b) => a.name.localeCompare(b.name))
@@ -39,7 +40,7 @@ export default () => {
3940
useEffect(() => {
4041
clearFlashes('files');
4142
setSelectedFiles([]);
42-
setDirectory(hash.length > 0 ? hash : '/');
43+
setDirectory(hashToPath(hash));
4344
}, [ hash ]);
4445

4546
useEffect(() => {

resources/scripts/components/server/files/FileObjectRow.tsx

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
22
import { faFileAlt, faFileArchive, faFileImport, faFolder } from '@fortawesome/free-solid-svg-icons';
3-
import { bytesToHuman, cleanDirectoryPath } from '@/helpers';
3+
import { bytesToHuman, encodePathSegments } from '@/helpers';
44
import { differenceInHours, format, formatDistanceToNow } from 'date-fns';
55
import React, { memo } from 'react';
66
import { FileObject } from '@/api/server/files/loadDirectory';
77
import FileDropdownMenu from '@/components/server/files/FileDropdownMenu';
88
import { ServerContext } from '@/state/server';
9-
import { NavLink, useHistory, useRouteMatch } from 'react-router-dom';
9+
import { NavLink, useRouteMatch } from 'react-router-dom';
1010
import tw from 'twin.macro';
1111
import isEqual from 'react-fast-compare';
1212
import styled from 'styled-components/macro';
1313
import SelectFileCheckbox from '@/components/server/files/SelectFileCheckbox';
1414
import { usePermissions } from '@/plugins/usePermissions';
15+
import { join } from 'path';
1516

1617
const Row = styled.div`
1718
${tw`flex bg-neutral-700 rounded-sm mb-px text-sm hover:text-neutral-100 cursor-pointer items-center no-underline hover:bg-neutral-600`};
@@ -21,33 +22,17 @@ const Clickable: React.FC<{ file: FileObject }> = memo(({ file, children }) => {
2122
const [ canReadContents ] = usePermissions([ 'file.read-content' ]);
2223
const directory = ServerContext.useStoreState(state => state.files.directory);
2324

24-
const history = useHistory();
2525
const match = useRouteMatch();
2626

27-
const destination = cleanDirectoryPath(`${directory}/${file.name}`).split('/').join('/');
28-
29-
const onRowClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
30-
// Don't rely on the onClick to work with the generated URL. Because of the way this
31-
// component re-renders you'll get redirected into a nested directory structure since
32-
// it'll cause the directory variable to update right away when you click.
33-
//
34-
// Just trust me future me, leave this be.
35-
if (!file.isFile) {
36-
e.preventDefault();
37-
history.push(`#${destination}`);
38-
}
39-
};
40-
4127
return (
4228
(!canReadContents || (file.isFile && !file.isEditable())) ?
4329
<div css={tw`flex flex-1 text-neutral-300 no-underline p-3 cursor-default overflow-hidden truncate`}>
4430
{children}
4531
</div>
4632
:
4733
<NavLink
48-
to={`${match.url}${file.isFile ? '/edit' : ''}#${destination}`}
34+
to={`${match.url}${file.isFile ? '/edit' : ''}#${encodePathSegments(join(directory, file.name))}`}
4935
css={tw`flex flex-1 text-neutral-300 no-underline p-3 overflow-hidden truncate`}
50-
onClick={onRowClick}
5136
>
5237
{children}
5338
</NavLink>
@@ -72,7 +57,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => (
7257
}
7358
</div>
7459
<div css={tw`flex-1 truncate`}>
75-
{decodeURIComponent(file.name)}
60+
{file.name}
7661
</div>
7762
{file.isFile &&
7863
<div css={tw`w-1/6 text-right mr-4 hidden sm:block`}>

resources/scripts/helpers.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function megabytesToHuman (mb: number): string {
1717

1818
export const randomInt = (low: number, high: number) => Math.floor(Math.random() * (high - low) + low);
1919

20-
export const cleanDirectoryPath = (path: string) => path.replace(/(^#\/*)|(\/(\/*))|(^$)/g, '/');
20+
export const cleanDirectoryPath = (path: string) => path.replace(/(\/(\/*))|(^$)/g, '/');
2121

2222
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
2323

@@ -50,3 +50,16 @@ export function fileBitsToString (mode: string, directory: boolean): string {
5050

5151
return buf;
5252
}
53+
54+
/**
55+
* URL-encodes the segments of a path.
56+
* This allows to use the path as part of a URL while preserving the slashes.
57+
* @param path the path to encode
58+
*/
59+
export function encodePathSegments (path: string): string {
60+
return path.split('/').map(s => encodeURIComponent(s)).join('/');
61+
}
62+
63+
export function hashToPath (hash: string): string {
64+
return hash.length > 0 ? decodeURIComponent(hash.substr(1)) : '/';
65+
}

0 commit comments

Comments
 (0)