Completed
Push — master ( 365fe3...003757 )
by Márk
01:41
created

StreamHandler::__invoke()   B

Complexity

Conditions 10
Paths 68

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
nc 68
nop 2
dl 0
loc 44
rs 7.6666
c 0
b 0
f 0

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
namespace GuzzleHttp\Handler;
3
4
use GuzzleHttp\Exception\RequestException;
5
use GuzzleHttp\Exception\ConnectException;
6
use GuzzleHttp\Promise\FulfilledPromise;
7
use GuzzleHttp\Promise\PromiseInterface;
8
use GuzzleHttp\Psr7;
9
use GuzzleHttp\TransferStats;
10
use Psr\Http\Message\RequestInterface;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Http\Message\StreamInterface;
13
14
/**
15
 * HTTP handler that uses PHP's HTTP stream wrapper.
16
 */
17
class StreamHandler
18
{
19
    private $lastHeaders = [];
20
21
    /**
22
     * Sends an HTTP request.
23
     *
24
     * @param RequestInterface $request Request to send.
25
     * @param array            $options Request transfer options.
26
     *
27
     * @return PromiseInterface
28
     */
29
    public function __invoke(RequestInterface $request, array $options)
30
    {
31
        // Sleep if there is a delay specified.
32
        if (isset($options['delay'])) {
33
            usleep($options['delay'] * 1000);
34
        }
35
36
        $startTime = isset($options['on_stats']) ? microtime(true) : null;
37
38
        try {
39
            // Does not support the expect header.
40
            $request = $request->withoutHeader('Expect');
41
42
            // Append a content-length header if body size is zero to match
43
            // cURL's behavior.
44
            if (0 === $request->getBody()->getSize()) {
45
                $request = $request->withHeader('Content-Length', 0);
46
            }
47
48
            return $this->createResponse(
49
                $request,
50
                $options,
51
                $this->createStream($request, $options),
52
                $startTime
53
            );
54
        } catch (\InvalidArgumentException $e) {
55
            throw $e;
56
        } catch (\Exception $e) {
57
            // Determine if the error was a networking error.
58
            $message = $e->getMessage();
59
            // This list can probably get more comprehensive.
60
            if (strpos($message, 'getaddrinfo') // DNS lookup failed
61
                || strpos($message, 'Connection refused')
62
                || strpos($message, "couldn't connect to host") // error on HHVM
63
                || strpos($message, "connection attempt failed")
64
            ) {
65
                $e = new ConnectException($e->getMessage(), $request, $e);
66
            }
67
            $e = RequestException::wrapException($request, $e);
68
            $this->invokeStats($options, $request, $startTime, null, $e);
69
70
            return \GuzzleHttp\Promise\rejection_for($e);
71
        }
72
    }
73
74
    private function invokeStats(
75
        array $options,
76
        RequestInterface $request,
77
        $startTime,
78
        ResponseInterface $response = null,
79
        $error = null
80
    ) {
81
        if (isset($options['on_stats'])) {
82
            $stats = new TransferStats(
83
                $request,
84
                $response,
85
                microtime(true) - $startTime,
86
                $error,
87
                []
88
            );
89
            call_user_func($options['on_stats'], $stats);
90
        }
91
    }
92
93
    private function createResponse(
94
        RequestInterface $request,
95
        array $options,
96
        $stream,
97
        $startTime
98
    ) {
99
        $hdrs = $this->lastHeaders;
100
        $this->lastHeaders = [];
101
        $parts = explode(' ', array_shift($hdrs), 3);
102
        $ver = explode('/', $parts[0])[1];
103
        $status = $parts[1];
104
        $reason = isset($parts[2]) ? $parts[2] : null;
105
        $headers = \GuzzleHttp\headers_from_lines($hdrs);
106
        list($stream, $headers) = $this->checkDecode($options, $headers, $stream);
107
        $stream = Psr7\stream_for($stream);
108
        $sink = $stream;
109
110
        if (strcasecmp('HEAD', $request->getMethod())) {
111
            $sink = $this->createSink($stream, $options);
112
        }
113
114
        $response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
115
116
        if (isset($options['on_headers'])) {
117
            try {
118
                $options['on_headers']($response);
119
            } catch (\Exception $e) {
120
                $msg = 'An error was encountered during the on_headers event';
121
                $ex = new RequestException($msg, $request, $response, $e);
122
                return \GuzzleHttp\Promise\rejection_for($ex);
123
            }
124
        }
125
126
        // Do not drain when the request is a HEAD request because they have
127
        // no body.
128
        if ($sink !== $stream) {
129
            $this->drain(
130
                $stream,
131
                $sink,
132
                $response->getHeaderLine('Content-Length')
133
            );
134
        }
135
136
        $this->invokeStats($options, $request, $startTime, $response, null);
137
138
        return new FulfilledPromise($response);
139
    }
140
141
    private function createSink(StreamInterface $stream, array $options)
142
    {
143
        if (!empty($options['stream'])) {
144
            return $stream;
145
        }
146
147
        $sink = isset($options['sink'])
148
            ? $options['sink']
149
            : fopen('php://temp', 'r+');
150
151
        return is_string($sink)
152
            ? new Psr7\LazyOpenStream($sink, 'w+')
153
            : Psr7\stream_for($sink);
154
    }
155
156
    private function checkDecode(array $options, array $headers, $stream)
157
    {
158
        // Automatically decode responses when instructed.
159
        if (!empty($options['decode_content'])) {
160
            $normalizedKeys = \GuzzleHttp\normalize_header_keys($headers);
161
            if (isset($normalizedKeys['content-encoding'])) {
162
                $encoding = $headers[$normalizedKeys['content-encoding']];
163
                if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') {
164
                    $stream = new Psr7\InflateStream(
165
                        Psr7\stream_for($stream)
166
                    );
167
                    $headers['x-encoded-content-encoding']
168
                        = $headers[$normalizedKeys['content-encoding']];
169
                    // Remove content-encoding header
170
                    unset($headers[$normalizedKeys['content-encoding']]);
171
                    // Fix content-length header
172 View Code Duplication
                    if (isset($normalizedKeys['content-length'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
173
                        $headers['x-encoded-content-length']
174
                            = $headers[$normalizedKeys['content-length']];
175
176
                        $length = (int) $stream->getSize();
177
                        if ($length === 0) {
178
                            unset($headers[$normalizedKeys['content-length']]);
179
                        } else {
180
                            $headers[$normalizedKeys['content-length']] = [$length];
181
                        }
182
                    }
183
                }
184
            }
185
        }
186
187
        return [$stream, $headers];
188
    }
189
190
    /**
191
     * Drains the source stream into the "sink" client option.
192
     *
193
     * @param StreamInterface $source
194
     * @param StreamInterface $sink
195
     * @param string          $contentLength Header specifying the amount of
196
     *                                       data to read.
197
     *
198
     * @return StreamInterface
199
     * @throws \RuntimeException when the sink option is invalid.
200
     */
201
    private function drain(
202
        StreamInterface $source,
203
        StreamInterface $sink,
204
        $contentLength
205
    ) {
206
        // If a content-length header is provided, then stop reading once
207
        // that number of bytes has been read. This can prevent infinitely
208
        // reading from a stream when dealing with servers that do not honor
209
        // Connection: Close headers.
210
        Psr7\copy_to_stream(
211
            $source,
212
            $sink,
213
            (strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1
214
        );
215
216
        $sink->seek(0);
217
        $source->close();
218
219
        return $sink;
220
    }
221
222
    /**
223
     * Create a resource and check to ensure it was created successfully
224
     *
225
     * @param callable $callback Callable that returns stream resource
226
     *
227
     * @return resource
228
     * @throws \RuntimeException on error
229
     */
230
    private function createResource(callable $callback)
231
    {
232
        $errors = null;
233
        set_error_handler(function ($_, $msg, $file, $line) use (&$errors) {
234
            $errors[] = [
235
                'message' => $msg,
236
                'file'    => $file,
237
                'line'    => $line
238
            ];
239
            return true;
240
        });
241
242
        $resource = $callback();
243
        restore_error_handler();
244
245
        if (!$resource) {
246
            $message = 'Error creating resource: ';
247
            foreach ($errors as $err) {
248
                foreach ($err as $key => $value) {
249
                    $message .= "[$key] $value" . PHP_EOL;
250
                }
251
            }
252
            throw new \RuntimeException(trim($message));
253
        }
254
255
        return $resource;
256
    }
257
258
    private function createStream(RequestInterface $request, array $options)
259
    {
260
        static $methods;
261
        if (!$methods) {
262
            $methods = array_flip(get_class_methods(__CLASS__));
263
        }
264
265
        // HTTP/1.1 streams using the PHP stream wrapper require a
266
        // Connection: close header
267
        if ($request->getProtocolVersion() == '1.1'
268
            && !$request->hasHeader('Connection')
269
        ) {
270
            $request = $request->withHeader('Connection', 'close');
271
        }
272
273
        // Ensure SSL is verified by default
274
        if (!isset($options['verify'])) {
275
            $options['verify'] = true;
276
        }
277
278
        $params = [];
279
        $context = $this->getDefaultContext($request);
280
281
        if (isset($options['on_headers']) && !is_callable($options['on_headers'])) {
282
            throw new \InvalidArgumentException('on_headers must be callable');
283
        }
284
285
        if (!empty($options)) {
286
            foreach ($options as $key => $value) {
287
                $method = "add_{$key}";
288
                if (isset($methods[$method])) {
289
                    $this->{$method}($request, $context, $value, $params);
290
                }
291
            }
292
        }
293
294
        if (isset($options['stream_context'])) {
295
            if (!is_array($options['stream_context'])) {
296
                throw new \InvalidArgumentException('stream_context must be an array');
297
            }
298
            $context = array_replace_recursive(
299
                $context,
300
                $options['stream_context']
301
            );
302
        }
303
304
        // Microsoft NTLM authentication only supported with curl handler
305
        if (isset($options['auth'])
306
            && is_array($options['auth'])
307
            && isset($options['auth'][2])
308
            && 'ntlm' == $options['auth'][2]
309
        ) {
310
            throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
311
        }
312
313
        $uri = $this->resolveHost($request, $options);
314
315
        $context = $this->createResource(
316
            function () use ($context, $params) {
317
                return stream_context_create($context, $params);
318
            }
319
        );
320
321
        return $this->createResource(
322
            function () use ($uri, &$http_response_header, $context, $options) {
323
                $resource = fopen((string) $uri, 'r', null, $context);
324
                $this->lastHeaders = $http_response_header;
325
326
                if (isset($options['read_timeout'])) {
327
                    $readTimeout = $options['read_timeout'];
328
                    $sec = (int) $readTimeout;
329
                    $usec = ($readTimeout - $sec) * 100000;
330
                    stream_set_timeout($resource, $sec, $usec);
331
                }
332
333
                return $resource;
334
            }
335
        );
336
    }
337
338
    private function resolveHost(RequestInterface $request, array $options)
339
    {
340
        $uri = $request->getUri();
341
342
        if (isset($options['force_ip_resolve']) && !filter_var($uri->getHost(), FILTER_VALIDATE_IP)) {
343
            if ('v4' === $options['force_ip_resolve']) {
344
                $records = dns_get_record($uri->getHost(), DNS_A);
345 View Code Duplication
                if (!isset($records[0]['ip'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
346
                    throw new ConnectException(
347
                        sprintf(
348
                            "Could not resolve IPv4 address for host '%s'",
349
                            $uri->getHost()
350
                        ),
351
                        $request
352
                    );
353
                }
354
                $uri = $uri->withHost($records[0]['ip']);
355
            } elseif ('v6' === $options['force_ip_resolve']) {
356
                $records = dns_get_record($uri->getHost(), DNS_AAAA);
357 View Code Duplication
                if (!isset($records[0]['ipv6'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
358
                    throw new ConnectException(
359
                        sprintf(
360
                            "Could not resolve IPv6 address for host '%s'",
361
                            $uri->getHost()
362
                        ),
363
                        $request
364
                    );
365
                }
366
                $uri = $uri->withHost('[' . $records[0]['ipv6'] . ']');
367
            }
368
        }
369
370
        return $uri;
371
    }
372
373
    private function getDefaultContext(RequestInterface $request)
374
    {
375
        $headers = '';
376
        foreach ($request->getHeaders() as $name => $value) {
377
            foreach ($value as $val) {
378
                $headers .= "$name: $val\r\n";
379
            }
380
        }
381
382
        $context = [
383
            'http' => [
384
                'method'           => $request->getMethod(),
385
                'header'           => $headers,
386
                'protocol_version' => $request->getProtocolVersion(),
387
                'ignore_errors'    => true,
388
                'follow_location'  => 0,
389
            ],
390
        ];
391
392
        $body = (string) $request->getBody();
393
394
        if (!empty($body)) {
395
            $context['http']['content'] = $body;
396
            // Prevent the HTTP handler from adding a Content-Type header.
397
            if (!$request->hasHeader('Content-Type')) {
398
                $context['http']['header'] .= "Content-Type:\r\n";
399
            }
400
        }
401
402
        $context['http']['header'] = rtrim($context['http']['header']);
403
404
        return $context;
405
    }
406
407
    private function add_proxy(RequestInterface $request, &$options, $value, &$params)
0 ignored issues
show
Unused Code introduced by
The parameter $params is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
408
    {
409
        if (!is_array($value)) {
410
            $options['http']['proxy'] = $value;
411
        } else {
412
            $scheme = $request->getUri()->getScheme();
413
            if (isset($value[$scheme])) {
414
                if (!isset($value['no'])
415
                    || !\GuzzleHttp\is_host_in_noproxy(
416
                        $request->getUri()->getHost(),
417
                        $value['no']
418
                    )
419
                ) {
420
                    $options['http']['proxy'] = $value[$scheme];
421
                }
422
            }
423
        }
424
    }
425
426
    private function add_timeout(RequestInterface $request, &$options, $value, &$params)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $params is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
427
    {
428
        if ($value > 0) {
429
            $options['http']['timeout'] = $value;
430
        }
431
    }
432
433
    private function add_verify(RequestInterface $request, &$options, $value, &$params)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $params is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
434
    {
435
        if ($value === true) {
436
            // PHP 5.6 or greater will find the system cert by default. When
437
            // < 5.6, use the Guzzle bundled cacert.
438
            if (PHP_VERSION_ID < 50600) {
439
                $options['ssl']['cafile'] = \GuzzleHttp\default_ca_bundle();
440
            }
441
        } elseif (is_string($value)) {
442
            $options['ssl']['cafile'] = $value;
443
            if (!file_exists($value)) {
444
                throw new \RuntimeException("SSL CA bundle not found: $value");
445
            }
446
        } elseif ($value === false) {
447
            $options['ssl']['verify_peer'] = false;
448
            $options['ssl']['verify_peer_name'] = false;
449
            return;
450
        } else {
451
            throw new \InvalidArgumentException('Invalid verify request option');
452
        }
453
454
        $options['ssl']['verify_peer'] = true;
455
        $options['ssl']['verify_peer_name'] = true;
456
        $options['ssl']['allow_self_signed'] = false;
457
    }
458
459
    private function add_cert(RequestInterface $request, &$options, $value, &$params)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $params is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
460
    {
461
        if (is_array($value)) {
462
            $options['ssl']['passphrase'] = $value[1];
463
            $value = $value[0];
464
        }
465
466
        if (!file_exists($value)) {
467
            throw new \RuntimeException("SSL certificate not found: {$value}");
468
        }
469
470
        $options['ssl']['local_cert'] = $value;
471
    }
472
473
    private function add_progress(RequestInterface $request, &$options, $value, &$params)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
474
    {
475
        $this->addNotification(
476
            $params,
477
            function ($code, $a, $b, $c, $transferred, $total) use ($value) {
478
                if ($code == STREAM_NOTIFY_PROGRESS) {
479
                    $value($total, $transferred, null, null);
480
                }
481
            }
482
        );
483
    }
484
485
    private function add_debug(RequestInterface $request, &$options, $value, &$params)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
486
    {
487
        if ($value === false) {
488
            return;
489
        }
490
491
        static $map = [
492
            STREAM_NOTIFY_CONNECT       => 'CONNECT',
493
            STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
494
            STREAM_NOTIFY_AUTH_RESULT   => 'AUTH_RESULT',
495
            STREAM_NOTIFY_MIME_TYPE_IS  => 'MIME_TYPE_IS',
496
            STREAM_NOTIFY_FILE_SIZE_IS  => 'FILE_SIZE_IS',
497
            STREAM_NOTIFY_REDIRECTED    => 'REDIRECTED',
498
            STREAM_NOTIFY_PROGRESS      => 'PROGRESS',
499
            STREAM_NOTIFY_FAILURE       => 'FAILURE',
500
            STREAM_NOTIFY_COMPLETED     => 'COMPLETED',
501
            STREAM_NOTIFY_RESOLVE       => 'RESOLVE',
502
        ];
503
        static $args = ['severity', 'message', 'message_code',
504
            'bytes_transferred', 'bytes_max'];
505
506
        $value = \GuzzleHttp\debug_resource($value);
507
        $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment('');
508
        $this->addNotification(
509
            $params,
510
            function () use ($ident, $value, $map, $args) {
511
                $passed = func_get_args();
512
                $code = array_shift($passed);
513
                fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
514
                foreach (array_filter($passed) as $i => $v) {
515
                    fwrite($value, $args[$i] . ': "' . $v . '" ');
516
                }
517
                fwrite($value, "\n");
518
            }
519
        );
520
    }
521
522
    private function addNotification(array &$params, callable $notify)
523
    {
524
        // Wrap the existing function if needed.
525
        if (!isset($params['notification'])) {
526
            $params['notification'] = $notify;
527
        } else {
528
            $params['notification'] = $this->callArray([
529
                $params['notification'],
530
                $notify
531
            ]);
532
        }
533
    }
534
535
    private function callArray(array $functions)
536
    {
537
        return function () use ($functions) {
538
            $args = func_get_args();
539
            foreach ($functions as $fn) {
540
                call_user_func_array($fn, $args);
541
            }
542
        };
543
    }
544
}
545