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