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(/^(\.\.\/|\/)+/, '')}