Completed
Pull Request — master (#14)
by Márk
05:00
created

RedirectPlugin::handleRequest()   B

Complexity

Conditions 6
Paths 2

Size

Total Lines 45
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 6.0018

Importance

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