PluginProbe
Media Cloud Sync / 1.3.11
Media Cloud Sync v1.3.11
1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 All 34 releases
media-cloud-sync / includes / sdk / s3 / GuzzleHttp / Handler / StreamHandler.php

StreamHandler.php in Media Cloud Sync 1.3.11, at includes/sdk/s3/GuzzleHttp/Handler/StreamHandler.php

465 lines 20.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Dudlewebs\WPMCS\s3\GuzzleHttp\Handler;
4
5 use Dudlewebs\WPMCS\s3\GuzzleHttp\Exception\ConnectException;
6 use Dudlewebs\WPMCS\s3\GuzzleHttp\Exception\RequestException;
7 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise as P;
8 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise\FulfilledPromise;
9 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise\PromiseInterface;
10 use Dudlewebs\WPMCS\s3\GuzzleHttp\Psr7;
11 use Dudlewebs\WPMCS\s3\GuzzleHttp\TransferStats;
12 use Dudlewebs\WPMCS\s3\GuzzleHttp\Utils;
13 use Dudlewebs\WPMCS\s3\Psr\Http\Message\RequestInterface;
14 use Dudlewebs\WPMCS\s3\Psr\Http\Message\ResponseInterface;
15 use Dudlewebs\WPMCS\s3\Psr\Http\Message\StreamInterface;
16 use Dudlewebs\WPMCS\s3\Psr\Http\Message\UriInterface;
17 /**
18 * HTTP handler that uses PHP's HTTP stream wrapper.
19 *
20 * @final
21 */
22 class StreamHandler
23 {
24 /**
25 * @var array
26 */
27 private $lastHeaders = [];
28 /**
29 * Sends an HTTP request.
30 *
31 * @param RequestInterface $request Request to send.
32 * @param array $options Request transfer options.
33 */
34 public function __invoke(RequestInterface $request, array $options) : PromiseInterface
35 {
36 // Sleep if there is a delay specified.
37 if (isset($options['delay'])) {
38 \usleep($options['delay'] * 1000);
39 }
40 $protocolVersion = $request->getProtocolVersion();
41 if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
42 throw new ConnectException(\sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request);
43 }
44 $startTime = isset($options['on_stats']) ? Utils::currentTime() : null;
45 try {
46 // Does not support the expect header.
47 $request = $request->withoutHeader('Expect');
48 // Append a content-length header if body size is zero to match
49 // the behavior of `CurlHandler`
50 if ((0 === \strcasecmp('PUT', $request->getMethod()) || 0 === \strcasecmp('POST', $request->getMethod())) && 0 === $request->getBody()->getSize()) {
51 $request = $request->withHeader('Content-Length', '0');
52 }
53 return $this->createResponse($request, $options, $this->createStream($request, $options), $startTime);
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 (\false !== \strpos($message, 'getaddrinfo') || \false !== \strpos($message, 'Connection refused') || \false !== \strpos($message, "couldn't connect to host") || \false !== \strpos($message, 'connection attempt failed')) {
61 $e = new ConnectException($e->getMessage(), $request, $e);
62 } else {
63 $e = RequestException::wrapException($request, $e);
64 }
65 $this->invokeStats($options, $request, $startTime, null, $e);
66 return P\Create::rejectionFor($e);
67 }
68 }
69 private function invokeStats(array $options, RequestInterface $request, ?float $startTime, ?ResponseInterface $response = null, ?\Throwable $error = null) : void
70 {
71 if (isset($options['on_stats'])) {
72 $stats = new TransferStats($request, $response, Utils::currentTime() - $startTime, $error, []);
73 $options['on_stats']($stats);
74 }
75 }
76 /**
77 * @param resource $stream
78 */
79 private function createResponse(RequestInterface $request, array $options, $stream, ?float $startTime) : PromiseInterface
80 {
81 $hdrs = $this->lastHeaders;
82 $this->lastHeaders = [];
83 try {
84 [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs);
85 } catch (\Exception $e) {
86 return P\Create::rejectionFor(new RequestException('An error was encountered while creating the response', $request, null, $e));
87 }
88 [$stream, $headers] = $this->checkDecode($options, $headers, $stream);
89 $stream = Psr7\Utils::streamFor($stream);
90 $sink = $stream;
91 if (\strcasecmp('HEAD', $request->getMethod())) {
92 $sink = $this->createSink($stream, $options);
93 }
94 try {
95 $response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
96 } catch (\Exception $e) {
97 return P\Create::rejectionFor(new RequestException('An error was encountered while creating the response', $request, null, $e));
98 }
99 if (isset($options['on_headers'])) {
100 try {
101 $options['on_headers']($response);
102 } catch (\Exception $e) {
103 return P\Create::rejectionFor(new RequestException('An error was encountered during the on_headers event', $request, $response, $e));
104 }
105 }
106 // Do not drain when the request is a HEAD request because they have
107 // no body.
108 if ($sink !== $stream) {
109 $this->drain($stream, $sink, $response->getHeaderLine('Content-Length'));
110 }
111 $this->invokeStats($options, $request, $startTime, $response, null);
112 return new FulfilledPromise($response);
113 }
114 private function createSink(StreamInterface $stream, array $options) : StreamInterface
115 {
116 if (!empty($options['stream'])) {
117 return $stream;
118 }
119 $sink = $options['sink'] ?? Psr7\Utils::tryFopen('php://temp', 'r+');
120 return \is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : Psr7\Utils::streamFor($sink);
121 }
122 /**
123 * @param resource $stream
124 */
125 private function checkDecode(array $options, array $headers, $stream) : array
126 {
127 // Automatically decode responses when instructed.
128 if (!empty($options['decode_content'])) {
129 $normalizedKeys = Utils::normalizeHeaderKeys($headers);
130 if (isset($normalizedKeys['content-encoding'])) {
131 $encoding = $headers[$normalizedKeys['content-encoding']];
132 if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') {
133 $stream = new Psr7\InflateStream(Psr7\Utils::streamFor($stream));
134 $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
135 // Remove content-encoding header
136 unset($headers[$normalizedKeys['content-encoding']]);
137 // Fix content-length header
138 if (isset($normalizedKeys['content-length'])) {
139 $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
140 $length = (int) $stream->getSize();
141 if ($length === 0) {
142 unset($headers[$normalizedKeys['content-length']]);
143 } else {
144 $headers[$normalizedKeys['content-length']] = [$length];
145 }
146 }
147 }
148 }
149 }
150 return [$stream, $headers];
151 }
152 /**
153 * Drains the source stream into the "sink" client option.
154 *
155 * @param string $contentLength Header specifying the amount of
156 * data to read.
157 *
158 * @throws \RuntimeException when the sink option is invalid.
159 */
160 private function drain(StreamInterface $source, StreamInterface $sink, string $contentLength) : StreamInterface
161 {
162 // If a content-length header is provided, then stop reading once
163 // that number of bytes has been read. This can prevent infinitely
164 // reading from a stream when dealing with servers that do not honor
165 // Connection: Close headers.
166 Psr7\Utils::copyToStream($source, $sink, \strlen($contentLength) > 0 && (int) $contentLength > 0 ? (int) $contentLength : -1);
167 $sink->seek(0);
168 $source->close();
169 return $sink;
170 }
171 /**
172 * Create a resource and check to ensure it was created successfully
173 *
174 * @param callable $callback Callable that returns stream resource
175 *
176 * @return resource
177 *
178 * @throws \RuntimeException on error
179 */
180 private function createResource(callable $callback)
181 {
182 $errors = [];
183 \set_error_handler(static function ($_, $msg, $file, $line) use(&$errors) : bool {
184 $errors[] = ['message' => $msg, 'file' => $file, 'line' => $line];
185 return \true;
186 });
187 try {
188 $resource = $callback();
189 } finally {
190 \restore_error_handler();
191 }
192 if (!$resource) {
193 $message = 'Error creating resource: ';
194 foreach ($errors as $err) {
195 foreach ($err as $key => $value) {
196 $message .= "[{$key}] {$value}" . \PHP_EOL;
197 }
198 }
199 throw new \RuntimeException(\trim($message));
200 }
201 return $resource;
202 }
203 /**
204 * @return resource
205 */
206 private function createStream(RequestInterface $request, array $options)
207 {
208 static $methods;
209 if (!$methods) {
210 $methods = \array_flip(\get_class_methods(__CLASS__));
211 }
212 if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) {
213 throw new RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request);
214 }
215 // HTTP/1.1 streams using the PHP stream wrapper require a
216 // Connection: close header
217 if ($request->getProtocolVersion() === '1.1' && !$request->hasHeader('Connection')) {
218 $request = $request->withHeader('Connection', 'close');
219 }
220 // Ensure SSL is verified by default
221 if (!isset($options['verify'])) {
222 $options['verify'] = \true;
223 }
224 $params = [];
225 $context = $this->getDefaultContext($request);
226 if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) {
227 throw new \InvalidArgumentException('on_headers must be callable');
228 }
229 if (!empty($options)) {
230 foreach ($options as $key => $value) {
231 $method = "add_{$key}";
232 if (isset($methods[$method])) {
233 $this->{$method}($request, $context, $value, $params);
234 }
235 }
236 }
237 if (isset($options['stream_context'])) {
238 if (!\is_array($options['stream_context'])) {
239 throw new \InvalidArgumentException('stream_context must be an array');
240 }
241 $context = \array_replace_recursive($context, $options['stream_context']);
242 }
243 // Microsoft NTLM authentication only supported with curl handler
244 if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) {
245 throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
246 }
247 $uri = $this->resolveHost($request, $options);
248 $contextResource = $this->createResource(static function () use($context, $params) {
249 return \stream_context_create($context, $params);
250 });
251 return $this->createResource(function () use($uri, $contextResource, $context, $options, $request) {
252 $resource = @\fopen((string) $uri, 'r', \false, $contextResource);
253 // See https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_http_response_header_predefined_variable
254 if (\function_exists('Dudlewebs\\WPMCS\\s3\\http_get_last_response_headers')) {
255 /** @var array|null */
256 $http_response_header = \Dudlewebs\WPMCS\s3\http_get_last_response_headers();
257 }
258 $this->lastHeaders = $http_response_header ?? [];
259 if (\false === $resource) {
260 throw new ConnectException(\sprintf('Connection refused for URI %s', $uri), $request, null, $context);
261 }
262 if (isset($options['read_timeout'])) {
263 $readTimeout = $options['read_timeout'];
264 $sec = (int) $readTimeout;
265 $usec = ($readTimeout - $sec) * 100000;
266 \stream_set_timeout($resource, $sec, $usec);
267 }
268 return $resource;
269 });
270 }
271 private function resolveHost(RequestInterface $request, array $options) : UriInterface
272 {
273 $uri = $request->getUri();
274 if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) {
275 if ('v4' === $options['force_ip_resolve']) {
276 $records = \dns_get_record($uri->getHost(), \DNS_A);
277 if (\false === $records || !isset($records[0]['ip'])) {
278 throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request);
279 }
280 return $uri->withHost($records[0]['ip']);
281 }
282 if ('v6' === $options['force_ip_resolve']) {
283 $records = \dns_get_record($uri->getHost(), \DNS_AAAA);
284 if (\false === $records || !isset($records[0]['ipv6'])) {
285 throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request);
286 }
287 return $uri->withHost('[' . $records[0]['ipv6'] . ']');
288 }
289 }
290 return $uri;
291 }
292 private function getDefaultContext(RequestInterface $request) : array
293 {
294 $headers = '';
295 foreach ($request->getHeaders() as $name => $value) {
296 foreach ($value as $val) {
297 $headers .= "{$name}: {$val}\r\n";
298 }
299 }
300 $context = ['http' => ['method' => $request->getMethod(), 'header' => $headers, 'protocol_version' => $request->getProtocolVersion(), 'ignore_errors' => \true, 'follow_location' => 0], 'ssl' => ['peer_name' => $request->getUri()->getHost()]];
301 $body = (string) $request->getBody();
302 if ('' !== $body) {
303 $context['http']['content'] = $body;
304 // Prevent the HTTP handler from adding a Content-Type header.
305 if (!$request->hasHeader('Content-Type')) {
306 $context['http']['header'] .= "Content-Type:\r\n";
307 }
308 }
309 $context['http']['header'] = \rtrim($context['http']['header']);
310 return $context;
311 }
312 /**
313 * @param mixed $value as passed via Request transfer options.
314 */
315 private function add_proxy(RequestInterface $request, array &$options, $value, array &$params) : void
316 {
317 $uri = null;
318 if (!\is_array($value)) {
319 $uri = $value;
320 } else {
321 $scheme = $request->getUri()->getScheme();
322 if (isset($value[$scheme])) {
323 if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {
324 $uri = $value[$scheme];
325 }
326 }
327 }
328 if (!$uri) {
329 return;
330 }
331 $parsed = $this->parse_proxy($uri);
332 $options['http']['proxy'] = $parsed['proxy'];
333 if ($parsed['auth']) {
334 if (!isset($options['http']['header'])) {
335 $options['http']['header'] = [];
336 }
337 $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}";
338 }
339 }
340 /**
341 * Parses the given proxy URL to make it compatible with the format PHP's stream context expects.
342 */
343 private function parse_proxy(string $url) : array
344 {
345 $parsed = \parse_url($url);
346 if ($parsed !== \false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
347 if (isset($parsed['host']) && isset($parsed['port'])) {
348 $auth = null;
349 if (isset($parsed['user']) && isset($parsed['pass'])) {
350 $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}");
351 }
352 return ['proxy' => "tcp://{$parsed['host']}:{$parsed['port']}", 'auth' => $auth ? "Basic {$auth}" : null];
353 }
354 }
355 // Return proxy as-is.
356 return ['proxy' => $url, 'auth' => null];
357 }
358 /**
359 * @param mixed $value as passed via Request transfer options.
360 */
361 private function add_timeout(RequestInterface $request, array &$options, $value, array &$params) : void
362 {
363 if ($value > 0) {
364 $options['http']['timeout'] = $value;
365 }
366 }
367 /**
368 * @param mixed $value as passed via Request transfer options.
369 */
370 private function add_crypto_method(RequestInterface $request, array &$options, $value, array &$params) : void
371 {
372 if ($value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT || $value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT || $value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT || \defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) {
373 $options['http']['crypto_method'] = $value;
374 return;
375 }
376 throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
377 }
378 /**
379 * @param mixed $value as passed via Request transfer options.
380 */
381 private function add_verify(RequestInterface $request, array &$options, $value, array &$params) : void
382 {
383 if ($value === \false) {
384 $options['ssl']['verify_peer'] = \false;
385 $options['ssl']['verify_peer_name'] = \false;
386 return;
387 }
388 if (\is_string($value)) {
389 $options['ssl']['cafile'] = $value;
390 if (!\file_exists($value)) {
391 throw new \RuntimeException("SSL CA bundle not found: {$value}");
392 }
393 } elseif ($value !== \true) {
394 throw new \InvalidArgumentException('Invalid verify request option');
395 }
396 $options['ssl']['verify_peer'] = \true;
397 $options['ssl']['verify_peer_name'] = \true;
398 $options['ssl']['allow_self_signed'] = \false;
399 }
400 /**
401 * @param mixed $value as passed via Request transfer options.
402 */
403 private function add_cert(RequestInterface $request, array &$options, $value, array &$params) : void
404 {
405 if (\is_array($value)) {
406 $options['ssl']['passphrase'] = $value[1];
407 $value = $value[0];
408 }
409 if (!\file_exists($value)) {
410 throw new \RuntimeException("SSL certificate not found: {$value}");
411 }
412 $options['ssl']['local_cert'] = $value;
413 }
414 /**
415 * @param mixed $value as passed via Request transfer options.
416 */
417 private function add_progress(RequestInterface $request, array &$options, $value, array &$params) : void
418 {
419 self::addNotification($params, static function ($code, $a, $b, $c, $transferred, $total) use($value) {
420 if ($code == \STREAM_NOTIFY_PROGRESS) {
421 // The upload progress cannot be determined. Use 0 for cURL compatibility:
422 // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html
423 $value($total, $transferred, 0, 0);
424 }
425 });
426 }
427 /**
428 * @param mixed $value as passed via Request transfer options.
429 */
430 private function add_debug(RequestInterface $request, array &$options, $value, array &$params) : void
431 {
432 if ($value === \false) {
433 return;
434 }
435 static $map = [\STREAM_NOTIFY_CONNECT => 'CONNECT', \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', \STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', \STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', \STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', \STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', \STREAM_NOTIFY_PROGRESS => 'PROGRESS', \STREAM_NOTIFY_FAILURE => 'FAILURE', \STREAM_NOTIFY_COMPLETED => 'COMPLETED', \STREAM_NOTIFY_RESOLVE => 'RESOLVE'];
436 static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max'];
437 $value = Utils::debugResource($value);
438 $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment('');
439 self::addNotification($params, static function (int $code, ...$passed) use($ident, $value, $map, $args) : void {
440 \fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
441 foreach (\array_filter($passed) as $i => $v) {
442 \fwrite($value, $args[$i] . ': "' . $v . '" ');
443 }
444 \fwrite($value, "\n");
445 });
446 }
447 private static function addNotification(array &$params, callable $notify) : void
448 {
449 // Wrap the existing function if needed.
450 if (!isset($params['notification'])) {
451 $params['notification'] = $notify;
452 } else {
453 $params['notification'] = self::callArray([$params['notification'], $notify]);
454 }
455 }
456 private static function callArray(array $functions) : callable
457 {
458 return static function (...$args) use($functions) {
459 foreach ($functions as $fn) {
460 $fn(...$args);
461 }
462 };
463 }
464 }
465