No Description

RedirectMiddleware.php 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. <?php
  2. namespace GuzzleHttp;
  3. use GuzzleHttp\Exception\BadResponseException;
  4. use GuzzleHttp\Exception\TooManyRedirectsException;
  5. use GuzzleHttp\Promise\PromiseInterface;
  6. use GuzzleHttp\Psr7;
  7. use Psr\Http\Message\RequestInterface;
  8. use Psr\Http\Message\ResponseInterface;
  9. use Psr\Http\Message\UriInterface;
  10. /**
  11. * Request redirect middleware.
  12. *
  13. * Apply this middleware like other middleware using
  14. * {@see GuzzleHttp\Middleware::redirect()}.
  15. */
  16. class RedirectMiddleware
  17. {
  18. const HISTORY_HEADER = 'X-Guzzle-Redirect-History';
  19. public static $defaultSettings = [
  20. 'max' => 5,
  21. 'protocols' => ['http', 'https'],
  22. 'strict' => false,
  23. 'referer' => false,
  24. 'track_redirects' => false,
  25. ];
  26. /** @var callable */
  27. private $nextHandler;
  28. /**
  29. * @param callable $nextHandler Next handler to invoke.
  30. */
  31. public function __construct(callable $nextHandler)
  32. {
  33. $this->nextHandler = $nextHandler;
  34. }
  35. /**
  36. * @param RequestInterface $request
  37. * @param array $options
  38. *
  39. * @return PromiseInterface
  40. */
  41. public function __invoke(RequestInterface $request, array $options)
  42. {
  43. $fn = $this->nextHandler;
  44. if (empty($options['allow_redirects'])) {
  45. return $fn($request, $options);
  46. }
  47. if ($options['allow_redirects'] === true) {
  48. $options['allow_redirects'] = self::$defaultSettings;
  49. } elseif (!is_array($options['allow_redirects'])) {
  50. throw new \InvalidArgumentException('allow_redirects must be true, false, or array');
  51. } else {
  52. // Merge the default settings with the provided settings
  53. $options['allow_redirects'] += self::$defaultSettings;
  54. }
  55. if (empty($options['allow_redirects']['max'])) {
  56. return $fn($request, $options);
  57. }
  58. return $fn($request, $options)
  59. ->then(function (ResponseInterface $response) use ($request, $options) {
  60. return $this->checkRedirect($request, $options, $response);
  61. });
  62. }
  63. /**
  64. * @param RequestInterface $request
  65. * @param array $options
  66. * @param ResponseInterface|PromiseInterface $response
  67. *
  68. * @return ResponseInterface|PromiseInterface
  69. */
  70. public function checkRedirect(
  71. RequestInterface $request,
  72. array $options,
  73. ResponseInterface $response
  74. ) {
  75. if (substr($response->getStatusCode(), 0, 1) != '3'
  76. || !$response->hasHeader('Location')
  77. ) {
  78. return $response;
  79. }
  80. $this->guardMax($request, $options);
  81. $nextRequest = $this->modifyRequest($request, $options, $response);
  82. if (isset($options['allow_redirects']['on_redirect'])) {
  83. call_user_func(
  84. $options['allow_redirects']['on_redirect'],
  85. $request,
  86. $response,
  87. $nextRequest->getUri()
  88. );
  89. }
  90. /** @var PromiseInterface|ResponseInterface $promise */
  91. $promise = $this($nextRequest, $options);
  92. // Add headers to be able to track history of redirects.
  93. if (!empty($options['allow_redirects']['track_redirects'])) {
  94. return $this->withTracking(
  95. $promise,
  96. (string) $nextRequest->getUri()
  97. );
  98. }
  99. return $promise;
  100. }
  101. private function withTracking(PromiseInterface $promise, $uri)
  102. {
  103. return $promise->then(
  104. function (ResponseInterface $response) use ($uri) {
  105. // Note that we are pushing to the front of the list as this
  106. // would be an earlier response than what is currently present
  107. // in the history header.
  108. $header = $response->getHeader(self::HISTORY_HEADER);
  109. array_unshift($header, $uri);
  110. return $response->withHeader(self::HISTORY_HEADER, $header);
  111. }
  112. );
  113. }
  114. private function guardMax(RequestInterface $request, array &$options)
  115. {
  116. $current = isset($options['__redirect_count'])
  117. ? $options['__redirect_count']
  118. : 0;
  119. $options['__redirect_count'] = $current + 1;
  120. $max = $options['allow_redirects']['max'];
  121. if ($options['__redirect_count'] > $max) {
  122. throw new TooManyRedirectsException(
  123. "Will not follow more than {$max} redirects",
  124. $request
  125. );
  126. }
  127. }
  128. /**
  129. * @param RequestInterface $request
  130. * @param array $options
  131. * @param ResponseInterface $response
  132. *
  133. * @return RequestInterface
  134. */
  135. public function modifyRequest(
  136. RequestInterface $request,
  137. array $options,
  138. ResponseInterface $response
  139. ) {
  140. // Request modifications to apply.
  141. $modify = [];
  142. $protocols = $options['allow_redirects']['protocols'];
  143. // Use a GET request if this is an entity enclosing request and we are
  144. // not forcing RFC compliance, but rather emulating what all browsers
  145. // would do.
  146. $statusCode = $response->getStatusCode();
  147. if ($statusCode == 303 ||
  148. ($statusCode <= 302 && $request->getBody() && !$options['allow_redirects']['strict'])
  149. ) {
  150. $modify['method'] = 'GET';
  151. $modify['body'] = '';
  152. }
  153. $modify['uri'] = $this->redirectUri($request, $response, $protocols);
  154. Psr7\rewind_body($request);
  155. // Add the Referer header if it is told to do so and only
  156. // add the header if we are not redirecting from https to http.
  157. if ($options['allow_redirects']['referer']
  158. && $modify['uri']->getScheme() === $request->getUri()->getScheme()
  159. ) {
  160. $uri = $request->getUri()->withUserInfo('', '');
  161. $modify['set_headers']['Referer'] = (string) $uri;
  162. } else {
  163. $modify['remove_headers'][] = 'Referer';
  164. }
  165. // Remove Authorization header if host is different.
  166. if ($request->getUri()->getHost() !== $modify['uri']->getHost()) {
  167. $modify['remove_headers'][] = 'Authorization';
  168. }
  169. return Psr7\modify_request($request, $modify);
  170. }
  171. /**
  172. * Set the appropriate URL on the request based on the location header
  173. *
  174. * @param RequestInterface $request
  175. * @param ResponseInterface $response
  176. * @param array $protocols
  177. *
  178. * @return UriInterface
  179. */
  180. private function redirectUri(
  181. RequestInterface $request,
  182. ResponseInterface $response,
  183. array $protocols
  184. ) {
  185. $location = Psr7\Uri::resolve(
  186. $request->getUri(),
  187. $response->getHeaderLine('Location')
  188. );
  189. // Ensure that the redirect URI is allowed based on the protocols.
  190. if (!in_array($location->getScheme(), $protocols)) {
  191. throw new BadResponseException(
  192. sprintf(
  193. 'Redirect URI, %s, does not use one of the allowed redirect protocols: %s',
  194. $location,
  195. implode(', ', $protocols)
  196. ),
  197. $request,
  198. $response
  199. );
  200. }
  201. return $location;
  202. }
  203. }