StreamHandler   F
last analyzed

Complexity

Total Complexity 89

Size/Duplication

Total Lines 512
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 257
c 0
b 0
f 0
dl 0
loc 512
rs 2
wmc 89

18 Methods

Rating   Name   Duplication   Size   Complexity  
A add_verify() 0 24 6
A getDefaultContext() 0 32 5
A add_proxy() 0 14 5
B __invoke() 0 41 9
A callArray() 0 6 2
A addNotification() 0 9 2
A invokeStats() 0 16 2
C createStream() 0 77 17
B createResponse() 0 46 6
A createSink() 0 13 4
A add_cert() 0 12 3
B resolveHost() 0 21 7
A createResource() 0 26 4
A add_timeout() 0 4 2
A drain() 0 19 3
A add_debug() 0 33 3
A add_progress() 0 7 2
B checkDecode() 0 32 7

How to fix   Complexity   

Complex Class

Complex classes like StreamHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use StreamHandler, and based on these observations, apply Extract Interface, too.

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\RejectedPromise;
8
use GuzzleHttp\Promise\PromiseInterface;
9
use GuzzleHttp\Psr7;
10
use GuzzleHttp\TransferStats;
11
use Psr\Http\Message\RequestInterface;
12
use Psr\Http\Message\ResponseInterface;
13
use Psr\Http\Message\StreamInterface;
14
15
/**
16
 * HTTP handler that uses PHP's HTTP stream wrapper.
17
 */
18
class StreamHandler
19
{
20
    private $lastHeaders = [];
21
22
    /**
23
     * Sends an HTTP request.
24
     *
25
     * @param RequestInterface $request Request to send.
26
     * @param array            $options Request transfer options.
27
     *
28
     * @return PromiseInterface
29
     */
30
    public function __invoke(RequestInterface $request, array $options)
31
    {
32
        // Sleep if there is a delay specified.
33
        if (isset($options['delay'])) {
34
            usleep($options['delay'] * 1000);
35
        }
36
37
        $startTime = isset($options['on_stats']) ? microtime(true) : null;
38
39
        try {
40
            // Does not support the expect header.
41
            $request = $request->withoutHeader('Expect');
42
43
            // Append a content-length header if body size is zero to match
44
            // cURL's behavior.
45
            if (0 === $request->getBody()->getSize()) {
46
                $request = $request->withHeader('Content-Length', 0);
47
            }
48
49
            return $this->createResponse(
50
                $request,
51
                $options,
52
                $this->createStream($request, $options),
53
                $startTime
54
            );
55
        } catch (\InvalidArgumentException $e) {
56
            throw $e;
57
        } catch (\Exception $e) {
58
            // Determine if the error was a networking error.
59
            $message = $e->getMessage();
60
            // This list can probably get more comprehensive.
61
            if (strpos($message, 'getaddrinfo') // DNS lookup failed
62
                || strpos($message, 'Connection refused')
63
                || strpos($message, "couldn't connect to host") // error on HHVM
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);
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\Promise\rejection_for() has been deprecated: rejection_for will be removed in guzzlehttp/promises:2.0. Use Create::rejectionFor instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

70
            return /** @scrutinizer ignore-deprecated */ \GuzzleHttp\Promise\rejection_for($e);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The function stream_for was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

107
        $stream = /** @scrutinizer ignore-call */ Psr7\stream_for($stream);
Loading history...
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);
0 ignored issues
show
Bug introduced by
$status of type string is incompatible with the type integer expected by parameter $status of GuzzleHttp\Psr7\Response::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

114
        $response = new Psr7\Response(/** @scrutinizer ignore-type */ $status, $headers, $sink, $ver, $reason);
Loading history...
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);
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\Promise\rejection_for() has been deprecated: rejection_for will be removed in guzzlehttp/promises:2.0. Use Create::rejectionFor instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

122
                return /** @scrutinizer ignore-deprecated */ \GuzzleHttp\Promise\rejection_for($ex);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The function stream_for was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

153
            : /** @scrutinizer ignore-call */ Psr7\stream_for($sink);
Loading history...
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)
0 ignored issues
show
Bug introduced by
The function stream_for was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

165
                        /** @scrutinizer ignore-call */ 
166
                        Psr7\stream_for($stream)
Loading history...
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
                    if (isset($normalizedKeys['content-length'])) {
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(
0 ignored issues
show
Bug introduced by
The function copy_to_stream was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

210
        /** @scrutinizer ignore-call */ 
211
        Psr7\copy_to_stream(
Loading history...
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, $options);
0 ignored issues
show
Unused Code introduced by
The call to GuzzleHttp\Handler\Strea...er::getDefaultContext() has too many arguments starting with $options. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

279
        /** @scrutinizer ignore-call */ 
280
        $context = $this->getDefaultContext($request, $options);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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
311
            throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
312
        }
313
314
        $uri = $this->resolveHost($request, $options);
315
316
        $context = $this->createResource(
317
            function () use ($context, $params) {
318
                return stream_context_create($context, $params);
319
            }
320
        );
321
322
        return $this->createResource(
323
            function () use ($uri, &$http_response_header, $context, $options) {
324
                $resource = fopen((string) $uri, 'r', null, $context);
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type boolean expected by parameter $use_include_path of fopen(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

324
                $resource = fopen((string) $uri, 'r', /** @scrutinizer ignore-type */ null, $context);
Loading history...
325
                $this->lastHeaders = $http_response_header;
326
327
                if (isset($options['read_timeout'])) {
328
                    $readTimeout = $options['read_timeout'];
329
                    $sec = (int) $readTimeout;
330
                    $usec = ($readTimeout - $sec) * 100000;
331
                    stream_set_timeout($resource, $sec, $usec);
332
                }
333
334
                return $resource;
335
            }
336
        );
337
    }
338
339
    private function resolveHost(RequestInterface $request, array $options)
340
    {
341
        $uri = $request->getUri();
342
343
        if (isset($options['force_ip_resolve']) && !filter_var($uri->getHost(), FILTER_VALIDATE_IP)) {
344
            if ('v4' === $options['force_ip_resolve']) {
345
                $records = dns_get_record($uri->getHost(), DNS_A);
346
                if (!isset($records[0]['ip'])) {
347
                    throw new ConnectException(sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request);
348
                }
349
                $uri = $uri->withHost($records[0]['ip']);
350
            } elseif ('v6' === $options['force_ip_resolve']) {
351
                $records = dns_get_record($uri->getHost(), DNS_AAAA);
352
                if (!isset($records[0]['ipv6'])) {
353
                    throw new ConnectException(sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request);
354
                }
355
                $uri = $uri->withHost('[' . $records[0]['ipv6'] . ']');
356
            }
357
        }
358
359
        return $uri;
360
    }
361
362
    private function getDefaultContext(RequestInterface $request)
363
    {
364
        $headers = '';
365
        foreach ($request->getHeaders() as $name => $value) {
366
            foreach ($value as $val) {
367
                $headers .= "$name: $val\r\n";
368
            }
369
        }
370
371
        $context = [
372
            'http' => [
373
                'method'           => $request->getMethod(),
374
                'header'           => $headers,
375
                'protocol_version' => $request->getProtocolVersion(),
376
                'ignore_errors'    => true,
377
                'follow_location'  => 0,
378
            ],
379
        ];
380
381
        $body = (string) $request->getBody();
382
383
        if (!empty($body)) {
384
            $context['http']['content'] = $body;
385
            // Prevent the HTTP handler from adding a Content-Type header.
386
            if (!$request->hasHeader('Content-Type')) {
387
                $context['http']['header'] .= "Content-Type:\r\n";
388
            }
389
        }
390
391
        $context['http']['header'] = rtrim($context['http']['header']);
392
393
        return $context;
394
    }
395
396
    private function add_proxy(RequestInterface $request, &$options, $value, &$params)
397
    {
398
        if (!is_array($value)) {
399
            $options['http']['proxy'] = $value;
400
        } else {
401
            $scheme = $request->getUri()->getScheme();
402
            if (isset($value[$scheme])) {
403
                if (!isset($value['no'])
404
                    || !\GuzzleHttp\is_host_in_noproxy(
405
                        $request->getUri()->getHost(),
406
                        $value['no']
407
                    )
408
                ) {
409
                    $options['http']['proxy'] = $value[$scheme];
410
                }
411
            }
412
        }
413
    }
414
415
    private function add_timeout(RequestInterface $request, &$options, $value, &$params)
416
    {
417
        if ($value > 0) {
418
            $options['http']['timeout'] = $value;
419
        }
420
    }
421
422
    private function add_verify(RequestInterface $request, &$options, $value, &$params)
423
    {
424
        if ($value === true) {
425
            // PHP 5.6 or greater will find the system cert by default. When
426
            // < 5.6, use the Guzzle bundled cacert.
427
            if (PHP_VERSION_ID < 50600) {
428
                $options['ssl']['cafile'] = \GuzzleHttp\default_ca_bundle();
429
            }
430
        } elseif (is_string($value)) {
431
            $options['ssl']['cafile'] = $value;
432
            if (!file_exists($value)) {
433
                throw new \RuntimeException("SSL CA bundle not found: $value");
434
            }
435
        } elseif ($value === false) {
436
            $options['ssl']['verify_peer'] = false;
437
            $options['ssl']['verify_peer_name'] = false;
438
            return;
439
        } else {
440
            throw new \InvalidArgumentException('Invalid verify request option');
441
        }
442
443
        $options['ssl']['verify_peer'] = true;
444
        $options['ssl']['verify_peer_name'] = true;
445
        $options['ssl']['allow_self_signed'] = false;
446
    }
447
448
    private function add_cert(RequestInterface $request, &$options, $value, &$params)
449
    {
450
        if (is_array($value)) {
451
            $options['ssl']['passphrase'] = $value[1];
452
            $value = $value[0];
453
        }
454
455
        if (!file_exists($value)) {
456
            throw new \RuntimeException("SSL certificate not found: {$value}");
457
        }
458
459
        $options['ssl']['local_cert'] = $value;
460
    }
461
462
    private function add_progress(RequestInterface $request, &$options, $value, &$params)
463
    {
464
        $this->addNotification(
465
            $params,
466
            function ($code, $a, $b, $c, $transferred, $total) use ($value) {
467
                if ($code == STREAM_NOTIFY_PROGRESS) {
468
                    $value($total, $transferred, null, null);
469
                }
470
            }
471
        );
472
    }
473
474
    private function add_debug(RequestInterface $request, &$options, $value, &$params)
475
    {
476
        if ($value === false) {
477
            return;
478
        }
479
480
        static $map = [
481
            STREAM_NOTIFY_CONNECT       => 'CONNECT',
482
            STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
483
            STREAM_NOTIFY_AUTH_RESULT   => 'AUTH_RESULT',
484
            STREAM_NOTIFY_MIME_TYPE_IS  => 'MIME_TYPE_IS',
485
            STREAM_NOTIFY_FILE_SIZE_IS  => 'FILE_SIZE_IS',
486
            STREAM_NOTIFY_REDIRECTED    => 'REDIRECTED',
487
            STREAM_NOTIFY_PROGRESS      => 'PROGRESS',
488
            STREAM_NOTIFY_FAILURE       => 'FAILURE',
489
            STREAM_NOTIFY_COMPLETED     => 'COMPLETED',
490
            STREAM_NOTIFY_RESOLVE       => 'RESOLVE',
491
        ];
492
        static $args = ['severity', 'message', 'message_code',
493
            'bytes_transferred', 'bytes_max'];
494
495
        $value = \GuzzleHttp\debug_resource($value);
496
        $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment('');
497
        $this->addNotification(
498
            $params,
499
            function () use ($ident, $value, $map, $args) {
500
                $passed = func_get_args();
501
                $code = array_shift($passed);
502
                fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
503
                foreach (array_filter($passed) as $i => $v) {
504
                    fwrite($value, $args[$i] . ': "' . $v . '" ');
505
                }
506
                fwrite($value, "\n");
507
            }
508
        );
509
    }
510
511
    private function addNotification(array &$params, callable $notify)
512
    {
513
        // Wrap the existing function if needed.
514
        if (!isset($params['notification'])) {
515
            $params['notification'] = $notify;
516
        } else {
517
            $params['notification'] = $this->callArray([
518
                $params['notification'],
519
                $notify
520
            ]);
521
        }
522
    }
523
524
    private function callArray(array $functions)
525
    {
526
        return function () use ($functions) {
527
            $args = func_get_args();
528
            foreach ($functions as $fn) {
529
                call_user_func_array($fn, $args);
530
            }
531
        };
532
    }
533
}
534