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 $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 /** 122 * Enable tracking on promise. 123 * 124 * @return PromiseInterface 125 */ 126 private function withTracking(PromiseInterface $promise, $uri, $statusCode) 127 { 128 return $promise->then( 129 function (ResponseInterface $response) use ($uri, $statusCode) { 130 // Note that we are pushing to the front of the list as this 131 // would be an earlier response than what is currently present 132 // in the history header. 133 $historyHeader = $response->getHeader(self::HISTORY_HEADER); 134 $statusHeader = $response->getHeader(self::STATUS_HISTORY_HEADER); 135 array_unshift($historyHeader, $uri); 136 array_unshift($statusHeader, $statusCode); 137 return $response->withHeader(self::HISTORY_HEADER, $historyHeader) 138 ->withHeader(self::STATUS_HISTORY_HEADER, $statusHeader); 139 } 140 ); 141 } 142 143 /** 144 * Check for too many redirects 145 * 146 * @return void 147 * 148 * @throws TooManyRedirectsException Too many redirects. 149 */ 150 private function guardMax(RequestInterface $request, array &$options) 151 { 152 $current = isset($options['__redirect_count']) 153 ? $options['__redirect_count'] 154 : 0; 155 $options['__redirect_count'] = $current + 1; 156 $max = $options['allow_redirects']['max']; 157 158 if ($options['__redirect_count'] > $max) { 159 throw new TooManyRedirectsException( 160 "Will not follow more than {$max} redirects", 161 $request 162 ); 163 } 164 } 165 166 /** 167 * @param RequestInterface $request 168 * @param array $options 169 * @param ResponseInterface $response 170 * 171 * @return RequestInterface 172 */ 173 public function modifyRequest( 174 RequestInterface $request, 175 array $options, 176 ResponseInterface $response 177 ) { 178 // Request modifications to apply. 179 $modify = []; 180 $protocols = $options['allow_redirects']['protocols']; 181 182 // Use a GET request if this is an entity enclosing request and we are 183 // not forcing RFC compliance, but rather emulating what all browsers 184 // would do. 185 $statusCode = $response->getStatusCode(); 186 if ($statusCode == 303 || 187 ($statusCode <= 302 && !$options['allow_redirects']['strict']) 188 ) { 189 $modify['method'] = 'GET'; 190 $modify['body'] = ''; 191 } 192 193 $uri = $this->redirectUri($request, $response, $protocols); 194 if (isset($options['idn_conversion']) && ($options['idn_conversion'] !== false)) { 195 $idnOptions = ($options['idn_conversion'] === true) ? IDNA_DEFAULT : $options['idn_conversion']; 196 $uri = Utils::idnUriConvert($uri, $idnOptions); 197 } 198 199 $modify['uri'] = $uri; 200 Psr7\rewind_body($request); 201 202 // Add the Referer header if it is told to do so and only 203 // add the header if we are not redirecting from https to http. 204 if ($options['allow_redirects']['referer'] 205 && $modify['uri']->getScheme() === $request->getUri()->getScheme() 206 ) { 207 $uri = $request->getUri()->withUserInfo(''); 208 $modify['set_headers']['Referer'] = (string) $uri; 209 } else { 210 $modify['remove_headers'][] = 'Referer'; 211 } 212 213 // Remove Authorization header if host is different. 214 if ($request->getUri()->getHost() !== $modify['uri']->getHost()) { 215 $modify['remove_headers'][] = 'Authorization'; 216 } 217 218 return Psr7\modify_request($request, $modify); 219 } 220 221 /** 222 * Set the appropriate URL on the request based on the location header 223 * 224 * @param RequestInterface $request 225 * @param ResponseInterface $response 226 * @param array $protocols 227 * 228 * @return UriInterface 229 */ 230 private function redirectUri( 231 RequestInterface $request, 232 ResponseInterface $response, 233 array $protocols 234 ) { 235 $location = Psr7\UriResolver::resolve( 236 $request->getUri(), 237 new Psr7\Uri($response->getHeaderLine('Location')) 238 ); 239 240 // Ensure that the redirect URI is allowed based on the protocols. 241 if (!in_array($location->getScheme(), $protocols)) { 242 throw new BadResponseException( 243 sprintf( 244 'Redirect URI, %s, does not use one of the allowed redirect protocols: %s', 245 $location, 246 implode(', ', $protocols) 247 ), 248 $request, 249 $response 250 ); 251 } 252 253 return $location; 254 } 255} 256