diff --git a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php index 68101fc2e9174..585471ed4a36f 100644 --- a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php +++ b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php @@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; +use Symfony\Component\Process\Process; use Symfony\Component\VarDumper\Caster\ImgStub; /** @@ -193,27 +194,14 @@ private function getCurlCommand(array $trace): ?string $dataArg = []; if ($json = $trace['options']['json'] ?? null) { - if (!$this->argMaxLengthIsSafe($payload = self::jsonEncode($json))) { - return null; - } - $dataArg[] = '--data '.escapeshellarg($payload); + $dataArg[] = '--data-raw '.$this->escapePayload(self::jsonEncode($json)); } elseif ($body = $trace['options']['body'] ?? null) { if (\is_string($body)) { - if (!$this->argMaxLengthIsSafe($body)) { - return null; - } - try { - $dataArg[] = '--data '.escapeshellarg($body); - } catch (\ValueError) { - return null; - } + $dataArg[] = '--data-raw '.$this->escapePayload($body); } elseif (\is_array($body)) { $body = explode('&', self::normalizeBody($body)); foreach ($body as $value) { - if (!$this->argMaxLengthIsSafe($payload = urldecode($value))) { - return null; - } - $dataArg[] = '--data '.escapeshellarg($payload); + $dataArg[] = '--data-raw '.$this->escapePayload(urldecode($value)); } } else { return null; @@ -250,13 +238,18 @@ private function getCurlCommand(array $trace): ?string return implode(" \\\n ", $command); } - /** - * Let's be defensive : we authorize only size of 8kio on Windows for escapeshellarg() argument to avoid a fatal error. - * - * @see https://github.com/php/php-src/blob/9458f5f2c8a8e3d6c65cc181747a5a75654b7c6e/ext/standard/exec.c#L397 - */ - private function argMaxLengthIsSafe(string $payload): bool + private function escapePayload(string $payload): string { - return \strlen($payload) < ('\\' === \DIRECTORY_SEPARATOR ? 8100 : 256000); + static $useProcess; + + if ($useProcess ??= class_exists(Process::class)) { + return (new Process([$payload]))->getCommandLine(); + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + return '"'.str_replace('"', '""', $payload).'"'; + } + + return "'".str_replace("'", "'\\''", $payload)."'"; } } diff --git a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php index 7a9f22cab1e9e..a168db95e6746 100644 --- a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php +++ b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php @@ -248,7 +248,7 @@ public static function provideCurlRequests(): iterable --header %1$sContent-Type: application/x-www-form-urlencoded%1$s \\ --header %1$sAccept-Encoding: gzip%1$s \\ --header %1$sUser-Agent: Symfony HttpClient (Native)%1$s \\ - --data %1$sfoobarbaz%1$s', + --data-raw %1$sfoobarbaz%1$s', ]; yield 'POST with array body' => [ [ @@ -286,7 +286,7 @@ public function __toString(): string --header %1$sContent-Length: 211%1$s \\ --header %1$sAccept-Encoding: gzip%1$s \\ --header %1$sUser-Agent: Symfony HttpClient (Native)%1$s \\ - --data %1$sfoo=fooval%1$s --data %1$sbar=barval%1$s --data %1$sbaz=bazval%1$s --data %1$sfoobar[baz]=bazval%1$s --data %1$sfoobar[qux]=quxval%1$s --data %1$sbazqux[0]=bazquxval1%1$s --data %1$sbazqux[1]=bazquxval2%1$s --data %1$sobject[fooprop]=foopropval%1$s --data %1$sobject[barprop]=barpropval%1$s --data %1$stostring=tostringval%1$s', + --data-raw %1$sfoo=fooval%1$s --data-raw %1$sbar=barval%1$s --data-raw %1$sbaz=bazval%1$s --data-raw %1$sfoobar[baz]=bazval%1$s --data-raw %1$sfoobar[qux]=quxval%1$s --data-raw %1$sbazqux[0]=bazquxval1%1$s --data-raw %1$sbazqux[1]=bazquxval2%1$s --data-raw %1$sobject[fooprop]=foopropval%1$s --data-raw %1$sobject[barprop]=barpropval%1$s --data-raw %1$stostring=tostringval%1$s', ]; // escapeshellarg on Windows replaces double quotes & percent signs with spaces @@ -337,7 +337,7 @@ public function __toString(): string --header %1$sContent-Length: 120%1$s \\ --header %1$sAccept-Encoding: gzip%1$s \\ --header %1$sUser-Agent: Symfony HttpClient (Native)%1$s \\ - --data %1$s{"foo":{"bar":"baz","qux":[1.1,1.0],"fred":["\u003Cfoo\u003E","\u0027bar\u0027","\u0022baz\u0022","\u0026blong\u0026"]}}%1$s', + --data-raw %1$s{"foo":{"bar":"baz","qux":[1.1,1.0],"fred":["\u003Cfoo\u003E","\u0027bar\u0027","\u0022baz\u0022","\u0026blong\u0026"]}}%1$s', ]; } } @@ -397,29 +397,7 @@ public function testItDoesNotGeneratesCurlCommandsForUnsupportedBodyType() /** * @requires extension openssl */ - public function testItDoesNotGeneratesCurlCommandsForNotEncodableBody() - { - $sut = new HttpClientDataCollector(); - $sut->registerClient('http_client', $this->httpClientThatHasTracedRequests([ - [ - 'method' => 'POST', - 'url' => 'http://localhost:8057/json', - 'options' => [ - 'body' => "\0", - ], - ], - ])); - $sut->lateCollect(); - $collectedData = $sut->getClients(); - self::assertCount(1, $collectedData['http_client']['traces']); - $curlCommand = $collectedData['http_client']['traces'][0]['curlCommand']; - self::assertNull($curlCommand); - } - - /** - * @requires extension openssl - */ - public function testItDoesNotGeneratesCurlCommandsForTooBigData() + public function testItDoesGenerateCurlCommandsForBigData() { $sut = new HttpClientDataCollector(); $sut->registerClient('http_client', $this->httpClientThatHasTracedRequests([ @@ -435,7 +413,7 @@ public function testItDoesNotGeneratesCurlCommandsForTooBigData() $collectedData = $sut->getClients(); self::assertCount(1, $collectedData['http_client']['traces']); $curlCommand = $collectedData['http_client']['traces'][0]['curlCommand']; - self::assertNull($curlCommand); + self::assertNotNull($curlCommand); } private function httpClientThatHasTracedRequests($tracedRequests): TraceableHttpClient