Avoid breaking the entire UI when naughty characters are present in the file name or directory; closes #2575
This commit is contained in:
parent
65d04d0c05
commit
903b5795db
|
@ -1,18 +1,10 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
|
|
||||||
export default (uuid: string, file: string, content: string): Promise<void> => {
|
export default async (uuid: string, file: string, content: string): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
await http.post(`/api/client/servers/${uuid}/files/write`, content, {
|
||||||
http.post(
|
params: { file },
|
||||||
`/api/client/servers/${uuid}/files/write`,
|
headers: {
|
||||||
content,
|
'Content-Type': 'text/plain',
|
||||||
{
|
},
|
||||||
params: { file },
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/plain',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(reject);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
|
@ -16,6 +16,7 @@ import Select from '@/components/elements/Select';
|
||||||
import modes from '@/modes';
|
import modes from '@/modes';
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
|
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||||
|
|
||||||
const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor'));
|
const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor'));
|
||||||
|
|
||||||
|
@ -60,9 +61,7 @@ export default () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
clearFlashes('files:view');
|
clearFlashes('files:view');
|
||||||
fetchFileContent()
|
fetchFileContent()
|
||||||
.then(content => {
|
.then(content => saveFileContents(uuid, encodeURIComponent(name || hash.replace(/^#/, '')), content))
|
||||||
return saveFileContents(uuid, name || hash.replace(/^#/, ''), content);
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (name) {
|
if (name) {
|
||||||
history.push(`/server/${id}/files/edit#/${name}`);
|
history.push(`/server/${id}/files/edit#/${name}`);
|
||||||
|
@ -87,7 +86,9 @@ export default () => {
|
||||||
return (
|
return (
|
||||||
<PageContentBlock>
|
<PageContentBlock>
|
||||||
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
|
<FlashMessageRender byKey={'files:view'} css={tw`mb-4`}/>
|
||||||
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
|
<ErrorBoundary>
|
||||||
|
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'}/>
|
||||||
|
</ErrorBoundary>
|
||||||
{hash.replace(/^#/, '').endsWith('.pteroignore') &&
|
{hash.replace(/^#/, '').endsWith('.pteroignore') &&
|
||||||
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
|
<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`}>
|
<p css={tw`text-neutral-300 text-sm`}>
|
||||||
|
|
|
@ -33,10 +33,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
||||||
.filter(directory => !!directory)
|
.filter(directory => !!directory)
|
||||||
.map((directory, index, dirs) => {
|
.map((directory, index, dirs) => {
|
||||||
if (!withinFileEditor && index === dirs.length - 1) {
|
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>) => {
|
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
@ -79,7 +79,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
||||||
}
|
}
|
||||||
{file &&
|
{file &&
|
||||||
<React.Fragment>
|
<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>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,6 +17,7 @@ import MassActionsBar from '@/components/server/files/MassActionsBar';
|
||||||
import UploadButton from '@/components/server/files/UploadButton';
|
import UploadButton from '@/components/server/files/UploadButton';
|
||||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||||
import { useStoreActions } from '@/state/hooks';
|
import { useStoreActions } from '@/state/hooks';
|
||||||
|
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||||
|
|
||||||
const sortFiles = (files: FileObject[]): FileObject[] => {
|
const sortFiles = (files: FileObject[]): FileObject[] => {
|
||||||
return files.sort((a, b) => a.name.localeCompare(b.name))
|
return files.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
@ -50,7 +51,9 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServerContentBlock title={'File Manager'} showFlashKey={'files'}>
|
<ServerContentBlock title={'File Manager'} showFlashKey={'files'}>
|
||||||
<FileManagerBreadcrumbs/>
|
<ErrorBoundary>
|
||||||
|
<FileManagerBreadcrumbs/>
|
||||||
|
</ErrorBoundary>
|
||||||
{
|
{
|
||||||
!files ?
|
!files ?
|
||||||
<Spinner size={'large'} centered/>
|
<Spinner size={'large'} centered/>
|
||||||
|
@ -81,18 +84,20 @@ export default () => {
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
}
|
}
|
||||||
<Can action={'file.create'}>
|
<Can action={'file.create'}>
|
||||||
<div css={tw`flex flex-wrap-reverse justify-end mt-4`}>
|
<ErrorBoundary>
|
||||||
<NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/>
|
<div css={tw`flex flex-wrap-reverse justify-end mt-4`}>
|
||||||
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
|
<NewDirectoryButton css={tw`w-full flex-none mt-4 sm:mt-0 sm:w-auto sm:mr-4`}/>
|
||||||
<NavLink
|
<UploadButton css={tw`flex-1 mr-4 sm:flex-none sm:mt-0`}/>
|
||||||
to={`/server/${id}/files/new${window.location.hash}`}
|
<NavLink
|
||||||
css={tw`flex-1 sm:flex-none sm:mt-0`}
|
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 css={tw`w-full`}>
|
||||||
</Button>
|
New File
|
||||||
</NavLink>
|
</Button>
|
||||||
</div>
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
</Can>
|
</Can>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import useFlash from '@/plugins/useFlash';
|
||||||
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
|
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
|
||||||
import { WithClassname } from '@/components/types';
|
import { WithClassname } from '@/components/types';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
|
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
directoryName: string;
|
directoryName: string;
|
||||||
|
@ -92,9 +93,9 @@ export default ({ className }: WithClassname) => {
|
||||||
<span css={tw`text-neutral-200`}>This directory will be created as</span>
|
<span css={tw`text-neutral-200`}>This directory will be created as</span>
|
||||||
/home/container/
|
/home/container/
|
||||||
<span css={tw`text-cyan-200`}>
|
<span css={tw`text-cyan-200`}>
|
||||||
{decodeURIComponent(
|
{decodeURIComponent(encodeURIComponent(
|
||||||
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
|
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, '')
|
||||||
)}
|
))}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div css={tw`flex justify-end`}>
|
<div css={tw`flex justify-end`}>
|
||||||
|
|
|
@ -28,6 +28,7 @@ import NetworkContainer from '@/components/server/network/NetworkContainer';
|
||||||
import InstallListener from '@/components/server/InstallListener';
|
import InstallListener from '@/components/server/InstallListener';
|
||||||
import StartupContainer from '@/components/server/startup/StartupContainer';
|
import StartupContainer from '@/components/server/startup/StartupContainer';
|
||||||
import requireServerPermission from '@/hoc/requireServerPermission';
|
import requireServerPermission from '@/hoc/requireServerPermission';
|
||||||
|
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||||
|
|
||||||
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
|
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
|
||||||
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
|
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.'}
|
message={'Please check back in a few minutes.'}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<>
|
<ErrorBoundary>
|
||||||
<TransitionRouter>
|
<TransitionRouter>
|
||||||
<Switch location={location}>
|
<Switch location={location}>
|
||||||
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
||||||
|
@ -173,7 +174,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
<Route path={'*'} component={NotFound}/>
|
<Route path={'*'} component={NotFound}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</TransitionRouter>
|
</TransitionRouter>
|
||||||
</>
|
</ErrorBoundary>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue