From 903b5795dbf74f1ca01093b799703792faa7d0ad Mon Sep 17 00:00:00 2001
From: Dane Everitt
Date: Thu, 22 Oct 2020 21:18:46 -0700
Subject: [PATCH] Avoid breaking the entire UI when naughty characters are
present in the file name or directory; closes #2575
---
.../api/server/files/saveFileContents.ts | 20 +++-------
.../components/elements/ErrorBoundary.tsx | 39 +++++++++++++++++++
.../server/files/FileEditContainer.tsx | 9 +++--
.../server/files/FileManagerBreadcrumbs.tsx | 6 +--
.../server/files/FileManagerContainer.tsx | 31 ++++++++-------
.../server/files/NewDirectoryButton.tsx | 7 ++--
resources/scripts/routers/ServerRouter.tsx | 5 ++-
7 files changed, 78 insertions(+), 39 deletions(-)
create mode 100644 resources/scripts/components/elements/ErrorBoundary.tsx
diff --git a/resources/scripts/api/server/files/saveFileContents.ts b/resources/scripts/api/server/files/saveFileContents.ts
index 22f1766c..b97e60a6 100644
--- a/resources/scripts/api/server/files/saveFileContents.ts
+++ b/resources/scripts/api/server/files/saveFileContents.ts
@@ -1,18 +1,10 @@
import http from '@/api/http';
-export default (uuid: string, file: string, content: string): Promise => {
- return new Promise((resolve, reject) => {
- http.post(
- `/api/client/servers/${uuid}/files/write`,
- content,
- {
- params: { file },
- headers: {
- 'Content-Type': 'text/plain',
- },
- },
- )
- .then(() => resolve())
- .catch(reject);
+export default async (uuid: string, file: string, content: string): Promise => {
+ await http.post(`/api/client/servers/${uuid}/files/write`, content, {
+ params: { file },
+ headers: {
+ 'Content-Type': 'text/plain',
+ },
});
};
diff --git a/resources/scripts/components/elements/ErrorBoundary.tsx b/resources/scripts/components/elements/ErrorBoundary.tsx
new file mode 100644
index 00000000..418d9e06
--- /dev/null
+++ b/resources/scripts/components/elements/ErrorBoundary.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import tw from 'twin.macro';
+import Icon from '@/components/elements/Icon';
+import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+
+interface State {
+ hasError: boolean;
+}
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+class ErrorBoundary extends React.Component<{}, State> {
+ state: State = {
+ hasError: false,
+ };
+
+ static getDerivedStateFromError () {
+ return { hasError: true };
+ }
+
+ componentDidCatch (error: Error) {
+ console.error(error);
+ }
+
+ render () {
+ return this.state.hasError ?
+
+
+
+
+ An error was encountered by the application while rendering this view. Try refreshing the page.
+
+
+
+ :
+ this.props.children;
+ }
+}
+
+export default ErrorBoundary;
diff --git a/resources/scripts/components/server/files/FileEditContainer.tsx b/resources/scripts/components/server/files/FileEditContainer.tsx
index 1faf99e5..8305e04b 100644
--- a/resources/scripts/components/server/files/FileEditContainer.tsx
+++ b/resources/scripts/components/server/files/FileEditContainer.tsx
@@ -16,6 +16,7 @@ import Select from '@/components/elements/Select';
import modes from '@/modes';
import useFlash from '@/plugins/useFlash';
import { ServerContext } from '@/state/server';
+import ErrorBoundary from '@/components/elements/ErrorBoundary';
const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor'));
@@ -60,9 +61,7 @@ export default () => {
setLoading(true);
clearFlashes('files:view');
fetchFileContent()
- .then(content => {
- return saveFileContents(uuid, name || hash.replace(/^#/, ''), content);
- })
+ .then(content => saveFileContents(uuid, encodeURIComponent(name || hash.replace(/^#/, '')), content))
.then(() => {
if (name) {
history.push(`/server/${id}/files/edit#/${name}`);
@@ -87,7 +86,9 @@ export default () => {
return (
-
+
+
+
{hash.replace(/^#/, '').endsWith('.pteroignore') &&
diff --git a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx
index ba43dbd6..968a651d 100644
--- a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx
+++ b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx
@@ -33,10 +33,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
.filter(directory => !!directory)
.map((directory, index, dirs) => {
if (!withinFileEditor && index === dirs.length - 1) {
- return { name: decodeURIComponent(directory) };
+ return { name: decodeURIComponent(encodeURIComponent(directory)) };
}
- return { name: decodeURIComponent(directory), path: `/${dirs.slice(0, index + 1).join('/')}` };
+ return { name: decodeURIComponent(encodeURIComponent(directory)), path: `/${dirs.slice(0, index + 1).join('/')}` };
});
const onSelectAllClick = (e: React.ChangeEvent) => {
@@ -79,7 +79,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
}
{file &&
- {decodeURIComponent(file)}
+ {decodeURIComponent(encodeURIComponent(file))}
}
diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx
index 4c46edc1..31985e94 100644
--- a/resources/scripts/components/server/files/FileManagerContainer.tsx
+++ b/resources/scripts/components/server/files/FileManagerContainer.tsx
@@ -17,6 +17,7 @@ import MassActionsBar from '@/components/server/files/MassActionsBar';
import UploadButton from '@/components/server/files/UploadButton';
import ServerContentBlock from '@/components/elements/ServerContentBlock';
import { useStoreActions } from '@/state/hooks';
+import ErrorBoundary from '@/components/elements/ErrorBoundary';
const sortFiles = (files: FileObject[]): FileObject[] => {
return files.sort((a, b) => a.name.localeCompare(b.name))
@@ -50,7 +51,9 @@ export default () => {
return (
-
+
+
+
{
!files ?
@@ -81,18 +84,20 @@ export default () => {
}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
>
}
diff --git a/resources/scripts/components/server/files/NewDirectoryButton.tsx b/resources/scripts/components/server/files/NewDirectoryButton.tsx
index 2be673db..e43f0aff 100644
--- a/resources/scripts/components/server/files/NewDirectoryButton.tsx
+++ b/resources/scripts/components/server/files/NewDirectoryButton.tsx
@@ -13,6 +13,7 @@ import useFlash from '@/plugins/useFlash';
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
import { WithClassname } from '@/components/types';
import FlashMessageRender from '@/components/FlashMessageRender';
+import ErrorBoundary from '@/components/elements/ErrorBoundary';
interface Values {
directoryName: string;
@@ -92,9 +93,9 @@ export default ({ className }: WithClassname) => {
This directory will be created as
/home/container/
- {decodeURIComponent(
- join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
- )}
+ {decodeURIComponent(encodeURIComponent(
+ join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, '')
+ ))}
diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx
index a90ff652..ec6bc20b 100644
--- a/resources/scripts/routers/ServerRouter.tsx
+++ b/resources/scripts/routers/ServerRouter.tsx
@@ -28,6 +28,7 @@ import NetworkContainer from '@/components/server/network/NetworkContainer';
import InstallListener from '@/components/server/InstallListener';
import StartupContainer from '@/components/server/startup/StartupContainer';
import requireServerPermission from '@/hoc/requireServerPermission';
+import ErrorBoundary from '@/components/elements/ErrorBoundary';
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
@@ -120,7 +121,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
message={'Please check back in a few minutes.'}
/>
:
- <>
+
@@ -173,7 +174,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
- >
+
}
>
}