diff --git a/src/Symfony/Component/HttpClient/NativeHttpClient.php b/src/Symfony/Component/HttpClient/NativeHttpClient.php index 3442b50ef3..b8e39e9479 100644 --- a/src/Symfony/Component/HttpClient/NativeHttpClient.php +++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php @@ -108,6 +108,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac 'size_body' => \strlen($options['body']), 'primary_ip' => '', 'primary_port' => 'http:' === $url['scheme'] ? 80 : 443, + 'debug' => \extension_loaded('curl') ? '' : "* Enable the curl extension for better performance\n", ]; if ($onProgress = $options['on_progress']) { @@ -139,6 +140,8 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac $info['size_download'] = $dlNow; } elseif (STREAM_NOTIFY_CONNECT === $code) { $info['connect_time'] += $now - $info['fopen_time']; + $info['debug'] .= $info['request_header']; + unset($info['request_header']); } else { return; } @@ -160,13 +163,16 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac $options['request_headers'][] = 'host: '.$host.$port; } + if (!isset($options['headers']['user-agent'])) { + $options['request_headers'][] = 'user-agent: Symfony HttpClient/Native'; + } + $context = [ 'http' => [ 'protocol_version' => $options['http_version'] ?: '1.1', 'method' => $method, 'content' => $options['body'], 'ignore_errors' => true, - 'user_agent' => 'Symfony HttpClient/Native', 'curl_verify_ssl_peer' => $options['verify_peer'], 'curl_verify_ssl_host' => $options['verify_host'], 'auto_decode' => false, // Disable dechunk filter, it's incompatible with stream_select() @@ -296,6 +302,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac $host = parse_url($url['authority'], PHP_URL_HOST); if (null === $ip = $multi->dnsCache[$host] ?? null) { + $info['debug'] .= "* Hostname was NOT found in DNS cache\n"; $now = microtime(true); if (!$ip = gethostbynamel($host)) { @@ -304,6 +311,9 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac $info['namelookup_time'] += microtime(true) - $now; $multi->dnsCache[$host] = $ip = $ip[0]; + $info['debug'] .= "* Added {$host}:0:{$ip} to DNS cache\n"; + } else { + $info['debug'] .= "* Hostname was found in DNS cache\n"; } $info['primary_ip'] = $ip; diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index a54ab6dc20..6af5f5af3d 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -28,6 +28,7 @@ final class CurlResponse implements ResponseInterface private static $performing = false; private $multi; + private $debugBuffer; /** * @internal @@ -38,6 +39,9 @@ final class CurlResponse implements ResponseInterface if (\is_resource($ch)) { $this->handle = $ch; + $this->debugBuffer = fopen('php://temp', 'w+'); + curl_setopt($ch, CURLOPT_VERBOSE, true); + curl_setopt($ch, CURLOPT_STDERR, $this->debugBuffer); } else { $this->info['url'] = $ch; $ch = $this->handle; @@ -143,6 +147,13 @@ final class CurlResponse implements ResponseInterface { if (!$info = $this->finalInfo) { self::perform($this->multi); + + if ('debug' === $type) { + rewind($this->debugBuffer); + + return stream_get_contents($this->debugBuffer); + } + $info = array_merge($this->info, curl_getinfo($this->handle)); $info['url'] = $this->info['url'] ?? $info['url']; $info['redirect_url'] = $this->info['redirect_url'] ?? null; @@ -154,6 +165,10 @@ final class CurlResponse implements ResponseInterface } if (!\in_array(curl_getinfo($this->handle, CURLINFO_PRIVATE), ['headers', 'content'], true)) { + rewind($this->debugBuffer); + $info['debug'] = stream_get_contents($this->debugBuffer); + fclose($this->debugBuffer); + $this->debugBuffer = null; $this->finalInfo = $info; } } diff --git a/src/Symfony/Component/HttpClient/Response/NativeResponse.php b/src/Symfony/Component/HttpClient/Response/NativeResponse.php index 037dd5da88..8be4b04163 100644 --- a/src/Symfony/Component/HttpClient/Response/NativeResponse.php +++ b/src/Symfony/Component/HttpClient/Response/NativeResponse.php @@ -34,6 +34,7 @@ final class NativeResponse implements ResponseInterface private $buffer; private $inflate; private $multi; + private $debugBuffer; /** * @internal @@ -76,12 +77,19 @@ final class NativeResponse implements ResponseInterface { if (!$info = $this->finalInfo) { self::perform($this->multi); + + if ('debug' === $type) { + return $this->info['debug']; + } + $info = $this->info; $info['url'] = implode('', $info['url']); - unset($info['fopen_time'], $info['size_body']); + unset($info['fopen_time'], $info['size_body'], $info['request_header']); if (null === $this->buffer) { $this->finalInfo = $info; + } else { + unset($info['debug']); } } @@ -112,10 +120,23 @@ final class NativeResponse implements ResponseInterface $url = $this->url; while (true) { + $context = stream_context_get_options($this->context); + + if ($proxy = $context['http']['proxy'] ?? null) { + $this->info['debug'] .= "* Establish HTTP proxy tunnel to {$proxy}\n"; + $this->info['request_header'] = $url; + } else { + $this->info['debug'] .= "* Trying {$this->info['primary_ip']}...\n"; + $this->info['request_header'] = $this->info['url']['path'].$this->info['url']['query']; + } + + $this->info['request_header'] = sprintf("> %s %s HTTP/%s \r\n", $context['http']['method'], $this->info['request_header'], $context['http']['protocol_version']); + $this->info['request_header'] .= implode("\r\n", $context['http']['header'])."\r\n\r\n"; + // Send request and follow redirects when needed $this->info['fopen_time'] = microtime(true); $this->handle = $h = fopen($url, 'r', false, $this->context); - self::addResponseHeaders($http_response_header, $this->info, $this->headers); + self::addResponseHeaders($http_response_header, $this->info, $this->headers, $this->info['debug']); $url = ($this->resolveRedirect)($this->multi, $this->headers['location'][0] ?? null, $this->context); if (null === $url) { @@ -136,7 +157,6 @@ final class NativeResponse implements ResponseInterface } stream_set_blocking($h, false); - $context = stream_context_get_options($this->context); $this->context = $this->resolveRedirect = null; if (isset($context['ssl']['peer_certificate_chain'])) { diff --git a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php index 98e96ea0a6..79cc40d5d8 100644 --- a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php +++ b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php @@ -189,19 +189,25 @@ trait ResponseTrait */ abstract protected static function select(ClientState $multi, float $timeout): int; - private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers): void + private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers, string &$debug = ''): void { foreach ($responseHeaders as $h) { if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([12345]\d\d) .*#', $h, $m)) { - $headers = []; + if ($headers) { + $debug .= "< \r\n"; + $headers = []; + } $info['http_code'] = (int) $m[1]; } elseif (2 === \count($m = explode(':', $h, 2))) { $headers[strtolower($m[0])][] = ltrim($m[1]); } + $debug .= "< {$h}\r\n"; $info['response_headers'][] = $h; } + $debug .= "< \r\n"; + if (!$info['http_code']) { throw new TransportException('Invalid or missing HTTP status line.'); } diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 2b4fc0c02e..fffe2ab81e 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -1907,7 +1907,7 @@ class Request } } - /* + /** * Returns the prefix as encoded in the string when the string starts with * the given prefix, false otherwise. * diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php b/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php index a54f8b518b..8b70a88f63 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php @@ -29,7 +29,7 @@ interface ControllerResolverInterface * As several resolvers can exist for a single application, a resolver must * return false when it is not able to determine the controller. * - * The resolver must only throw an exception when it should be able to load + * The resolver must only throw an exception when it should be able to load a * controller but cannot because of some errors made by the developer. * * @return callable|false A PHP callable representing the Controller, diff --git a/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php b/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php index 32435ec866..fc4ba56d9b 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/FragmentListener.php @@ -24,7 +24,7 @@ use Symfony\Component\HttpKernel\UriSigner; * All URL paths starting with /_fragment are handled as * content fragments by this listener. * - * If throws an AccessDeniedHttpException exception if the request + * Throws an AccessDeniedHttpException exception if the request * is not signed or if it is not an internal sub-request. * * @author Fabien Potencier diff --git a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php index eb49b8d5fd..e543836fc9 100644 --- a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php +++ b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php @@ -419,6 +419,46 @@ class WorkflowTest extends TestCase $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents); } + public function testApplyDoesNotTriggerExtraGuardWithEventDispatcher() + { + $transitions[] = new Transition('a-b', 'a', 'b'); + $transitions[] = new Transition('a-c', 'a', 'c'); + $definition = new Definition(['a', 'b', 'c'], $transitions); + + $subject = new Subject(); + $eventDispatcher = new EventDispatcherMock(); + $workflow = new Workflow($definition, new MultipleStateMarkingStore(), $eventDispatcher, 'workflow_name'); + + $eventNameExpected = [ + 'workflow.entered', + 'workflow.workflow_name.entered', + 'workflow.guard', + 'workflow.workflow_name.guard', + 'workflow.workflow_name.guard.a-b', + 'workflow.leave', + 'workflow.workflow_name.leave', + 'workflow.workflow_name.leave.a', + 'workflow.transition', + 'workflow.workflow_name.transition', + 'workflow.workflow_name.transition.a-b', + 'workflow.enter', + 'workflow.workflow_name.enter', + 'workflow.workflow_name.enter.b', + 'workflow.entered', + 'workflow.workflow_name.entered', + 'workflow.workflow_name.entered.b', + 'workflow.completed', + 'workflow.workflow_name.completed', + 'workflow.workflow_name.completed.a-b', + 'workflow.announce', + 'workflow.workflow_name.announce', + ]; + + $marking = $workflow->apply($subject, 'a-b'); + + $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents); + } + public function testApplyWithContext() { $definition = $this->createComplexWorkflowDefinition();