From 6a0e9977e6b4b46747808283a33e70fc4c34718f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 4 Feb 2020 10:29:10 +0100 Subject: [PATCH 1/2] Fix CS --- Client.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Client.php b/Client.php index 3135fbce..8db4e68e 100644 --- a/Client.php +++ b/Client.php @@ -199,7 +199,7 @@ public function getCookieJar() public function getCrawler() { if (null === $this->crawler) { - @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', static::class.'::'.__FUNCTION__), E_USER_DEPRECATED); // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); } @@ -214,7 +214,7 @@ public function getCrawler() public function getInternalResponse() { if (null === $this->internalResponse) { - @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', static::class.'::'.__FUNCTION__), E_USER_DEPRECATED); // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); } @@ -234,7 +234,7 @@ public function getInternalResponse() public function getResponse() { if (null === $this->response) { - @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', static::class.'::'.__FUNCTION__), E_USER_DEPRECATED); // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); } @@ -249,7 +249,7 @@ public function getResponse() public function getInternalRequest() { if (null === $this->internalRequest) { - @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', static::class.'::'.__FUNCTION__), E_USER_DEPRECATED); // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); } @@ -269,7 +269,7 @@ public function getInternalRequest() public function getRequest() { if (null === $this->request) { - @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', static::class.'::'.__FUNCTION__), E_USER_DEPRECATED); // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); } @@ -314,8 +314,8 @@ public function clickLink(string $linkText): Crawler */ public function submit(Form $form, array $values = []/*, array $serverParameters = []*/) { - if (\func_num_args() < 3 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { - @trigger_error(sprintf('The "%s()" method will have a new "array $serverParameters = []" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + if (\func_num_args() < 3 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { + @trigger_error(sprintf('The "%s()" method will have a new "array $serverParameters = []" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', static::class.'::'.__FUNCTION__), E_USER_DEPRECATED); } $form->setValues($values); From 090ce406505149d6852a7c03b0346dec3b8cf612 Mon Sep 17 00:00:00 2001 From: Anna Filina Date: Sat, 22 Feb 2020 09:12:42 -0500 Subject: [PATCH 2/2] [BrowserKit] Nested file array prevents uploading file --- HttpBrowser.php | 35 ++++++++++----- Tests/HttpBrowserTest.php | 94 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 114 insertions(+), 15 deletions(-) diff --git a/HttpBrowser.php b/HttpBrowser.php index b2331ea4..a1e6dd9a 100644 --- a/HttpBrowser.php +++ b/HttpBrowser.php @@ -73,18 +73,9 @@ private function getBodyAndExtraHeaders(Request $request): array } $fields = $request->getParameters(); - $hasFile = false; - foreach ($request->getFiles() as $name => $file) { - if (!isset($file['tmp_name'])) { - continue; - } - - $hasFile = true; - $fields[$name] = DataPart::fromPath($file['tmp_name'], $file['name']); - } - if ($hasFile) { - $part = new FormDataPart($fields); + if ($uploadedFiles = $this->getUploadedFiles($request->getFiles())) { + $part = new FormDataPart($uploadedFiles); return [$part->bodyToIterable(), $part->getPreparedHeaders()->toArray()]; } @@ -119,4 +110,26 @@ private function getHeaders(Request $request): array return $headers; } + + /** + * Recursively go through the list. If the file has a tmp_name, convert it to a DataPart. + * Keep the original hierarchy. + */ + private function getUploadedFiles(array $files): array + { + $uploadedFiles = []; + foreach ($files as $name => $file) { + if (!\is_array($file)) { + return $uploadedFiles; + } + if (!isset($file['tmp_name'])) { + $uploadedFiles[$name] = $this->getUploadedFiles($file); + } + if (isset($file['tmp_name'])) { + $uploadedFiles[$name] = DataPart::fromPath($file['tmp_name'], $file['name']); + } + } + + return $uploadedFiles; + } } diff --git a/Tests/HttpBrowserTest.php b/Tests/HttpBrowserTest.php index 44eed997..fa3d531a 100644 --- a/Tests/HttpBrowserTest.php +++ b/Tests/HttpBrowserTest.php @@ -27,17 +27,17 @@ public function getBrowser(array $server = [], History $history = null, CookieJa /** * @dataProvider validContentTypes */ - public function testRequestHeaders(array $request, array $exepectedCall) + public function testRequestHeaders(array $requestArguments, array $expectedArguments) { $client = $this->createMock(HttpClientInterface::class); $client ->expects($this->once()) ->method('request') - ->with(...$exepectedCall) + ->with(...$expectedArguments) ->willReturn($this->createMock(ResponseInterface::class)); $browser = new HttpBrowser($client); - $browser->request(...$request); + $browser->request(...$requestArguments); } public function validContentTypes() @@ -61,7 +61,7 @@ public function validContentTypes() ]; } - public function testMultiPartRequest() + public function testMultiPartRequestWithSingleFile() { $client = $this->createMock(HttpClientInterface::class); $client @@ -81,4 +81,90 @@ public function testMultiPartRequest() file_put_contents($path, 'my_file'); $browser->request('POST', 'http://example.com/', [], ['file' => ['tmp_name' => $path, 'name' => 'foo']]); } + + public function testMultiPartRequestWithNormalFlatArray() + { + $client = $this->createMock(HttpClientInterface::class); + $this->expectClientToSendRequestWithFiles($client, ['file1_content', 'file2_content']); + + $browser = new HttpBrowser($client); + $browser->request('POST', 'http://example.com/', [], [ + 'file1' => $this->getUploadedFile('file1'), + 'file2' => $this->getUploadedFile('file2'), + ]); + } + + public function testMultiPartRequestWithNormalNestedArray() + { + $client = $this->createMock(HttpClientInterface::class); + $this->expectClientToSendRequestWithFiles($client, ['file1_content', 'file2_content']); + + $browser = new HttpBrowser($client); + $browser->request('POST', 'http://example.com/', [], [ + 'level1' => [ + 'level2' => [ + 'file1' => $this->getUploadedFile('file1'), + 'file2' => $this->getUploadedFile('file2'), + ], + ], + ]); + } + + public function testMultiPartRequestWithBracketedArray() + { + $client = $this->createMock(HttpClientInterface::class); + $this->expectClientToSendRequestWithFiles($client, ['file1_content', 'file2_content']); + + $browser = new HttpBrowser($client); + $browser->request('POST', 'http://example.com/', [], [ + 'form[file1]' => $this->getUploadedFile('file1'), + 'form[file2]' => $this->getUploadedFile('file2'), + ]); + } + + public function testMultiPartRequestWithInvalidItem() + { + $client = $this->createMock(HttpClientInterface::class); + $this->expectClientToSendRequestWithFiles($client, ['file1_content']); + + $browser = new HttpBrowser($client); + $browser->request('POST', 'http://example.com/', [], [ + 'file1' => $this->getUploadedFile('file1'), + 'file2' => 'INVALID', + ]); + } + + private function uploadFile(string $data): string + { + $path = tempnam(sys_get_temp_dir(), 'http'); + file_put_contents($path, $data); + + return $path; + } + + private function getUploadedFile(string $name): array + { + return [ + 'tmp_name' => $this->uploadFile($name.'_content'), + 'name' => $name.'_name', + ]; + } + + protected function expectClientToSendRequestWithFiles(HttpClientInterface $client, $fileContents) + { + $client + ->expects($this->once()) + ->method('request') + ->with('POST', 'http://example.com/', $this->callback(function ($options) use ($fileContents) { + $this->assertStringContainsString('Content-Type: multipart/form-data', implode('', $options['headers'])); + $this->assertInstanceOf('\Generator', $options['body']); + $body = implode('', iterator_to_array($options['body'], false)); + foreach ($fileContents as $content) { + $this->assertStringContainsString($content, $body); + } + + return true; + })) + ->willReturn($this->createMock(ResponseInterface::class)); + } }