PluginProbe
Depicter — Popup & Slider Builder / trunk
Depicter — Popup & Slider Builder vtrunk
4.8.1 trunk 1.0.0 1.1.0 1.1.2 1.1.4 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.3.5 1.3.8 1.5.0 1.5.1 1.5.2 1.5.5 1.6.0 1.6.1 1.6.2 1.7.0 All 76 releases
depicter / modules / GuzzleHttp / RedirectMiddleware.php

RedirectMiddleware.php in Depicter — Popup & Slider Builder trunk, at modules/GuzzleHttp/RedirectMiddleware.php

229 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Depicter\GuzzleHttp;
4
5 use Depicter\GuzzleHttp\Exception\BadResponseException;
6 use Depicter\GuzzleHttp\Exception\TooManyRedirectsException;
7 use Depicter\GuzzleHttp\Promise\PromiseInterface;
8 use Depicter\Psr\Http\Message\RequestInterface;
9 use Depicter\Psr\Http\Message\ResponseInterface;
10 use Depicter\Psr\Http\Message\UriInterface;
11
12 /**
13 * Request redirect middleware.
14 *
15 * Apply this middleware like other middleware using
16 * {@see \Depicter\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 }
229