Avoid breaking the entire UI when naughty characters are present in the file name or directory; closes #2575

This commit is contained in:
Dane Everitt 2020-10-22 21:18:46 -07:00
parent 65d04d0c05
commit 903b5795db
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
7 changed files with 78 additions and 39 deletions

View File

@ -1,18 +1,10 @@
import http from '@/api/http';
export default (uuid: string, file: string, content: string): Promise<void> => {
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<void> => {
await http.post(`/api/client/servers/${uuid}/files/write`, content, {
params: { file },
headers: {
'Content-Type': 'text/plain',
},
});
};

View File

@ -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 ?
<div css={tw`flex items-center justify-center w-full my-4`}>
<div css={tw`flex items-center bg-neutral-900 rounded p-3 text-red-500`}>
<Icon icon={faExclamationTriangle} css={tw`h-4 w-auto mr-2`}/>
<p css={tw`text-sm text-neutral-100`}>
An error was encountered by the application while rendering this view. Try refreshing the page.
</p>
</div>
</div>
:
this.props.children;
}
}
export default ErrorBoundary;

View File

@ -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 (
<PageContentBlock>
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
<ErrorBoundary>
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
</ErrorBoundary>
{hash.replace(/^#/, '').endsWith('.pteroignore') &&
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
<p css={tw`text-neutral-300 text-sm`}>

View File

@ -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<HTMLInputElement>) => {
@ -79,7 +79,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
}
{file &&
<React.Fragment>
<span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(file)}</span>
<span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(encodeURIComponent(file))}</span>
</React.Fragment>
}
</div>

View File

@ -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 (
<ServerContentBlock title={'File Manager'} showFlashKey={'files'}>
<FileManagerBreadcrumbs/>
<ErrorBoundary>
<FileManagerBreadcrumbs/>
</ErrorBoundary>
{
!files ?
<Spinner size={'large'} centered/>
@ -81,18 +84,20 @@ export default () => {
</CSSTransition>
}
<Can action={'file.create'}>
<div css={tw`flex flex-wrap-reverse justify-end mt-4`}>
<NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/>
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
<NavLink
to={`/server/${id}/files/new${window.location.hash}`}
css={tw`flex-1 sm:flex-none sm:mt-0`}
>
<Button css={tw`w-full`}>
New File
</Button>
</NavLink>
</div>
<ErrorBoundary>
<div css={tw`flex flex-wrap-reverse justify-end mt-4`}>
<NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/>
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
<NavLink
to={`/server/${id}/files/new${window.location.hash}`}
css={tw`flex-1 sm:flex-none sm:mt-0`}
>
<Button css={tw`w-full`}>
New File
</Button>
</NavLink>
</div>
</ErrorBoundary>
</Can>
</>
}

View File

@ -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) => {
<span css={tw`text-neutral-200`}>This directory will be created as</span>
&nbsp;/home/container/
<span css={tw`text-cyan-200`}>
{decodeURIComponent(
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
)}
{decodeURIComponent(encodeURIComponent(
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, '')
))}
</span>
</p>
<div css={tw`flex justify-end`}>

View File

@ -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.'}
/>
:
<>
<ErrorBoundary>
<TransitionRouter>
<Switch location={location}>
<Route path={`${match.path}`} component={ServerConsole} exact/>
@ -173,7 +174,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
<Route path={'*'} component={NotFound}/>
</Switch>
</TransitionRouter>
</>
</ErrorBoundary>
}
</>
}