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