Completed
Pull Request — master (#14)
by Márk
06:15
created

RedirectPlugin::createUri()   C

Complexity

Conditions 12
Paths 67

Size

Total Lines 49
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 49
rs 5.1474
cc 12
eloc 27
nc 67
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    public function __construct(array $config = [])
110
    {
111
        $resolver = new OptionsResolver();
112
        $resolver->setDefaults([
113
            'preserve_header' => true,
114
            'use_default_for_multiple' => true,
115
        ]);
116
        $resolver->setAllowedTypes('preserve_header', ['bool', 'array']);
117
        $resolver->setAllowedTypes('use_default_for_multiple', 'bool');
118
        $resolver->setNormalizer('preserve_header', function (OptionsResolver $resolver, $value) {
119
            if (is_bool($value) && false === $value) {
120
                return [];
121
            }
122
123
            return $value;
124
        });
125
        $options = $resolver->resolve($config);
126
127
        $this->preserveHeader = $options['preserve_header'];
128
        $this->useDefaultForMultiple = $options['use_default_for_multiple'];
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
135
    {
136
        // Check in storage
137
        if (array_key_exists($request->getRequestTarget(), $this->redirectStorage)) {
138
            $uri = $this->redirectStorage[$request->getRequestTarget()]['uri'];
139
            $statusCode = $this->redirectStorage[$request->getRequestTarget()]['status'];
140
            $redirectRequest = $this->buildRedirectRequest($request, $uri, $statusCode);
141
142
            return $first($redirectRequest);
143
        }
144
145
        return $next($request)->then(function (ResponseInterface $response) use ($request, $first) {
146
            $statusCode = $response->getStatusCode();
147
148
            if (!array_key_exists($statusCode, $this->redirectCodes)) {
149
                return $response;
150
            }
151
152
            $uri = $this->createUri($response, $request);
153
            $redirectRequest = $this->buildRedirectRequest($request, $uri, $statusCode);
154
            $chainIdentifier = spl_object_hash((object) $first);
155
156
            if (!array_key_exists($chainIdentifier, $this->circularDetection)) {
157
                $this->circularDetection[$chainIdentifier] = [];
158
            }
159
160
            $this->circularDetection[$chainIdentifier][] = $request->getRequestTarget();
161
162
            if (in_array($redirectRequest->getRequestTarget(), $this->circularDetection[$chainIdentifier])) {
163
                throw new CircularRedirectionException('Circular redirection detected', $request, $response);
164
            }
165
166
            if ($this->redirectCodes[$statusCode]['permanent']) {
167
                $this->redirectStorage[$request->getRequestTarget()] = [
168
                    'uri' => $uri,
169
                    'status' => $statusCode,
170
                ];
171
            }
172
173
            // Call redirect request in synchrone
174
            $redirectPromise = $first($redirectRequest);
175
176
            return $redirectPromise->wait();
177
        });
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
    protected function buildRedirectRequest(RequestInterface $request, UriInterface $uri, $statusCode)
190
    {
191
        $request = $request->withUri($uri);
192
193
        if (false !== $this->redirectCodes[$statusCode]['switch'] && !in_array($request->getMethod(), $this->redirectCodes[$statusCode]['switch']['unless'])) {
194
            $request = $request->withMethod($this->redirectCodes[$statusCode]['switch']['to']);
195
        }
196
197
        if (is_array($this->preserveHeader)) {
198
            $headers = array_keys($request->getHeaders());
199
200
            foreach ($headers as $name) {
201
                if (!in_array($name, $this->preserveHeader)) {
202
                    $request = $request->withoutHeader($name);
203
                }
204
            }
205
        }
206
207
        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
    private function createUri(ResponseInterface $response, RequestInterface $request)
222
    {
223
        if ($this->redirectCodes[$response->getStatusCode()]['multiple'] && (!$this->useDefaultForMultiple || !$response->hasHeader('Location'))) {
224
            throw new MultipleRedirectionException('Cannot choose a redirection', $request, $response);
225
        }
226
227
        if (!$response->hasHeader('Location')) {
228
            throw new HttpException('Redirect status code, but no location header present in the response', $request, $response);
229
        }
230
231
        $location = $response->getHeaderLine('Location');
232
        $parsedLocation = parse_url($location);
233
234
        if (false === $parsedLocation) {
235
            throw new HttpException(sprintf('Location %s could not be parsed', $location), $request, $response);
236
        }
237
238
        $uri = $request->getUri();
239
240
        if (array_key_exists('scheme', $parsedLocation)) {
241
            $uri = $uri->withScheme($parsedLocation['scheme']);
242
        }
243
244
        if (array_key_exists('host', $parsedLocation)) {
245
            $uri = $uri->withHost($parsedLocation['host']);
246
        }
247
248
        if (array_key_exists('port', $parsedLocation)) {
249
            $uri = $uri->withPort($parsedLocation['port']);
250
        }
251
252
        if (array_key_exists('path', $parsedLocation)) {
253
            $uri = $uri->withPath($parsedLocation['path']);
254
        }
255
256
        if (array_key_exists('query', $parsedLocation)) {
257
            $uri = $uri->withQuery($parsedLocation['query']);
258
        } else {
259
            $uri = $uri->withQuery('');
260
        }
261
262
        if (array_key_exists('fragment', $parsedLocation)) {
263
            $uri = $uri->withFragment($parsedLocation['fragment']);
264
        } else {
265
            $uri = $uri->withFragment('');
266
        }
267
268
        return $uri;
269
    }
270
}
271