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 }>) - + } }