Completed
Pull Request — master (#14)
by Márk
02:59
created

RedirectPlugin::buildRedirectRequest()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 20
ccs 14
cts 14
cp 1
rs 8.8571
cc 6
eloc 10
nc 4
nop 3
crap 6
1
<?php
2
3
namespace Http\Client\Common\Plugin;
4
5
use Http\Client\Common\Exception\CircularRedirectionException;
6
use Http\Client\Common\Exception\MultipleRedirectionException;
7
use Http\Client\Common\Plugin;
8
use Http\Client\Exception\HttpException;
9
use Psr\Http\Message\MessageInterface;
10
use Psr\Http\Message\RequestInterface;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Http\Message\UriInterface;
13
use Symfony\Component\OptionsResolver\OptionsResolver;
14
15
/**
16
 * Follow redirections.
17
 *
18
 * @author Joel Wurtz <[email protected]>
19
 */
20
class RedirectPlugin implements Plugin
21
{
22
    /**
23
     * Rule on how to redirect, change method for the new request.
24
     *
25
     * @var array
26
     */
27
    protected $redirectCodes = [
28
        300 => [
29
            'switch' => [
30
                'unless' => ['GET', 'HEAD'],
31
                'to' => 'GET',
32
            ],
33
            'multiple' => true,
34
            'permanent' => false,
35
        ],
36
        301 => [
37
            'switch' => [
38
                'unless' => ['GET', 'HEAD'],
39
                'to' => 'GET',
40
            ],
41
            'multiple' => false,
42
            'permanent' => true,
43
        ],
44
        302 => [
45
            'switch' => [
46
                'unless' => ['GET', 'HEAD'],
47
                'to' => 'GET',
48
            ],
49
            'multiple' => false,
50
            'permanent' => false,
51
        ],
52
        303 => [
53
            'switch' => [
54
                'unless' => ['GET', 'HEAD'],
55
                'to' => 'GET',
56
            ],
57
            'multiple' => false,
58
            'permanent' => false,
59
        ],
60
        307 => [
61
            'switch' => false,
62
            'multiple' => false,
63
            'permanent' => false,
64
        ],
65
        308 => [
66
            'switch' => false,
67
            'multiple' => false,
68
            'permanent' => true,
69
        ],
70
    ];
71
72
    /**
73
     * Determine how header should be preserved from old request.
74
     *
75
     * @var bool|array
76
     *
77
     * true     will keep all previous headers (default value)
78
     * false    will ditch all previous headers
79
     * string[] will keep only headers with the specified names
80
     */
81
    protected $preserveHeader;
82
83
    /**
84
     * Store all previous redirect from 301 / 308 status code.
85
     *
86
     * @var array
87
     */
88
    protected $redirectStorage = [];
89
90
    /**
91
     * Whether the location header must be directly used for a multiple redirection status code (300).
92
     *
93
     * @var bool
94
     */
95
    protected $useDefaultForMultiple;
96
97
    /**
98
     * @var array
99
     */
100
    protected $circularDetection = [];
101
102
    /**
103
     * @param array $config {
104
     *
105
     *     @var bool|string[] $preserve_header True keeps all headers, false remove all of them, an array is interpreted as a list of header names to keep.
106
     *     @var bool $use_default_for_multiple Whether the location header must be directly used for a multiple redirection status code (300).
107
     * }
108
     */
109 12
    public function __construct(array $config = [])
110
    {
111 12
        $resolver = new OptionsResolver();
112 12
        $resolver->setDefaults([
113 12
            'preserve_header' => true,
114 12
            'use_default_for_multiple' => true,
115 12
        ]);
116 12
        $resolver->setAllowedTypes('preserve_header', ['bool', 'array']);
117 12
        $resolver->setAllowedTypes('use_default_for_multiple', 'bool');
118
        $resolver->setNormalizer('preserve_header', function (OptionsResolver $resolver, $value) {
119 12
            if (is_bool($value) && false === $value) {
120
                return [];
121
            }
122
123 12
            return $value;
124 12
        });
125 12
        $options = $resolver->resolve($config);
126
127 12
        $this->preserveHeader = $options['preserve_header'];
128 12
        $this->useDefaultForMultiple = $options['use_default_for_multiple'];
129 12
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134 11
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
135
    {
136
        // Check in storage
137 11
        if (array_key_exists($request->getRequestTarget(), $this->redirectStorage)) {
138 1
            $uri = $this->redirectStorage[$request->getRequestTarget()]['uri'];
139 1
            $statusCode = $this->redirectStorage[$request->getRequestTarget()]['status'];
140 1
            $redirectRequest = $this->buildRedirectRequest($request, $uri, $statusCode);
141
142 1
            return $first($redirectRequest);
143
        }
144
145 10
        return $next($request)->then(function (ResponseInterface $response) use ($request, $first) {
146 10
            $statusCode = $response->getStatusCode();
147
148 10
            if (!array_key_exists($statusCode, $this->redirectCodes)) {
149
                return $response;
150
            }
151
152 10
            $uri = $this->createUri($response, $request);
153 6
            $redirectRequest = $this->buildRedirectRequest($request, $uri, $statusCode);
154 6
            $chainIdentifier = spl_object_hash((object) $first);
155
156 6
            if (!array_key_exists($chainIdentifier, $this->circularDetection)) {
157 5
                $this->circularDetection[$chainIdentifier] = [];
158 5
            }
159
160 6
            $this->circularDetection[$chainIdentifier][] = $request->getRequestTarget();
161
162 6
            if (in_array($redirectRequest->getRequestTarget(), $this->circularDetection[$chainIdentifier])) {
163 1
                throw new CircularRedirectionException('Circular redirection detected', $request, $response);
164
            }
165
166 5
            if ($this->redirectCodes[$statusCode]['permanent']) {
167 1
                $this->redirectStorage[$request->getRequestTarget()] = [
168 1
                    'uri' => $uri,
169 1
                    'status' => $statusCode,
170
                ];
171 1
            }
172
173
            // Call redirect request in synchrone
174 5
            $redirectPromise = $first($redirectRequest);
175
176 5
            return $redirectPromise->wait();
177 10
        });
178
    }
179
180
    /**
181
     * Builds the redirect request.
182
     *
183
     * @param RequestInterface $request    Original request
184
     * @param UriInterface     $uri        New uri
185
     * @param int              $statusCode Status code from the redirect response
186
     *
187
     * @return MessageInterface|RequestInterface
188
     */
189 7
    protected function buildRedirectRequest(RequestInterface $request, UriInterface $uri, $statusCode)
190
    {
191 7
        $request = $request->withUri($uri);
192
193 7
        if (false !== $this->redirectCodes[$statusCode]['switch'] && !in_array($request->getMethod(), $this->redirectCodes[$statusCode]['switch']['unless'])) {
194 1
            $request = $request->withMethod($this->redirectCodes[$statusCode]['switch']['to']);
195 1
        }
196
197 7
        if (is_array($this->preserveHeader)) {
198 1
            $headers = array_keys($request->getHeaders());
199
200 1
            foreach ($headers as $name) {
201 1
                if (!in_array($name, $this->preserveHeader)) {
202 1
                    $request = $request->withoutHeader($name);
203 1
                }
204 1
            }
205 1
        }
206
207 7
        return $request;
208
    }
209
210
    /**
211
     * Creates a new Uri from the old request and the location header.
212
     *
213
     * @param ResponseInterface $response The redirect response
214
     * @param RequestInterface  $request  The original request
215
     *
216
     * @throws HttpException                If location header is not usable (missing or incorrect)
217
     * @throws MultipleRedirectionException If a 300 status code is received and default location cannot be resolved (doesn't use the location header or not present)
218
     *
219
     * @return UriInterface
220
     */
221 10
    private function createUri(ResponseInterface $response, RequestInterface $request)
222
    {
223 10
        if ($this->redirectCodes[$response->getStatusCode()]['multiple'] && (!$this->useDefaultForMultiple || !$response->hasHeader('Location'))) {
224 2
            throw new MultipleRedirectionException('Cannot choose a redirection', $request, $response);
225
        }
226
227 8
        if (!$response->hasHeader('Location')) {
228 1
            throw new HttpException('Redirect status code, but no location header present in the response', $request, $response);
229
        }
230
231 7
        $location = $response->getHeaderLine('Location');
232 7
        $parsedLocation = parse_url($location);
233
234 7
        if (false === $parsedLocation) {
235 1
            throw new HttpException(sprintf('Location %s could not be parsed', $location), $request, $response);
236
        }
237
238 6
        $uri = $request->getUri();
239
240 6
        if (array_key_exists('scheme', $parsedLocation)) {
241 1
            $uri = $uri->withScheme($parsedLocation['scheme']);
242 1
        }
243
244 6
        if (array_key_exists('host', $parsedLocation)) {
245 1
            $uri = $uri->withHost($parsedLocation['host']);
246 1
        }
247
248 6
        if (array_key_exists('port', $parsedLocation)) {
249 1
            $uri = $uri->withPort($parsedLocation['port']);
250 1
        }
251
252 6
        if (array_key_exists('path', $parsedLocation)) {
253 6
            $uri = $uri->withPath($parsedLocation['path']);
254 6
        }
255
256 6
        if (array_key_exists('query', $parsedLocation)) {
257 1
            $uri = $uri->withQuery($parsedLocation['query']);
258 1
        } else {
259 5
            $uri = $uri->withQuery('');
260
        }
261
262 6
        if (array_key_exists('fragment', $parsedLocation)) {
263 1
            $uri = $uri->withFragment($parsedLocation['fragment']);
264 1
        } else {
265 5
            $uri = $uri->withFragment('');
266
        }
267
268 6
        return $uri;
269
    }
270
}
271