diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php index a175f390f..9e705651b 100644 --- a/app/Http/Controllers/Api/Client/Servers/FileController.php +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -6,6 +6,7 @@ use Carbon\CarbonImmutable; use Illuminate\Http\Response; use Pterodactyl\Models\Server; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Collection; use Pterodactyl\Services\Nodes\NodeJWTService; use Illuminate\Contracts\Routing\ResponseFactory; use Pterodactyl\Repositories\Wings\DaemonFileRepository; @@ -70,7 +71,7 @@ class FileController extends ClientApiController { $contents = $this->fileRepository ->setServer($server) - ->getDirectory(urlencode(urldecode($request->get('directory') ?? '/'))); + ->getDirectory($this->encode($request->get('directory') ?? '/')); return $this->fractal->collection($contents) ->transformWith($this->getTransformer(FileObjectTransformer::class)) @@ -91,7 +92,7 @@ class FileController extends ClientApiController { return new Response( $this->fileRepository->setServer($server)->getContent( - urlencode(urldecode($request->get('file'))), config('pterodactyl.files.max_edit_size') + $this->encode($request->get('file')), config('pterodactyl.files.max_edit_size') ), Response::HTTP_OK, ['Content-Type' => 'text/plain'] @@ -113,7 +114,7 @@ class FileController extends ClientApiController $token = $this->jwtService ->setExpiresAt(CarbonImmutable::now()->addMinutes(15)) ->setClaims([ - 'file_path' => $request->get('file'), + 'file_path' => rawurldecode($request->get('file')), 'server_uuid' => $server->uuid, ]) ->handle($server->node, $request->user()->id . $server->uuid); @@ -142,7 +143,7 @@ class FileController extends ClientApiController public function write(WriteFileContentRequest $request, Server $server): JsonResponse { $this->fileRepository->setServer($server)->putContent( - $request->get('file'), + $this->encode($request->get('file')), $request->getContent() ); @@ -261,4 +262,18 @@ class FileController extends ClientApiController return new JsonResponse([], Response::HTTP_NO_CONTENT); } + + /** + * Encodes a given file name & path in a format that should work for a good majority + * of file names without too much confusing logic. + * + * @param string $path + * @return string + */ + private function encode(string $path): string + { + return Collection::make(explode('/', rawurldecode($path)))->map(function ($value) { + return rawurlencode($value); + })->join('/'); + } } diff --git a/resources/scripts/api/server/files/getFileContents.ts b/resources/scripts/api/server/files/getFileContents.ts index cec8788b5..da380362d 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: file.split('/').map(item => encodeURIComponent(item)).join('/') }, + params: { file: encodeURI(decodeURI(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 1bfd7877f..d29cc1605 100644 --- a/resources/scripts/api/server/files/loadDirectory.ts +++ b/resources/scripts/api/server/files/loadDirectory.ts @@ -17,7 +17,7 @@ export interface FileObject { export default async (uuid: string, directory?: string): Promise => { const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, { - params: { directory: directory?.split('/').map(item => encodeURIComponent(item)).join('/') }, + params: { directory: encodeURI(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 b97e60a6b..7f6f44efc 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 }, + params: { file: encodeURI(decodeURI(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 8305e04bd..4dd519f8d 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, encodeURIComponent(name || hash.replace(/^#/, '')), content)) + .then(content => saveFileContents(uuid, name || 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 968a651db..11b7abde7 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(encodeURIComponent(directory)) }; + return { name: directory }; } - return { name: decodeURIComponent(encodeURIComponent(directory)), path: `/${dirs.slice(0, index + 1).join('/')}` }; + return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` }; }); const onSelectAllClick = (e: React.ChangeEvent) => { @@ -79,7 +79,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => { } {file && - {decodeURIComponent(encodeURIComponent(file))} + {decodeURI(file)} } diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index 31985e940..6b274f969 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -36,7 +36,7 @@ export default () => { useEffect(() => { clearFlashes('files'); setSelectedFiles([]); - setDirectory(hash.length > 0 ? hash : '/'); + setDirectory(hash.length > 0 ? decodeURI(hash) : '/'); }, [ hash ]); useEffect(() => { diff --git a/resources/scripts/components/server/files/FileObjectRow.tsx b/resources/scripts/components/server/files/FileObjectRow.tsx index 26c37ed21..f4ac06219 100644 --- a/resources/scripts/components/server/files/FileObjectRow.tsx +++ b/resources/scripts/components/server/files/FileObjectRow.tsx @@ -24,6 +24,8 @@ 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 onRowClick = (e: React.MouseEvent) => { // Don't rely on the onClick to work with the generated URL. Because of the way this // component re-renders you'll get redirected into a nested directory structure since @@ -32,7 +34,7 @@ const Clickable: React.FC<{ file: FileObject }> = memo(({ file, children }) => { // Just trust me future me, leave this be. if (!file.isFile) { e.preventDefault(); - history.push(`#${cleanDirectoryPath(`${directory}/${file.name}`)}`); + history.push(`#${destination}`); } }; @@ -43,7 +45,7 @@ const Clickable: React.FC<{ file: FileObject }> = memo(({ file, children }) => { : diff --git a/resources/scripts/components/server/files/NewDirectoryButton.tsx b/resources/scripts/components/server/files/NewDirectoryButton.tsx index 0056cf688..dc91377bc 100644 --- a/resources/scripts/components/server/files/NewDirectoryButton.tsx +++ b/resources/scripts/components/server/files/NewDirectoryButton.tsx @@ -92,9 +92,7 @@ export default ({ className }: WithClassname) => { This directory will be created as  /home/container/ - {decodeURIComponent(encodeURIComponent( - join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''), - ))} + {join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, '')}