RedirectMiddleware.php 8.1 KB

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