Skip to content

Commit 903b579

Browse files
committed
Avoid breaking the entire UI when naughty characters are present in the file name or directory; closes pterodactyl#2575
1 parent 65d04d0 commit 903b579

File tree

7 files changed

+78
-39
lines changed

7 files changed

+78
-39
lines changed
Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
import http from '@/api/http';
22

3-
export default (uuid: string, file: string, content: string): Promise<void> => {
4-
return new Promise((resolve, reject) => {
5-
http.post(
6-
`/api/client/servers/${uuid}/files/write`,
7-
content,
8-
{
9-
params: { file },
10-
headers: {
11-
'Content-Type': 'text/plain',
12-
},
13-
},
14-
)
15-
.then(() => resolve())
16-
.catch(reject);
3+
export default async (uuid: string, file: string, content: string): Promise<void> => {
4+
await http.post(`/api/client/servers/${uuid}/files/write`, content, {
5+
params: { file },
6+
headers: {
7+
'Content-Type': 'text/plain',
8+
},
179
});
1810
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from 'react';
2+
import tw from 'twin.macro';
3+
import Icon from '@/components/elements/Icon';
4+
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
5+
6+
interface State {
7+
hasError: boolean;
8+
}
9+
10+
// eslint-disable-next-line @typescript-eslint/ban-types
11+
class ErrorBoundary extends React.Component<{}, State> {
12+
state: State = {
13+
hasError: false,
14+
};
15+
16+
static getDerivedStateFromError () {
17+
return { hasError: true };
18+
}
19+
20+
componentDidCatch (error: Error) {
21+
console.error(error);
22+
}
23+
24+
render () {
25+
return this.state.hasError ?
26+
<div css={tw`flex items-center justify-center w-full my-4`}>
27+
<div css={tw`flex items-center bg-neutral-900 rounded p-3 text-red-500`}>
28+
<Icon icon={faExclamationTriangle} css={tw`h-4 w-auto mr-2`}/>
29+
<p css={tw`text-sm text-neutral-100`}>
30+
An error was encountered by the application while rendering this view. Try refreshing the page.
31+
</p>
32+
</div>
33+
</div>
34+
:
35+
this.props.children;
36+
}
37+
}
38+
39+
export default ErrorBoundary;

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Select from '@/components/elements/Select';
1616
import modes from '@/modes';
1717
import useFlash from '@/plugins/useFlash';
1818
import { ServerContext } from '@/state/server';
19+
import ErrorBoundary from '@/components/elements/ErrorBoundary';
1920

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

@@ -60,9 +61,7 @@ export default () => {
6061
setLoading(true);
6162
clearFlashes('files:view');
6263
fetchFileContent()
63-
.then(content => {
64-
return saveFileContents(uuid, name || hash.replace(/^#/, ''), content);
65-
})
64+
.then(content => saveFileContents(uuid, encodeURIComponent(name || hash.replace(/^#/, '')), content))
6665
.then(() => {
6766
if (name) {
6867
history.push(`/server/${id}/files/edit#/${name}`);
@@ -87,7 +86,9 @@ export default () => {
8786
return (
8887
<PageContentBlock>
8988
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
90-
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
89+
<ErrorBoundary>
90+
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
91+
</ErrorBoundary>
9192
{hash.replace(/^#/, '').endsWith('.pteroignore') &&
9293
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
9394
<p css={tw`text-neutral-300 text-sm`}>

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
3333
.filter(directory => !!directory)
3434
.map((directory, index, dirs) => {
3535
if (!withinFileEditor && index === dirs.length - 1) {
36-
return { name: decodeURIComponent(directory) };
36+
return { name: decodeURIComponent(encodeURIComponent(directory)) };
3737
}
3838

39-
return { name: decodeURIComponent(directory), path: `/${dirs.slice(0, index + 1).join('/')}` };
39+
return { name: decodeURIComponent(encodeURIComponent(directory)), path: `/${dirs.slice(0, index + 1).join('/')}` };
4040
});
4141

4242
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -79,7 +79,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
7979
}
8080
{file &&
8181
<React.Fragment>
82-
<span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(file)}</span>
82+
<span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(encodeURIComponent(file))}</span>
8383
</React.Fragment>
8484
}
8585
</div>

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

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import MassActionsBar from '@/components/server/files/MassActionsBar';
1717
import UploadButton from '@/components/server/files/UploadButton';
1818
import ServerContentBlock from '@/components/elements/ServerContentBlock';
1919
import { useStoreActions } from '@/state/hooks';
20+
import ErrorBoundary from '@/components/elements/ErrorBoundary';
2021

2122
const sortFiles = (files: FileObject[]): FileObject[] => {
2223
return files.sort((a, b) => a.name.localeCompare(b.name))
@@ -50,7 +51,9 @@ export default () => {
5051

5152
return (
5253
<ServerContentBlock title={'File Manager'} showFlashKey={'files'}>
53-
<FileManagerBreadcrumbs/>
54+
<ErrorBoundary>
55+
<FileManagerBreadcrumbs/>
56+
</ErrorBoundary>
5457
{
5558
!files ?
5659
<Spinner size={'large'} centered/>
@@ -81,18 +84,20 @@ export default () => {
8184
</CSSTransition>
8285
}
8386
<Can action={'file.create'}>
84-
<div css={tw`flex flex-wrap-reverse justify-end mt-4`}>
85-
<NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/>
86-
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
87-
<NavLink
88-
to={`/server/${id}/files/new${window.location.hash}`}
89-
css={tw`flex-1 sm:flex-none sm:mt-0`}
90-
>
91-
<Button css={tw`w-full`}>
92-
New File
93-
</Button>
94-
</NavLink>
95-
</div>
87+
<ErrorBoundary>
88+
<div css={tw`flex flex-wrap-reverse justify-end mt-4`}>
89+
<NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/>
90+
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
91+
<NavLink
92+
to={`/server/${id}/files/new${window.location.hash}`}
93+
css={tw`flex-1 sm:flex-none sm:mt-0`}
94+
>
95+
<Button css={tw`w-full`}>
96+
New File
97+
</Button>
98+
</NavLink>
99+
</div>
100+
</ErrorBoundary>
96101
</Can>
97102
</>
98103
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import useFlash from '@/plugins/useFlash';
1313
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
1414
import { WithClassname } from '@/components/types';
1515
import FlashMessageRender from '@/components/FlashMessageRender';
16+
import ErrorBoundary from '@/components/elements/ErrorBoundary';
1617

1718
interface Values {
1819
directoryName: string;
@@ -92,9 +93,9 @@ export default ({ className }: WithClassname) => {
9293
<span css={tw`text-neutral-200`}>This directory will be created as</span>
9394
&nbsp;/home/container/
9495
<span css={tw`text-cyan-200`}>
95-
{decodeURIComponent(
96-
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
97-
)}
96+
{decodeURIComponent(encodeURIComponent(
97+
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, '')
98+
))}
9899
</span>
99100
</p>
100101
<div css={tw`flex justify-end`}>

resources/scripts/routers/ServerRouter.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import NetworkContainer from '@/components/server/network/NetworkContainer';
2828
import InstallListener from '@/components/server/InstallListener';
2929
import StartupContainer from '@/components/server/startup/StartupContainer';
3030
import requireServerPermission from '@/hoc/requireServerPermission';
31+
import ErrorBoundary from '@/components/elements/ErrorBoundary';
3132

3233
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
3334
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
@@ -120,7 +121,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
120121
message={'Please check back in a few minutes.'}
121122
/>
122123
:
123-
<>
124+
<ErrorBoundary>
124125
<TransitionRouter>
125126
<Switch location={location}>
126127
<Route path={`${match.path}`} component={ServerConsole} exact/>
@@ -173,7 +174,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
173174
<Route path={'*'} component={NotFound}/>
174175
</Switch>
175176
</TransitionRouter>
176-
</>
177+
</ErrorBoundary>
177178
}
178179
</>
179180
}

0 commit comments

Comments
 (0)