HttpBrowser.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\BrowserKit;
  11. use Symfony\Component\HttpClient\HttpClient;
  12. use Symfony\Component\Mime\Part\AbstractPart;
  13. use Symfony\Component\Mime\Part\DataPart;
  14. use Symfony\Component\Mime\Part\Multipart\FormDataPart;
  15. use Symfony\Component\Mime\Part\TextPart;
  16. use Symfony\Contracts\HttpClient\HttpClientInterface;
  17. /**
  18. * An implementation of a browser using the HttpClient component
  19. * to make real HTTP requests.
  20. *
  21. * @author Fabien Potencier <fabien@symfony.com>
  22. */
  23. class HttpBrowser extends AbstractBrowser
  24. {
  25. private $client;
  26. public function __construct(HttpClientInterface $client = null, History $history = null, CookieJar $cookieJar = null)
  27. {
  28. if (!$client && !class_exists(HttpClient::class)) {
  29. throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
  30. }
  31. $this->client = $client ?? HttpClient::create();
  32. parent::__construct([], $history, $cookieJar);
  33. }
  34. protected function doRequest($request): Response
  35. {
  36. $headers = $this->getHeaders($request);
  37. [$body, $extraHeaders] = $this->getBodyAndExtraHeaders($request);
  38. $response = $this->client->request($request->getMethod(), $request->getUri(), [
  39. 'headers' => array_merge($headers, $extraHeaders),
  40. 'body' => $body,
  41. 'max_redirects' => 0,
  42. ]);
  43. return new Response($response->getContent(false), $response->getStatusCode(), $response->getHeaders(false));
  44. }
  45. /**
  46. * @return array [$body, $headers]
  47. */
  48. private function getBodyAndExtraHeaders(Request $request): array
  49. {
  50. if (\in_array($request->getMethod(), ['GET', 'HEAD'])) {
  51. return ['', []];
  52. }
  53. if (!class_exists(AbstractPart::class)) {
  54. throw new \LogicException('You cannot pass non-empty bodies as the Mime component is not installed. Try running "composer require symfony/mime".');
  55. }
  56. if (null !== $content = $request->getContent()) {
  57. $part = new TextPart($content, 'utf-8', 'plain', '8bit');
  58. return [$part->bodyToString(), $part->getPreparedHeaders()->toArray()];
  59. }
  60. $fields = $request->getParameters();
  61. if ($uploadedFiles = $this->getUploadedFiles($request->getFiles())) {
  62. $part = new FormDataPart(array_merge($fields, $uploadedFiles));
  63. return [$part->bodyToIterable(), $part->getPreparedHeaders()->toArray()];
  64. }
  65. if (empty($fields)) {
  66. return ['', []];
  67. }
  68. return [http_build_query($fields, '', '&', PHP_QUERY_RFC1738), ['Content-Type' => 'application/x-www-form-urlencoded']];
  69. }
  70. private function getHeaders(Request $request): array
  71. {
  72. $headers = [];
  73. foreach ($request->getServer() as $key => $value) {
  74. $key = strtolower(str_replace('_', '-', $key));
  75. $contentHeaders = ['content-length' => true, 'content-md5' => true, 'content-type' => true];
  76. if (0 === strpos($key, 'http-')) {
  77. $headers[substr($key, 5)] = $value;
  78. } elseif (isset($contentHeaders[$key])) {
  79. // CONTENT_* are not prefixed with HTTP_
  80. $headers[$key] = $value;
  81. }
  82. }
  83. $cookies = [];
  84. foreach ($this->getCookieJar()->allRawValues($request->getUri()) as $name => $value) {
  85. $cookies[] = $name.'='.$value;
  86. }
  87. if ($cookies) {
  88. $headers['cookie'] = implode('; ', $cookies);
  89. }
  90. return $headers;
  91. }
  92. /**
  93. * Recursively go through the list. If the file has a tmp_name, convert it to a DataPart.
  94. * Keep the original hierarchy.
  95. */
  96. private function getUploadedFiles(array $files): array
  97. {
  98. $uploadedFiles = [];
  99. foreach ($files as $name => $file) {
  100. if (!\is_array($file)) {
  101. return $uploadedFiles;
  102. }
  103. if (!isset($file['tmp_name'])) {
  104. $uploadedFiles[$name] = $this->getUploadedFiles($file);
  105. }
  106. if (isset($file['tmp_name'])) {
  107. $uploadedFiles[$name] = DataPart::fromPath($file['tmp_name'], $file['name']);
  108. }
  109. }
  110. return $uploadedFiles;
  111. }
  112. }