From dac592b513778b2e7de3450c5ce6ef1d4a2af4bd Mon Sep 17 00:00:00 2001 From: Daniel Burger <48986191+danielburger1337@users.noreply.github.com> Date: Wed, 8 Nov 2023 05:54:37 +0100 Subject: [PATCH] [HttpFoundation] Add `UploadedFile::getClientOriginalPath()` to support directory uploads --- .../Component/Form/NativeRequestHandler.php | 11 +-- .../Form/Tests/NativeRequestHandlerTest.php | 4 + .../Component/HttpFoundation/CHANGELOG.md | 5 ++ .../HttpFoundation/File/UploadedFile.php | 17 +++++ .../Component/HttpFoundation/FileBag.php | 22 +++--- .../Fixtures/webkitdirectory/nested/test.txt | 1 + .../File/Fixtures/webkitdirectory/test.txt | 1 + .../Tests/File/UploadedFileTest.php | 22 ++++++ .../HttpFoundation/Tests/FileBagTest.php | 76 +++++++++++++------ 9 files changed, 120 insertions(+), 39 deletions(-) create mode 100644 src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/nested/test.txt create mode 100644 src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/test.txt diff --git a/src/Symfony/Component/Form/NativeRequestHandler.php b/src/Symfony/Component/Form/NativeRequestHandler.php index 9ac1f2ea9ae27..8c74bd1ded8ae 100644 --- a/src/Symfony/Component/Form/NativeRequestHandler.php +++ b/src/Symfony/Component/Form/NativeRequestHandler.php @@ -29,6 +29,7 @@ class NativeRequestHandler implements RequestHandlerInterface */ private const FILE_KEYS = [ 'error', + 'full_path', 'name', 'size', 'tmp_name', @@ -186,9 +187,7 @@ private static function fixPhpFilesArray(mixed $data): mixed return $data; } - // Remove extra key added by PHP 8.1. - unset($data['full_path']); - $keys = array_keys($data); + $keys = array_keys($data + ['full_path' => null]); sort($keys); if (self::FILE_KEYS !== $keys || !isset($data['name']) || !\is_array($data['name'])) { @@ -207,7 +206,9 @@ private static function fixPhpFilesArray(mixed $data): mixed 'type' => $data['type'][$key], 'tmp_name' => $data['tmp_name'][$key], 'size' => $data['size'][$key], - ]); + ] + (isset($data['full_path'][$key]) ? [ + 'full_path' => $data['full_path'][$key], + ] : [])); } return $files; @@ -219,7 +220,7 @@ private static function stripEmptyFiles(mixed $data): mixed return $data; } - $keys = array_keys($data); + $keys = array_keys($data + ['full_path' => null]); sort($keys); if (self::FILE_KEYS === $keys) { diff --git a/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php b/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php index bdb0763f9d50f..679c3366d8256 100644 --- a/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php +++ b/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php @@ -99,6 +99,9 @@ public function testFixBuggyFilesArray() 'name' => [ 'field' => 'upload.txt', ], + 'full_path' => [ + 'field' => 'path/to/file/upload.txt', + ], 'type' => [ 'field' => 'text/plain', ], @@ -118,6 +121,7 @@ public function testFixBuggyFilesArray() $this->assertTrue($form->isSubmitted()); $this->assertEquals([ 'name' => 'upload.txt', + 'full_path' => 'path/to/file/upload.txt', 'type' => 'text/plain', 'tmp_name' => 'owfdskjasdfsa', 'error' => \UPLOAD_ERR_OK, diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index 1a3ef0e411ea1..d4d07411f70e7 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `UploadedFile::getClientOriginalPath()` + 7.0 --- diff --git a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php index e27cf3812d3c5..b0a01f30f68b6 100644 --- a/src/Symfony/Component/HttpFoundation/File/UploadedFile.php +++ b/src/Symfony/Component/HttpFoundation/File/UploadedFile.php @@ -35,6 +35,7 @@ class UploadedFile extends File private string $originalName; private string $mimeType; private int $error; + private string $originalPath; /** * Accepts the information of the uploaded file as provided by the PHP global $_FILES. @@ -63,6 +64,7 @@ class UploadedFile extends File public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, bool $test = false) { $this->originalName = $this->getName($originalName); + $this->originalPath = strtr($originalName, '\\', '/'); $this->mimeType = $mimeType ?: 'application/octet-stream'; $this->error = $error ?: \UPLOAD_ERR_OK; $this->test = $test; @@ -92,6 +94,21 @@ public function getClientOriginalExtension(): string return pathinfo($this->originalName, \PATHINFO_EXTENSION); } + /** + * Returns the original file full path. + * + * It is extracted from the request from which the file has been uploaded. + * This should not be considered as a safe value to use for a file name/path on your servers. + * + * If this file was uploaded with the "webkitdirectory" upload directive, this will contain + * the path of the file relative to the uploaded root directory. Otherwise this will be identical + * to getClientOriginalName(). + */ + public function getClientOriginalPath(): string + { + return $this->originalPath; + } + /** * Returns the file mime type. * diff --git a/src/Symfony/Component/HttpFoundation/FileBag.php b/src/Symfony/Component/HttpFoundation/FileBag.php index 0541750bb2304..561e7cdea7912 100644 --- a/src/Symfony/Component/HttpFoundation/FileBag.php +++ b/src/Symfony/Component/HttpFoundation/FileBag.php @@ -21,7 +21,7 @@ */ class FileBag extends ParameterBag { - private const FILE_KEYS = ['error', 'name', 'size', 'tmp_name', 'type']; + private const FILE_KEYS = ['error', 'full_path', 'name', 'size', 'tmp_name', 'type']; /** * @param array|UploadedFile[] $parameters An array of HTTP files @@ -65,18 +65,18 @@ protected function convertFileInformation(array|UploadedFile $file): array|Uploa } $file = $this->fixPhpFilesArray($file); - $keys = array_keys($file); + $keys = array_keys($file + ['full_path' => null]); sort($keys); - if (self::FILE_KEYS == $keys) { - if (\UPLOAD_ERR_NO_FILE == $file['error']) { + if (self::FILE_KEYS === $keys) { + if (\UPLOAD_ERR_NO_FILE === $file['error']) { $file = null; } else { - $file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error'], false); + $file = new UploadedFile($file['tmp_name'], $file['full_path'] ?? $file['name'], $file['type'], $file['error'], false); } } else { $file = array_map(fn ($v) => $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v, $file); - if (array_keys($keys) === $keys) { + if (array_is_list($file)) { $file = array_filter($file); } } @@ -98,12 +98,10 @@ protected function convertFileInformation(array|UploadedFile $file): array|Uploa */ protected function fixPhpFilesArray(array $data): array { - // Remove extra key added by PHP 8.1. - unset($data['full_path']); - $keys = array_keys($data); + $keys = array_keys($data + ['full_path' => null]); sort($keys); - if (self::FILE_KEYS != $keys || !isset($data['name']) || !\is_array($data['name'])) { + if (self::FILE_KEYS !== $keys || !isset($data['name']) || !\is_array($data['name'])) { return $data; } @@ -119,7 +117,9 @@ protected function fixPhpFilesArray(array $data): array 'type' => $data['type'][$key], 'tmp_name' => $data['tmp_name'][$key], 'size' => $data['size'][$key], - ]); + ] + (isset($data['full_path'][$key]) ? [ + 'full_path' => $data['full_path'][$key], + ] : [])); } return $files; diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/nested/test.txt b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/nested/test.txt new file mode 100644 index 0000000000000..83e5e03f72d03 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/nested/test.txt @@ -0,0 +1 @@ +nested webkitdirectory text diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/test.txt b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/test.txt new file mode 100644 index 0000000000000..0d872b4804a73 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/webkitdirectory/test.txt @@ -0,0 +1 @@ +webkitdirectory text diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php b/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php index 69179fc37ef74..9c18ad1839420 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/File/UploadedFileTest.php @@ -322,4 +322,26 @@ public function testGetMaxFilesize() $this->assertSame(\PHP_INT_MAX, $size); } } + + public function testgetClientOriginalPath() + { + $file = new UploadedFile( + __DIR__.'/Fixtures/test.gif', + 'test.gif', + 'image/gif' + ); + + $this->assertEquals('test.gif', $file->getClientOriginalPath()); + } + + public function testgetClientOriginalPathWebkitDirectory() + { + $file = new UploadedFile( + __DIR__.'/Fixtures/webkitdirectory/test.txt', + 'webkitdirectory/test.txt', + 'text/plain', + ); + + $this->assertEquals('webkitdirectory/test.txt', $file->getClientOriginalPath()); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/FileBagTest.php b/src/Symfony/Component/HttpFoundation/Tests/FileBagTest.php index b12621e7dd464..1afc61d2ad64e 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/FileBagTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/FileBagTest.php @@ -32,27 +32,12 @@ public function testFileMustBeAnArrayOrUploadedFile() public function testShouldConvertsUploadedFiles() { $tmpFile = $this->createTempFile(); - $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain'); + $name = basename($tmpFile); - $bag = new FileBag(['file' => [ - 'name' => basename($tmpFile), - 'type' => 'text/plain', - 'tmp_name' => $tmpFile, - 'error' => 0, - 'size' => null, - ]]); - - $this->assertEquals($file, $bag->get('file')); - } - - public function testShouldConvertsUploadedFilesPhp81() - { - $tmpFile = $this->createTempFile(); - $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain'); + $file = new UploadedFile($tmpFile, $name, 'text/plain'); $bag = new FileBag(['file' => [ - 'name' => basename($tmpFile), - 'full_path' => basename($tmpFile), + 'name' => $name, 'type' => 'text/plain', 'tmp_name' => $tmpFile, 'error' => 0, @@ -104,12 +89,13 @@ public function testShouldNotRemoveEmptyUploadedFilesForAssociativeArray() public function testShouldConvertUploadedFilesWithPhpBug() { $tmpFile = $this->createTempFile(); - $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain'); + $name = basename($tmpFile); + $file = new UploadedFile($tmpFile, $name, 'text/plain'); $bag = new FileBag([ 'child' => [ 'name' => [ - 'file' => basename($tmpFile), + 'file' => $name, ], 'type' => [ 'file' => 'text/plain', @@ -133,12 +119,13 @@ public function testShouldConvertUploadedFilesWithPhpBug() public function testShouldConvertNestedUploadedFilesWithPhpBug() { $tmpFile = $this->createTempFile(); - $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain'); + $name = basename($tmpFile); + $file = new UploadedFile($tmpFile, $name, 'text/plain'); $bag = new FileBag([ 'child' => [ 'name' => [ - 'sub' => ['file' => basename($tmpFile)], + 'sub' => ['file' => $name], ], 'type' => [ 'sub' => ['file' => 'text/plain'], @@ -162,13 +149,56 @@ public function testShouldConvertNestedUploadedFilesWithPhpBug() public function testShouldNotConvertNestedUploadedFiles() { $tmpFile = $this->createTempFile(); - $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain'); + $name = basename($tmpFile); + $file = new UploadedFile($tmpFile, $name, 'text/plain'); $bag = new FileBag(['image' => ['file' => $file]]); $files = $bag->all(); $this->assertEquals($file, $files['image']['file']); } + public function testWebkitDirectoryUpload() + { + $file1 = __DIR__.'/File/Fixtures/webkitdirectory/test.txt'; + $file2 = __DIR__.'/File/Fixtures/webkitdirectory/nested/test.txt'; + + $bag = new FileBag([ + 'child' => [ + 'name' => [ + 'test.txt', + 'test.txt', + ], + 'full_path' => [ + 'webkitdirectory/test.txt', + 'webkitdirectory/nested/test.txt', + ], + 'type' => [ + 'text/plain', + 'text/plain', + ], + 'tmp_name' => [ + $file1, + $file2, + ], + 'error' => [ + 0, 0, + ], + 'size' => [ + null, null, + ], + ], + ]); + + /** @var UploadedFile[] */ + $files = $bag->get('child'); + + $this->assertEquals('test.txt', $files[0]->getClientOriginalName()); + $this->assertEquals('test.txt', $files[1]->getClientOriginalName()); + + $this->assertEquals('webkitdirectory/test.txt', $files[0]->getClientOriginalPath()); + $this->assertEquals('webkitdirectory/nested/test.txt', $files[1]->getClientOriginalPath()); + } + protected function createTempFile() { $tempFile = tempnam(sys_get_temp_dir().'/form_test', 'FormTest');