PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.4
Booking for Appointments and Events Calendar – Amelia v2.4.4
2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / vendor / rmccue / requests / src / Transport / Fsockopen.php
ameliabooking / vendor / rmccue / requests / src / Transport Last commit date
Curl.php 5 months ago Fsockopen.php 5 months ago
Fsockopen.php
521 lines
1 <?php
2 /**
3 * fsockopen HTTP transport
4 *
5 * @package Requests\Transport
6 */
7
8 namespace AmeliaVendor\WpOrg\Requests\Transport;
9
10 use AmeliaVendor\WpOrg\Requests\Capability;
11 use AmeliaVendor\WpOrg\Requests\Exception;
12 use AmeliaVendor\WpOrg\Requests\Exception\InvalidArgument;
13 use AmeliaVendor\WpOrg\Requests\Port;
14 use AmeliaVendor\WpOrg\Requests\Requests;
15 use AmeliaVendor\WpOrg\Requests\Ssl;
16 use AmeliaVendor\WpOrg\Requests\Transport;
17 use AmeliaVendor\WpOrg\Requests\Utility\CaseInsensitiveDictionary;
18 use AmeliaVendor\WpOrg\Requests\Utility\InputValidator;
19
20 /**
21 * fsockopen HTTP transport
22 *
23 * @package Requests\Transport
24 */
25 final class Fsockopen implements Transport {
26 /**
27 * Second to microsecond conversion
28 *
29 * @var integer
30 */
31 const SECOND_IN_MICROSECONDS = 1000000;
32
33 /**
34 * Raw HTTP data
35 *
36 * @var string
37 */
38 public $headers = '';
39
40 /**
41 * Stream metadata
42 *
43 * @var array Associative array of properties, see {@link https://www.php.net/stream_get_meta_data}
44 */
45 public $info;
46
47 /**
48 * What's the maximum number of bytes we should keep?
49 *
50 * @var int|bool Byte count, or false if no limit.
51 */
52 private $max_bytes = false;
53
54 /**
55 * Cache for received connection errors.
56 *
57 * @var string
58 */
59 private $connect_error = '';
60
61 /**
62 * Perform a request
63 *
64 * @param string|Stringable $url URL to request
65 * @param array $headers Associative array of request headers
66 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
67 * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation
68 * @return string Raw HTTP result
69 *
70 * @throws \AmeliaVendor\WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable.
71 * @throws \AmeliaVendor\WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array.
72 * @throws \AmeliaVendor\WpOrg\Requests\Exception\InvalidArgument When the passed $data parameter is not an array or string.
73 * @throws \AmeliaVendor\WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
74 * @throws \AmeliaVendor\WpOrg\Requests\Exception On failure to connect to socket (`fsockopenerror`)
75 * @throws \AmeliaVendor\WpOrg\Requests\Exception On socket timeout (`timeout`)
76 */
77 public function request($url, $headers = [], $data = [], $options = []) {
78 if (InputValidator::is_string_or_stringable($url) === false) {
79 throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url));
80 }
81
82 if (is_array($headers) === false) {
83 throw InvalidArgument::create(2, '$headers', 'array', gettype($headers));
84 }
85
86 if (!is_array($data) && !is_string($data)) {
87 if ($data === null) {
88 $data = '';
89 } else {
90 throw InvalidArgument::create(3, '$data', 'array|string', gettype($data));
91 }
92 }
93
94 if (is_array($options) === false) {
95 throw InvalidArgument::create(4, '$options', 'array', gettype($options));
96 }
97
98 $options['hooks']->dispatch('fsockopen.before_request');
99
100 $url_parts = parse_url($url);
101 if (empty($url_parts)) {
102 throw new Exception('Invalid URL.', 'invalidurl', $url);
103 }
104
105 $host = $url_parts['host'];
106 $context = stream_context_create();
107 $verifyname = false;
108 $case_insensitive_headers = new CaseInsensitiveDictionary($headers);
109
110 // HTTPS support
111 if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') {
112 $remote_socket = 'ssl://' . $host;
113 if (!isset($url_parts['port'])) {
114 $url_parts['port'] = Port::HTTPS;
115 }
116
117 $context_options = [
118 'verify_peer' => true,
119 'capture_peer_cert' => true,
120 ];
121 $verifyname = true;
122
123 // SNI, if enabled (OpenSSL >=0.9.8j)
124 // phpcs:ignore PHPCompatibility.Constants.NewConstants.openssl_tlsext_server_nameFound
125 if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) {
126 $context_options['SNI_enabled'] = true;
127 if (isset($options['verifyname']) && $options['verifyname'] === false) {
128 $context_options['SNI_enabled'] = false;
129 }
130 }
131
132 if (isset($options['verify'])) {
133 if ($options['verify'] === false) {
134 $context_options['verify_peer'] = false;
135 $context_options['verify_peer_name'] = false;
136 $verifyname = false;
137 } elseif (is_string($options['verify'])) {
138 $context_options['cafile'] = $options['verify'];
139 }
140 }
141
142 if (isset($options['verifyname']) && $options['verifyname'] === false) {
143 $context_options['verify_peer_name'] = false;
144 $verifyname = false;
145 }
146
147 // Handle the PHP 8.4 deprecation (PHP 9.0 removal) of the function signature we use for stream_context_set_option().
148 // Ref: https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#stream_context_set_option
149 if (function_exists('stream_context_set_options')) {
150 // PHP 8.3+.
151 // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.stream_context_set_optionsFound
152 stream_context_set_options($context, ['ssl' => $context_options]);
153 } else {
154 // PHP < 8.3.
155 // phpcs:ignore PHPCompatibility.FunctionUse.OptionalToRequiredFunctionParameters
156 stream_context_set_option($context, ['ssl' => $context_options]);
157 }
158 } else {
159 $remote_socket = 'tcp://' . $host;
160 }
161
162 $this->max_bytes = $options['max_bytes'];
163
164 if (!isset($url_parts['port'])) {
165 $url_parts['port'] = Port::HTTP;
166 }
167
168 $remote_socket .= ':' . $url_parts['port'];
169
170 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
171 set_error_handler([$this, 'connect_error_handler'], E_WARNING | E_NOTICE);
172
173 $options['hooks']->dispatch('fsockopen.remote_socket', [&$remote_socket]);
174
175 $socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context);
176
177 restore_error_handler();
178
179 if ($verifyname && !$this->verify_certificate_from_context($host, $context)) {
180 throw new Exception('SSL certificate did not match the requested domain name', 'ssl.no_match');
181 }
182
183 if (!$socket) {
184 if ($errno === 0) {
185 // Connection issue
186 throw new Exception(rtrim($this->connect_error), 'fsockopen.connect_error');
187 }
188
189 throw new Exception($errstr, 'fsockopenerror', null, $errno);
190 }
191
192 $data_format = $options['data_format'];
193
194 if ($data_format === 'query') {
195 $path = self::format_get($url_parts, $data);
196 $data = '';
197 } else {
198 $path = self::format_get($url_parts, []);
199 }
200
201 $options['hooks']->dispatch('fsockopen.remote_host_path', [&$path, $url]);
202
203 $request_body = '';
204 $out = sprintf("%s %s HTTP/%.1F\r\n", $options['type'], $path, $options['protocol_version']);
205
206 if ($options['type'] !== Requests::TRACE) {
207 if (is_array($data)) {
208 $request_body = http_build_query($data, '', '&');
209 } else {
210 $request_body = $data;
211 }
212
213 // Always include Content-length on POST requests to prevent
214 // 411 errors from some servers when the body is empty.
215 if (!empty($data) || $options['type'] === Requests::POST) {
216 if (!isset($case_insensitive_headers['Content-Length'])) {
217 $headers['Content-Length'] = strlen($request_body);
218 }
219
220 if (!isset($case_insensitive_headers['Content-Type'])) {
221 $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
222 }
223 }
224 }
225
226 if (!isset($case_insensitive_headers['Host'])) {
227 $out .= sprintf('Host: %s', $url_parts['host']);
228 $scheme_lower = strtolower($url_parts['scheme']);
229
230 if (($scheme_lower === 'http' && $url_parts['port'] !== Port::HTTP) || ($scheme_lower === 'https' && $url_parts['port'] !== Port::HTTPS)) {
231 $out .= ':' . $url_parts['port'];
232 }
233
234 $out .= "\r\n";
235 }
236
237 if (!isset($case_insensitive_headers['User-Agent'])) {
238 $out .= sprintf("User-Agent: %s\r\n", $options['useragent']);
239 }
240
241 $accept_encoding = $this->accept_encoding();
242 if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) {
243 $out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding);
244 }
245
246 $headers = Requests::flatten($headers);
247
248 if (!empty($headers)) {
249 $out .= implode("\r\n", $headers) . "\r\n";
250 }
251
252 $options['hooks']->dispatch('fsockopen.after_headers', [&$out]);
253
254 if (substr($out, -2) !== "\r\n") {
255 $out .= "\r\n";
256 }
257
258 if (!isset($case_insensitive_headers['Connection'])) {
259 $out .= "Connection: Close\r\n";
260 }
261
262 $out .= "\r\n" . $request_body;
263
264 $options['hooks']->dispatch('fsockopen.before_send', [&$out]);
265
266 fwrite($socket, $out);
267 $options['hooks']->dispatch('fsockopen.after_send', [$out]);
268
269 if (!$options['blocking']) {
270 fclose($socket);
271 $fake_headers = '';
272 $options['hooks']->dispatch('fsockopen.after_request', [&$fake_headers]);
273 return '';
274 }
275
276 $timeout_sec = (int) floor($options['timeout']);
277 if ($timeout_sec === $options['timeout']) {
278 $timeout_msec = 0;
279 } else {
280 $timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS;
281 }
282
283 stream_set_timeout($socket, $timeout_sec, $timeout_msec);
284
285 $response = '';
286 $body = '';
287 $headers = '';
288 $this->info = stream_get_meta_data($socket);
289 $size = 0;
290 $doingbody = false;
291 $download = false;
292 if ($options['filename']) {
293 // phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silenced the PHP native warning in favour of throwing an exception.
294 $download = @fopen($options['filename'], 'wb');
295 if ($download === false) {
296 $error = error_get_last();
297 throw new Exception($error['message'], 'fopen');
298 }
299 }
300
301 while (!feof($socket)) {
302 $this->info = stream_get_meta_data($socket);
303 if ($this->info['timed_out']) {
304 throw new Exception('fsocket timed out', 'timeout');
305 }
306
307 $block = fread($socket, Requests::BUFFER_SIZE);
308 if (!$doingbody) {
309 $response .= $block;
310 if (strpos($response, "\r\n\r\n")) {
311 list($headers, $block) = explode("\r\n\r\n", $response, 2);
312 $doingbody = true;
313 }
314 }
315
316 // Are we in body mode now?
317 if ($doingbody) {
318 $options['hooks']->dispatch('request.progress', [$block, $size, $this->max_bytes]);
319 $data_length = strlen($block);
320 if ($this->max_bytes) {
321 // Have we already hit a limit?
322 if ($size === $this->max_bytes) {
323 continue;
324 }
325
326 if (($size + $data_length) > $this->max_bytes) {
327 // Limit the length
328 $limited_length = ($this->max_bytes - $size);
329 $block = substr($block, 0, $limited_length);
330 }
331 }
332
333 $size += strlen($block);
334 if ($download) {
335 fwrite($download, $block);
336 } else {
337 $body .= $block;
338 }
339 }
340 }
341
342 $this->headers = $headers;
343
344 if ($download) {
345 fclose($download);
346 } else {
347 $this->headers .= "\r\n\r\n" . $body;
348 }
349
350 fclose($socket);
351
352 $options['hooks']->dispatch('fsockopen.after_request', [&$this->headers, &$this->info]);
353 return $this->headers;
354 }
355
356 /**
357 * Send multiple requests simultaneously
358 *
359 * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see \WpOrg\Requests\Transport::request()}
360 * @param array $options Global options, see {@see \WpOrg\Requests\Requests::response()} for documentation
361 * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well)
362 *
363 * @throws \AmeliaVendor\WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access.
364 * @throws \AmeliaVendor\WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
365 */
366 public function request_multiple($requests, $options) {
367 // If you're not requesting, we can't get any responses ¯\_(ツ)_/¯
368 if (empty($requests)) {
369 return [];
370 }
371
372 if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) {
373 throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests));
374 }
375
376 if (is_array($options) === false) {
377 throw InvalidArgument::create(2, '$options', 'array', gettype($options));
378 }
379
380 $responses = [];
381 $class = get_class($this);
382 foreach ($requests as $id => $request) {
383 try {
384 $handler = new $class();
385 $responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']);
386
387 $request['options']['hooks']->dispatch('transport.internal.parse_response', [&$responses[$id], $request]);
388 } catch (Exception $e) {
389 $responses[$id] = $e;
390 }
391
392 if (!is_string($responses[$id])) {
393 $request['options']['hooks']->dispatch('multiple.request.complete', [&$responses[$id], $id]);
394 }
395 }
396
397 return $responses;
398 }
399
400 /**
401 * Retrieve the encodings we can accept
402 *
403 * @return string Accept-Encoding header value
404 */
405 private static function accept_encoding() {
406 $type = [];
407 if (function_exists('gzinflate')) {
408 $type[] = 'deflate;q=1.0';
409 }
410
411 if (function_exists('gzuncompress')) {
412 $type[] = 'compress;q=0.5';
413 }
414
415 $type[] = 'gzip;q=0.5';
416
417 return implode(', ', $type);
418 }
419
420 /**
421 * Format a URL given GET data
422 *
423 * @param array $url_parts Array of URL parts as received from {@link https://www.php.net/parse_url}
424 * @param array|object $data Data to build query using, see {@link https://www.php.net/http_build_query}
425 * @return string URL with data
426 */
427 private static function format_get($url_parts, $data) {
428 if (!empty($data)) {
429 if (empty($url_parts['query'])) {
430 $url_parts['query'] = '';
431 }
432
433 $url_parts['query'] .= '&' . http_build_query($data, '', '&');
434 $url_parts['query'] = trim($url_parts['query'], '&');
435 }
436
437 if (isset($url_parts['path'])) {
438 if (isset($url_parts['query'])) {
439 $get = $url_parts['path'] . '?' . $url_parts['query'];
440 } else {
441 $get = $url_parts['path'];
442 }
443 } else {
444 $get = '/';
445 }
446
447 return $get;
448 }
449
450 /**
451 * Error handler for stream_socket_client()
452 *
453 * @param int $errno Error number (e.g. E_WARNING)
454 * @param string $errstr Error message
455 */
456 public function connect_error_handler($errno, $errstr) {
457 // Double-check we can handle it
458 if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) {
459 // Return false to indicate the default error handler should engage
460 return false;
461 }
462
463 $this->connect_error .= $errstr . "\n";
464 return true;
465 }
466
467 /**
468 * Verify the certificate against common name and subject alternative names
469 *
470 * Unfortunately, PHP doesn't check the certificate against the alternative
471 * names, leading things like 'https://www.github.com/' to be invalid.
472 * Instead
473 *
474 * @link https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1
475 *
476 * @param string $host Host name to verify against
477 * @param resource $context Stream context
478 * @return bool
479 *
480 * @throws \AmeliaVendor\WpOrg\Requests\Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`)
481 * @throws \AmeliaVendor\WpOrg\Requests\Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`)
482 */
483 public function verify_certificate_from_context($host, $context) {
484 $meta = stream_context_get_options($context);
485
486 // If we don't have SSL options, then we couldn't make the connection at
487 // all
488 if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) {
489 throw new Exception(rtrim($this->connect_error), 'ssl.connect_error');
490 }
491
492 $cert = openssl_x509_parse($meta['ssl']['peer_certificate']);
493
494 return Ssl::verify_certificate($host, $cert);
495 }
496
497 /**
498 * Self-test whether the transport can be used.
499 *
500 * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}.
501 *
502 * @codeCoverageIgnore
503 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
504 * @return bool Whether the transport can be used.
505 */
506 public static function test($capabilities = []) {
507 if (!function_exists('fsockopen')) {
508 return false;
509 }
510
511 // If needed, check that streams support SSL
512 if (isset($capabilities[Capability::SSL]) && $capabilities[Capability::SSL]) {
513 if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) {
514 return false;
515 }
516 }
517
518 return true;
519 }
520 }
521