PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 2.8.16
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v2.8.16
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / vendor / symfony / http-client / NativeHttpClient.php

NativeHttpClient.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 2.8.16, at vendor/symfony/http-client/NativeHttpClient.php

469 lines 19.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <[email protected]>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Symfony\Component\HttpClient;
13
14 use Psr\Log\LoggerAwareInterface;
15 use Psr\Log\LoggerAwareTrait;
16 use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
17 use Symfony\Component\HttpClient\Exception\TransportException;
18 use Symfony\Component\HttpClient\Internal\NativeClientState;
19 use Symfony\Component\HttpClient\Response\NativeResponse;
20 use Symfony\Component\HttpClient\Response\ResponseStream;
21 use Symfony\Contracts\HttpClient\HttpClientInterface;
22 use Symfony\Contracts\HttpClient\ResponseInterface;
23 use Symfony\Contracts\HttpClient\ResponseStreamInterface;
24 use Symfony\Contracts\Service\ResetInterface;
25
26 /**
27 * A portable implementation of the HttpClientInterface contracts based on PHP stream wrappers.
28 *
29 * PHP stream wrappers are able to fetch response bodies concurrently,
30 * but each request is opened synchronously.
31 *
32 * @author Nicolas Grekas <[email protected]>
33 */
34 final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
35 {
36 use HttpClientTrait;
37 use LoggerAwareTrait;
38
39 private $defaultOptions = self::OPTIONS_DEFAULTS;
40 private static $emptyDefaults = self::OPTIONS_DEFAULTS;
41
42 /** @var NativeClientState */
43 private $multi;
44
45 /**
46 * @param array $defaultOptions Default request's options
47 * @param int $maxHostConnections The maximum number of connections to open
48 *
49 * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
50 */
51 public function __construct(array $defaultOptions = [], int $maxHostConnections = 6)
52 {
53 $this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
54
55 if ($defaultOptions) {
56 [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
57 }
58
59 $this->multi = new NativeClientState();
60 $this->multi->maxHostConnections = 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX;
61 }
62
63 /**
64 * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
65 *
66 * {@inheritdoc}
67 */
68 public function request(string $method, string $url, array $options = []): ResponseInterface
69 {
70 [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
71
72 if ($options['bindto']) {
73 if (file_exists($options['bindto'])) {
74 throw new TransportException(__CLASS__.' cannot bind to local Unix sockets, use e.g. CurlHttpClient instead.');
75 }
76 if (str_starts_with($options['bindto'], 'if!')) {
77 throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.');
78 }
79 if (str_starts_with($options['bindto'], 'host!')) {
80 $options['bindto'] = substr($options['bindto'], 5);
81 }
82 }
83
84 $hasContentLength = isset($options['normalized_headers']['content-length']);
85 $hasBody = '' !== $options['body'] || 'POST' === $method || $hasContentLength;
86
87 $options['body'] = self::getBodyAsString($options['body']);
88
89 if ('chunked' === substr($options['normalized_headers']['transfer-encoding'][0] ?? '', \strlen('Transfer-Encoding: '))) {
90 unset($options['normalized_headers']['transfer-encoding']);
91 $options['headers'] = array_merge(...array_values($options['normalized_headers']));
92 $options['body'] = self::dechunk($options['body']);
93 }
94 if ('' === $options['body'] && $hasBody && !$hasContentLength) {
95 $options['headers'][] = 'Content-Length: 0';
96 }
97 if ($hasBody && !isset($options['normalized_headers']['content-type'])) {
98 $options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
99 }
100
101 if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
102 // gzip is the most widely available algo, no need to deal with deflate
103 $options['headers'][] = 'Accept-Encoding: gzip';
104 }
105
106 if ($options['peer_fingerprint']) {
107 if (isset($options['peer_fingerprint']['pin-sha256']) && 1 === \count($options['peer_fingerprint'])) {
108 throw new TransportException(__CLASS__.' cannot verify "pin-sha256" fingerprints, please provide a "sha256" one.');
109 }
110
111 unset($options['peer_fingerprint']['pin-sha256']);
112 }
113
114 $info = [
115 'response_headers' => [],
116 'url' => $url,
117 'error' => null,
118 'canceled' => false,
119 'http_method' => $method,
120 'http_code' => 0,
121 'redirect_count' => 0,
122 'start_time' => 0.0,
123 'connect_time' => 0.0,
124 'redirect_time' => 0.0,
125 'pretransfer_time' => 0.0,
126 'starttransfer_time' => 0.0,
127 'total_time' => 0.0,
128 'namelookup_time' => 0.0,
129 'size_upload' => 0,
130 'size_download' => 0,
131 'size_body' => \strlen($options['body']),
132 'primary_ip' => '',
133 'primary_port' => 'http:' === $url['scheme'] ? 80 : 443,
134 'debug' => \extension_loaded('curl') ? '' : "* Enable the curl extension for better performance\n",
135 ];
136
137 if ($onProgress = $options['on_progress']) {
138 // Memoize the last progress to ease calling the callback periodically when no network transfer happens
139 $lastProgress = [0, 0];
140 $maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : \INF;
141 $onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) {
142 if ($info['total_time'] >= $maxDuration) {
143 throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
144 }
145
146 $progressInfo = $info;
147 $progressInfo['url'] = implode('', $info['url']);
148 unset($progressInfo['size_body']);
149
150 if ($progress && -1 === $progress[0]) {
151 // Response completed
152 $lastProgress[0] = max($lastProgress);
153 } else {
154 $lastProgress = $progress ?: $lastProgress;
155 }
156
157 $onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
158 };
159 } elseif (0 < $options['max_duration']) {
160 $maxDuration = $options['max_duration'];
161 $onProgress = static function () use (&$info, $maxDuration): void {
162 if ($info['total_time'] >= $maxDuration) {
163 throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
164 }
165 };
166 }
167
168 // Always register a notification callback to compute live stats about the response
169 $notification = static function (int $code, int $severity, ?string $msg, int $msgCode, int $dlNow, int $dlSize) use ($onProgress, &$info) {
170 $info['total_time'] = microtime(true) - $info['start_time'];
171
172 if (\STREAM_NOTIFY_PROGRESS === $code) {
173 $info['starttransfer_time'] = $info['starttransfer_time'] ?: $info['total_time'];
174 $info['size_upload'] += $dlNow ? 0 : $info['size_body'];
175 $info['size_download'] = $dlNow;
176 } elseif (\STREAM_NOTIFY_CONNECT === $code) {
177 $info['connect_time'] = $info['total_time'];
178 $info['debug'] .= $info['request_header'];
179 unset($info['request_header']);
180 } else {
181 return;
182 }
183
184 if ($onProgress) {
185 $onProgress($dlNow, $dlSize);
186 }
187 };
188
189 if ($options['resolve']) {
190 $this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache;
191 }
192
193 $this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, implode('', $url)));
194
195 if (!isset($options['normalized_headers']['user-agent'])) {
196 $options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
197 }
198
199 if (0 < $options['max_duration']) {
200 $options['timeout'] = min($options['max_duration'], $options['timeout']);
201 }
202
203 $bindto = $options['bindto'];
204 if (!$bindto && (70322 === \PHP_VERSION_ID || 70410 === \PHP_VERSION_ID)) {
205 $bindto = '0:0';
206 }
207
208 $context = [
209 'http' => [
210 'protocol_version' => min($options['http_version'] ?: '1.1', '1.1'),
211 'method' => $method,
212 'content' => $options['body'],
213 'ignore_errors' => true,
214 'curl_verify_ssl_peer' => $options['verify_peer'],
215 'curl_verify_ssl_host' => $options['verify_host'],
216 'auto_decode' => false, // Disable dechunk filter, it's incompatible with stream_select()
217 'timeout' => $options['timeout'],
218 'follow_location' => false, // We follow redirects ourselves - the native logic is too limited
219 ],
220 'ssl' => array_filter([
221 'verify_peer' => $options['verify_peer'],
222 'verify_peer_name' => $options['verify_host'],
223 'cafile' => $options['cafile'],
224 'capath' => $options['capath'],
225 'local_cert' => $options['local_cert'],
226 'local_pk' => $options['local_pk'],
227 'passphrase' => $options['passphrase'],
228 'ciphers' => $options['ciphers'],
229 'peer_fingerprint' => $options['peer_fingerprint'],
230 'capture_peer_cert_chain' => $options['capture_peer_cert_chain'],
231 'allow_self_signed' => (bool) $options['peer_fingerprint'],
232 'SNI_enabled' => true,
233 'disable_compression' => true,
234 ], static function ($v) { return null !== $v; }),
235 'socket' => [
236 'bindto' => $bindto,
237 'tcp_nodelay' => true,
238 ],
239 ];
240
241 $context = stream_context_create($context, ['notification' => $notification]);
242
243 $resolver = static function ($multi) use ($context, $options, $url, &$info, $onProgress) {
244 [$host, $port] = self::parseHostPort($url, $info);
245
246 if (!isset($options['normalized_headers']['host'])) {
247 $options['headers'][] = 'Host: '.$host.$port;
248 }
249
250 $proxy = self::getProxy($options['proxy'], $url, $options['no_proxy']);
251
252 if (!self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy, 'https:' === $url['scheme'])) {
253 $ip = self::dnsResolve($host, $multi, $info, $onProgress);
254 $url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
255 }
256
257 return [self::createRedirectResolver($options, $host, $proxy, $info, $onProgress), implode('', $url)];
258 };
259
260 return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolver, $onProgress, $this->logger);
261 }
262
263 /**
264 * {@inheritdoc}
265 */
266 public function stream($responses, float $timeout = null): ResponseStreamInterface
267 {
268 if ($responses instanceof NativeResponse) {
269 $responses = [$responses];
270 } elseif (!is_iterable($responses)) {
271 throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of NativeResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
272 }
273
274 return new ResponseStream(NativeResponse::stream($responses, $timeout));
275 }
276
277 public function reset()
278 {
279 $this->multi->reset();
280 }
281
282 private static function getBodyAsString($body): string
283 {
284 if (\is_resource($body)) {
285 return stream_get_contents($body);
286 }
287
288 if (!$body instanceof \Closure) {
289 return $body;
290 }
291
292 $result = '';
293
294 while ('' !== $data = $body(self::$CHUNK_SIZE)) {
295 if (!\is_string($data)) {
296 throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
297 }
298
299 $result .= $data;
300 }
301
302 return $result;
303 }
304
305 /**
306 * Extracts the host and the port from the URL.
307 */
308 private static function parseHostPort(array $url, array &$info): array
309 {
310 if ($port = parse_url($url['authority'], \PHP_URL_PORT) ?: '') {
311 $info['primary_port'] = $port;
312 $port = ':'.$port;
313 } else {
314 $info['primary_port'] = 'http:' === $url['scheme'] ? 80 : 443;
315 }
316
317 return [parse_url($url['authority'], \PHP_URL_HOST), $port];
318 }
319
320 /**
321 * Resolves the IP of the host using the local DNS cache if possible.
322 */
323 private static function dnsResolve($host, NativeClientState $multi, array &$info, ?\Closure $onProgress): string
324 {
325 if (null === $ip = $multi->dnsCache[$host] ?? null) {
326 $info['debug'] .= "* Hostname was NOT found in DNS cache\n";
327 $now = microtime(true);
328
329 if (!$ip = gethostbynamel($host)) {
330 throw new TransportException(sprintf('Could not resolve host "%s".', $host));
331 }
332
333 $info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now);
334 $multi->dnsCache[$host] = $ip = $ip[0];
335 $info['debug'] .= "* Added {$host}:0:{$ip} to DNS cache\n";
336 } else {
337 $info['debug'] .= "* Hostname was found in DNS cache\n";
338 }
339
340 $info['primary_ip'] = $ip;
341
342 if ($onProgress) {
343 // Notify DNS resolution
344 $onProgress();
345 }
346
347 return $ip;
348 }
349
350 /**
351 * Handles redirects - the native logic is too buggy to be used.
352 */
353 private static function createRedirectResolver(array $options, string $host, ?array $proxy, array &$info, ?\Closure $onProgress): \Closure
354 {
355 $redirectHeaders = [];
356 if (0 < $maxRedirects = $options['max_redirects']) {
357 $redirectHeaders = ['host' => $host];
358 $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
359 return 0 !== stripos($h, 'Host:');
360 });
361
362 if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
363 $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) {
364 return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
365 });
366 }
367 }
368
369 return static function (NativeClientState $multi, ?string $location, $context) use (&$redirectHeaders, $proxy, &$info, $maxRedirects, $onProgress): ?string {
370 if (null === $location || $info['http_code'] < 300 || 400 <= $info['http_code']) {
371 $info['redirect_url'] = null;
372
373 return null;
374 }
375
376 try {
377 $url = self::parseUrl($location);
378 } catch (InvalidArgumentException $e) {
379 $info['redirect_url'] = null;
380
381 return null;
382 }
383
384 $url = self::resolveUrl($url, $info['url']);
385 $info['redirect_url'] = implode('', $url);
386
387 if ($info['redirect_count'] >= $maxRedirects) {
388 return null;
389 }
390
391 $info['url'] = $url;
392 ++$info['redirect_count'];
393 $info['redirect_time'] = microtime(true) - $info['start_time'];
394
395 // Do like curl and browsers: turn POST to GET on 301, 302 and 303
396 if (\in_array($info['http_code'], [301, 302, 303], true)) {
397 $options = stream_context_get_options($context)['http'];
398
399 if ('POST' === $options['method'] || 303 === $info['http_code']) {
400 $info['http_method'] = $options['method'] = 'HEAD' === $options['method'] ? 'HEAD' : 'GET';
401 $options['content'] = '';
402 $filterContentHeaders = static function ($h) {
403 return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
404 };
405 $options['header'] = array_filter($options['header'], $filterContentHeaders);
406 $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
407 $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
408
409 stream_context_set_option($context, ['http' => $options]);
410 }
411 }
412
413 [$host, $port] = self::parseHostPort($url, $info);
414
415 if (false !== (parse_url($location, \PHP_URL_HOST) ?? false)) {
416 // Authorization and Cookie headers MUST NOT follow except for the initial host name
417 $requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
418 $requestHeaders[] = 'Host: '.$host.$port;
419 $dnsResolve = !self::configureHeadersAndProxy($context, $host, $requestHeaders, $proxy, 'https:' === $url['scheme']);
420 } else {
421 $dnsResolve = isset(stream_context_get_options($context)['ssl']['peer_name']);
422 }
423
424 if ($dnsResolve) {
425 $ip = self::dnsResolve($host, $multi, $info, $onProgress);
426 $url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
427 }
428
429 return implode('', $url);
430 };
431 }
432
433 private static function configureHeadersAndProxy($context, string $host, array $requestHeaders, ?array $proxy, bool $isSsl): bool
434 {
435 if (null === $proxy) {
436 stream_context_set_option($context, 'http', 'header', $requestHeaders);
437 stream_context_set_option($context, 'ssl', 'peer_name', $host);
438
439 return false;
440 }
441
442 // Matching "no_proxy" should follow the behavior of curl
443
444 foreach ($proxy['no_proxy'] as $rule) {
445 $dotRule = '.'.ltrim($rule, '.');
446
447 if ('*' === $rule || $host === $rule || str_ends_with($host, $dotRule)) {
448 stream_context_set_option($context, 'http', 'proxy', null);
449 stream_context_set_option($context, 'http', 'request_fulluri', false);
450 stream_context_set_option($context, 'http', 'header', $requestHeaders);
451 stream_context_set_option($context, 'ssl', 'peer_name', $host);
452
453 return false;
454 }
455 }
456
457 if (null !== $proxy['auth']) {
458 $requestHeaders[] = 'Proxy-Authorization: '.$proxy['auth'];
459 }
460
461 stream_context_set_option($context, 'http', 'proxy', $proxy['url']);
462 stream_context_set_option($context, 'http', 'request_fulluri', !$isSsl);
463 stream_context_set_option($context, 'http', 'header', $requestHeaders);
464 stream_context_set_option($context, 'ssl', 'peer_name', null);
465
466 return true;
467 }
468 }
469