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
7 changed files with 78 additions and 39 deletions
|
@ -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',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
39
resources/scripts/components/elements/ErrorBoundary.tsx
Normal file
39
resources/scripts/components/elements/ErrorBoundary.tsx
Normal 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;
|
|
@ -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`}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
/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`}>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue