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