PluginProbe
StockPack – Stock photos and AI images from Unsplash, Adobe Stock, Freepik and more / 2.5.0
StockPack – Stock photos and AI images from Unsplash, Adobe Stock, Freepik and more v2.5.0
3.7.2 3.7.1 3.7.0 3.6.0 3.6.1 3.5.2 trunk 2.0 2.1 2.2 2.3 2.4 2.4.1 2.4.2 2.4.3 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.6.0 2.6.1 All 73 releases
stockpack / vendor / guzzlehttp / guzzle / src / Handler / StreamHandler.php

StreamHandler.php in StockPack – Stock photos and AI images from Unsplash, Adobe Stock, Freepik and more 2.5.0, at vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php

545 lines 18.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace GuzzleHttp\Handler;
3
4 use GuzzleHttp\Exception\ConnectException;
5 use GuzzleHttp\Exception\RequestException;
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']) ? \GuzzleHttp\_current_time() : 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 \GuzzleHttp\_current_time() - $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 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(
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 if (!isset($records[0]['ip'])) {
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 if (!isset($records[0]['ipv6'])) {
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)
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)
427 {
428 if ($value > 0) {
429 $options['http']['timeout'] = $value;
430 }
431 }
432
433 private function add_verify(RequestInterface $request, &$options, $value, &$params)
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)
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)
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)
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