diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php index 0e3a62f2e..a4b0ae283 100644 --- a/app/Http/Controllers/Api/Client/Servers/FileController.php +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -72,7 +72,7 @@ class FileController extends ClientApiController { $contents = $this->fileRepository ->setServer($server) - ->getDirectory($this->encode($request->get('directory') ?? '/')); + ->getDirectory(rawurldecode($request->get('directory') ?? '/')); return $this->fractal->collection($contents) ->transformWith($this->getTransformer(FileObjectTransformer::class)) @@ -93,7 +93,7 @@ class FileController extends ClientApiController { return new Response( $this->fileRepository->setServer($server)->getContent( - $this->encode($request->get('file')), config('pterodactyl.files.max_edit_size') + rawurldecode($request->get('file')), config('pterodactyl.files.max_edit_size') ), Response::HTTP_OK, ['Content-Type' => 'text/plain'] diff --git a/app/Transformers/Daemon/FileObjectTransformer.php b/app/Transformers/Daemon/FileObjectTransformer.php index f19d9028c..b2c1deed3 100644 --- a/app/Transformers/Daemon/FileObjectTransformer.php +++ b/app/Transformers/Daemon/FileObjectTransformer.php @@ -23,7 +23,7 @@ class FileObjectTransformer extends BaseDaemonTransformer public function transform(array $item) { return [ - 'name' => Arr::get($item, 'name'), + 'name' => rawurlencode(Arr::get($item, 'name')), 'mode' => Arr::get($item, 'mode'), 'mode_bits' => Arr::get($item, 'mode_bits'), 'size' => Arr::get($item, 'size'), diff --git a/resources/scripts/api/server/files/getFileContents.ts b/resources/scripts/api/server/files/getFileContents.ts index da380362d..ef25b1dbc 100644 --- a/resources/scripts/api/server/files/getFileContents.ts +++ b/resources/scripts/api/server/files/getFileContents.ts @@ -3,7 +3,7 @@ import http from '@/api/http'; export default (server: string, file: string): Promise => { return new Promise((resolve, reject) => { http.get(`/api/client/servers/${server}/files/contents`, { - params: { file: encodeURI(decodeURI(file)) }, + params: { file }, transformResponse: res => res, responseType: 'text', }) diff --git a/resources/scripts/api/server/files/loadDirectory.ts b/resources/scripts/api/server/files/loadDirectory.ts index 52bf8853e..985b5c94d 100644 --- a/resources/scripts/api/server/files/loadDirectory.ts +++ b/resources/scripts/api/server/files/loadDirectory.ts @@ -18,7 +18,9 @@ export interface FileObject { export default async (uuid: string, directory?: string): Promise => { const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, { - params: { directory: encodeURI(directory ?? '/') }, + // At this point the directory is still encoded so we need to decode it since axios + // will automatically re-encode this value before sending it along in the request. + params: { directory: decodeURI(directory ?? '/') }, }); return (data.data || []).map(rawDataToFileObject); diff --git a/resources/scripts/api/server/files/saveFileContents.ts b/resources/scripts/api/server/files/saveFileContents.ts index 7f6f44efc..b97e60a6b 100644 --- a/resources/scripts/api/server/files/saveFileContents.ts +++ b/resources/scripts/api/server/files/saveFileContents.ts @@ -2,7 +2,7 @@ import http from '@/api/http'; export default async (uuid: string, file: string, content: string): Promise => { await http.post(`/api/client/servers/${uuid}/files/write`, content, { - params: { file: encodeURI(decodeURI(file)) }, + params: { file }, headers: { 'Content-Type': 'text/plain', }, diff --git a/resources/scripts/components/server/files/FileEditContainer.tsx b/resources/scripts/components/server/files/FileEditContainer.tsx index bf12909b8..f26baf8b0 100644 --- a/resources/scripts/components/server/files/FileEditContainer.tsx +++ b/resources/scripts/components/server/files/FileEditContainer.tsx @@ -61,7 +61,7 @@ export default () => { setLoading(true); clearFlashes('files:view'); fetchFileContent() - .then(content => saveFileContents(uuid, name || hash.replace(/^#/, ''), content)) + .then(content => saveFileContents(uuid, name || decodeURI(hash.replace(/^#/, '')), content)) .then(() => { if (name) { history.push(`/server/${id}/files/edit#/${name}`); diff --git a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx index 00dcd949d..7353edadf 100644 --- a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx +++ b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { ServerContext } from '@/state/server'; -import { NavLink } from 'react-router-dom'; +import { NavLink, useLocation } from 'react-router-dom'; import { cleanDirectoryPath } from '@/helpers'; import tw from 'twin.macro'; @@ -14,14 +14,28 @@ export default ({ renderLeft, withinFileEditor, isNewFile }: Props) => { const [ file, setFile ] = useState(null); const id = ServerContext.useStoreState(state => state.server.data!.id); const directory = ServerContext.useStoreState(state => state.files.directory); + const { hash } = useLocation(); useEffect(() => { - const parts = cleanDirectoryPath(window.location.hash).split('/'); + let pathHash = cleanDirectoryPath(hash); + try { + pathHash = decodeURI(pathHash); + } catch (e) { + console.warn('Error decoding URL parts in hash:', e); + } if (withinFileEditor && !isNewFile) { - setFile(parts.pop() || null); + let name = pathHash.split('/').pop() || null; + if (name) { + try { + name = decodeURIComponent(name); + } catch (e) { + console.warn('Error decoding filename:', e); + } + } + setFile(name); } - }, [ withinFileEditor, isNewFile ]); + }, [ withinFileEditor, isNewFile, hash ]); const breadcrumbs = (): { name: string; path?: string }[] => directory.split('/') .filter(directory => !!directory) @@ -51,16 +65,16 @@ export default ({ renderLeft, withinFileEditor, isNewFile }: Props) => { to={`/server/${id}/files#${crumb.path}`} css={tw`px-1 text-neutral-200 no-underline hover:text-neutral-100`} > - {crumb.name} + {decodeURIComponent(crumb.name)} / : - {crumb.name} + {decodeURIComponent(crumb.name)} )) } {file && - {decodeURI(file)} + {file} } diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index 692f4d9ea..613a1baaa 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -39,7 +39,7 @@ export default () => { useEffect(() => { clearFlashes('files'); setSelectedFiles([]); - setDirectory(hash.length > 0 ? decodeURI(hash) : '/'); + setDirectory(hash.length > 0 ? hash : '/'); }, [ hash ]); useEffect(() => { diff --git a/resources/scripts/components/server/files/FileObjectRow.tsx b/resources/scripts/components/server/files/FileObjectRow.tsx index 65e784828..fd18bd7ba 100644 --- a/resources/scripts/components/server/files/FileObjectRow.tsx +++ b/resources/scripts/components/server/files/FileObjectRow.tsx @@ -24,7 +24,7 @@ const Clickable: React.FC<{ file: FileObject }> = memo(({ file, children }) => { const history = useHistory(); const match = useRouteMatch(); - const destination = cleanDirectoryPath(`${directory}/${file.name}`).split('/').map(v => encodeURI(v)).join('/'); + const destination = cleanDirectoryPath(`${directory}/${file.name}`).split('/').join('/'); const onRowClick = (e: React.MouseEvent) => { // Don't rely on the onClick to work with the generated URL. Because of the way this @@ -72,7 +72,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => ( }
- {file.name} + {decodeURIComponent(file.name)}
{file.isFile &&