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