[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * HTTP API: WP_Http_Streams class 4 * 5 * @package WordPress 6 * @subpackage HTTP 7 * @since 4.4.0 8 */ 9 10 /** 11 * Core class used to integrate PHP Streams as an HTTP transport. 12 * 13 * @since 2.7.0 14 * @since 3.7.0 Combined with the fsockopen transport and switched to `stream_socket_client()`. 15 * @deprecated 6.4.0 Use WP_Http 16 * @see WP_Http 17 */ 18 #[AllowDynamicProperties] 19 class WP_Http_Streams { 20 /** 21 * Send a HTTP request to a URI using PHP Streams. 22 * 23 * @see WP_Http::request() For default options descriptions. 24 * 25 * @since 2.7.0 26 * @since 3.7.0 Combined with the fsockopen transport and switched to stream_socket_client(). 27 * 28 * @param string $url The request URL. 29 * @param string|array $args Optional. Override the defaults. 30 * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error 31 */ 32 public function request( $url, $args = array() ) { 33 $defaults = array( 34 'method' => 'GET', 35 'timeout' => 5, 36 'redirection' => 5, 37 'httpversion' => '1.0', 38 'blocking' => true, 39 'headers' => array(), 40 'body' => null, 41 'cookies' => array(), 42 'decompress' => false, 43 'stream' => false, 44 'filename' => null, 45 ); 46 47 $parsed_args = wp_parse_args( $args, $defaults ); 48 49 if ( isset( $parsed_args['headers']['User-Agent'] ) ) { 50 $parsed_args['user-agent'] = $parsed_args['headers']['User-Agent']; 51 unset( $parsed_args['headers']['User-Agent'] ); 52 } elseif ( isset( $parsed_args['headers']['user-agent'] ) ) { 53 $parsed_args['user-agent'] = $parsed_args['headers']['user-agent']; 54 unset( $parsed_args['headers']['user-agent'] ); 55 } 56 57 // Construct Cookie: header if any cookies are set. 58 WP_Http::buildCookieHeader( $parsed_args ); 59 60 $parsed_url = parse_url( $url ); 61 62 $connect_host = $parsed_url['host']; 63 64 $secure_transport = ( 'ssl' === $parsed_url['scheme'] || 'https' === $parsed_url['scheme'] ); 65 if ( ! isset( $parsed_url['port'] ) ) { 66 if ( 'ssl' === $parsed_url['scheme'] || 'https' === $parsed_url['scheme'] ) { 67 $parsed_url['port'] = 443; 68 $secure_transport = true; 69 } else { 70 $parsed_url['port'] = 80; 71 } 72 } 73 74 // Always pass a path, defaulting to the root in cases such as http://example.com. 75 if ( ! isset( $parsed_url['path'] ) ) { 76 $parsed_url['path'] = '/'; 77 } 78 79 if ( isset( $parsed_args['headers']['Host'] ) || isset( $parsed_args['headers']['host'] ) ) { 80 if ( isset( $parsed_args['headers']['Host'] ) ) { 81 $parsed_url['host'] = $parsed_args['headers']['Host']; 82 } else { 83 $parsed_url['host'] = $parsed_args['headers']['host']; 84 } 85 unset( $parsed_args['headers']['Host'], $parsed_args['headers']['host'] ); 86 } 87 88 /* 89 * Certain versions of PHP have issues with 'localhost' and IPv6, It attempts to connect 90 * to ::1, which fails when the server is not set up for it. For compatibility, always 91 * connect to the IPv4 address. 92 */ 93 if ( 'localhost' === strtolower( $connect_host ) ) { 94 $connect_host = '127.0.0.1'; 95 } 96 97 $connect_host = $secure_transport ? 'ssl://' . $connect_host : 'tcp://' . $connect_host; 98 99 $is_local = isset( $parsed_args['local'] ) && $parsed_args['local']; 100 $ssl_verify = isset( $parsed_args['sslverify'] ) && $parsed_args['sslverify']; 101 102 if ( $is_local ) { 103 /** 104 * Filters whether SSL should be verified for local HTTP API requests. 105 * 106 * @since 2.8.0 107 * @since 5.1.0 The `$url` parameter was added. 108 * 109 * @param bool|string $ssl_verify Boolean to control whether to verify the SSL connection 110 * or path to an SSL certificate. 111 * @param string $url The request URL. 112 */ 113 $ssl_verify = apply_filters( 'https_local_ssl_verify', $ssl_verify, $url ); 114 } elseif ( ! $is_local ) { 115 /** This filter is documented in wp-includes/class-wp-http.php */ 116 $ssl_verify = apply_filters( 'https_ssl_verify', $ssl_verify, $url ); 117 } 118 119 $proxy = new WP_HTTP_Proxy(); 120 121 $context = stream_context_create( 122 array( 123 'ssl' => array( 124 'verify_peer' => $ssl_verify, 125 // 'CN_match' => $parsed_url['host'], // This is handled by self::verify_ssl_certificate(). 126 'capture_peer_cert' => $ssl_verify, 127 'SNI_enabled' => true, 128 'cafile' => $parsed_args['sslcertificates'], 129 'allow_self_signed' => ! $ssl_verify, 130 ), 131 ) 132 ); 133 134 $timeout = (int) floor( $parsed_args['timeout'] ); 135 $utimeout = 0; 136 137 if ( $timeout !== (int) $parsed_args['timeout'] ) { 138 $utimeout = 1000000 * $parsed_args['timeout'] % 1000000; 139 } 140 141 $connect_timeout = max( $timeout, 1 ); 142 143 // Store error number. 144 $connection_error = null; 145 146 // Store error string. 147 $connection_error_str = null; 148 149 if ( ! WP_DEBUG ) { 150 // In the event that the SSL connection fails, silence the many PHP warnings. 151 if ( $secure_transport ) { 152 $error_reporting = error_reporting( 0 ); 153 } 154 155 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) { 156 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 157 $handle = @stream_socket_client( 158 'tcp://' . $proxy->host() . ':' . $proxy->port(), 159 $connection_error, 160 $connection_error_str, 161 $connect_timeout, 162 STREAM_CLIENT_CONNECT, 163 $context 164 ); 165 } else { 166 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 167 $handle = @stream_socket_client( 168 $connect_host . ':' . $parsed_url['port'], 169 $connection_error, 170 $connection_error_str, 171 $connect_timeout, 172 STREAM_CLIENT_CONNECT, 173 $context 174 ); 175 } 176 177 if ( $secure_transport ) { 178 error_reporting( $error_reporting ); 179 } 180 } else { 181 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) { 182 $handle = stream_socket_client( 183 'tcp://' . $proxy->host() . ':' . $proxy->port(), 184 $connection_error, 185 $connection_error_str, 186 $connect_timeout, 187 STREAM_CLIENT_CONNECT, 188 $context 189 ); 190 } else { 191 $handle = stream_socket_client( 192 $connect_host . ':' . $parsed_url['port'], 193 $connection_error, 194 $connection_error_str, 195 $connect_timeout, 196 STREAM_CLIENT_CONNECT, 197 $context 198 ); 199 } 200 } 201 202 if ( false === $handle ) { 203 // SSL connection failed due to expired/invalid cert, or, OpenSSL configuration is broken. 204 if ( $secure_transport && 0 === $connection_error && '' === $connection_error_str ) { 205 return new WP_Error( 'http_request_failed', __( 'The SSL certificate for the host could not be verified.' ) ); 206 } 207 208 return new WP_Error( 'http_request_failed', $connection_error . ': ' . $connection_error_str ); 209 } 210 211 // Verify that the SSL certificate is valid for this request. 212 if ( $secure_transport && $ssl_verify && ! $proxy->is_enabled() ) { 213 if ( ! self::verify_ssl_certificate( $handle, $parsed_url['host'] ) ) { 214 return new WP_Error( 'http_request_failed', __( 'The SSL certificate for the host could not be verified.' ) ); 215 } 216 } 217 218 stream_set_timeout( $handle, $timeout, $utimeout ); 219 220 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) { // Some proxies require full URL in this field. 221 $request_path = $url; 222 } else { 223 $request_path = $parsed_url['path'] . ( isset( $parsed_url['query'] ) ? '?' . $parsed_url['query'] : '' ); 224 } 225 226 $headers = strtoupper( $parsed_args['method'] ) . ' ' . $request_path . ' HTTP/' . $parsed_args['httpversion'] . "\r\n"; 227 228 $include_port_in_host_header = ( 229 ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) 230 || ( 'http' === $parsed_url['scheme'] && 80 !== $parsed_url['port'] ) 231 || ( 'https' === $parsed_url['scheme'] && 443 !== $parsed_url['port'] ) 232 ); 233 234 if ( $include_port_in_host_header ) { 235 $headers .= 'Host: ' . $parsed_url['host'] . ':' . $parsed_url['port'] . "\r\n"; 236 } else { 237 $headers .= 'Host: ' . $parsed_url['host'] . "\r\n"; 238 } 239 240 if ( isset( $parsed_args['user-agent'] ) ) { 241 $headers .= 'User-agent: ' . $parsed_args['user-agent'] . "\r\n"; 242 } 243 244 if ( is_array( $parsed_args['headers'] ) ) { 245 foreach ( (array) $parsed_args['headers'] as $header => $header_value ) { 246 $headers .= $header . ': ' . $header_value . "\r\n"; 247 } 248 } else { 249 $headers .= $parsed_args['headers']; 250 } 251 252 if ( $proxy->use_authentication() ) { 253 $headers .= $proxy->authentication_header() . "\r\n"; 254 } 255 256 $headers .= "\r\n"; 257 258 if ( ! is_null( $parsed_args['body'] ) ) { 259 $headers .= $parsed_args['body']; 260 } 261 262 fwrite( $handle, $headers ); 263 264 if ( ! $parsed_args['blocking'] ) { 265 stream_set_blocking( $handle, 0 ); 266 fclose( $handle ); 267 return array( 268 'headers' => array(), 269 'body' => '', 270 'response' => array( 271 'code' => false, 272 'message' => false, 273 ), 274 'cookies' => array(), 275 ); 276 } 277 278 $response = ''; 279 $body_started = false; 280 $keep_reading = true; 281 $block_size = 4096; 282 283 if ( isset( $parsed_args['limit_response_size'] ) ) { 284 $block_size = min( $block_size, $parsed_args['limit_response_size'] ); 285 } 286 287 // If streaming to a file setup the file handle. 288 if ( $parsed_args['stream'] ) { 289 if ( ! WP_DEBUG ) { 290 $stream_handle = @fopen( $parsed_args['filename'], 'w+' ); 291 } else { 292 $stream_handle = fopen( $parsed_args['filename'], 'w+' ); 293 } 294 295 if ( ! $stream_handle ) { 296 return new WP_Error( 297 'http_request_failed', 298 sprintf( 299 /* translators: 1: fopen(), 2: File name. */ 300 __( 'Could not open handle for %1$s to %2$s.' ), 301 'fopen()', 302 $parsed_args['filename'] 303 ) 304 ); 305 } 306 307 $bytes_written = 0; 308 309 while ( ! feof( $handle ) && $keep_reading ) { 310 $block = fread( $handle, $block_size ); 311 if ( ! $body_started ) { 312 $response .= $block; 313 if ( strpos( $response, "\r\n\r\n" ) ) { 314 $processed_response = WP_Http::processResponse( $response ); 315 $body_started = true; 316 $block = $processed_response['body']; 317 unset( $response ); 318 $processed_response['body'] = ''; 319 } 320 } 321 322 $this_block_size = strlen( $block ); 323 324 if ( isset( $parsed_args['limit_response_size'] ) 325 && ( $bytes_written + $this_block_size ) > $parsed_args['limit_response_size'] 326 ) { 327 $this_block_size = ( $parsed_args['limit_response_size'] - $bytes_written ); 328 $block = substr( $block, 0, $this_block_size ); 329 } 330 331 $bytes_written_to_file = fwrite( $stream_handle, $block ); 332 333 if ( $bytes_written_to_file !== $this_block_size ) { 334 fclose( $handle ); 335 fclose( $stream_handle ); 336 return new WP_Error( 'http_request_failed', __( 'Failed to write request to temporary file.' ) ); 337 } 338 339 $bytes_written += $bytes_written_to_file; 340 341 $keep_reading = ( 342 ! isset( $parsed_args['limit_response_size'] ) 343 || $bytes_written < $parsed_args['limit_response_size'] 344 ); 345 } 346 347 fclose( $stream_handle ); 348 349 } else { 350 $header_length = 0; 351 352 while ( ! feof( $handle ) && $keep_reading ) { 353 $block = fread( $handle, $block_size ); 354 $response .= $block; 355 356 if ( ! $body_started && strpos( $response, "\r\n\r\n" ) ) { 357 $header_length = strpos( $response, "\r\n\r\n" ) + 4; 358 $body_started = true; 359 } 360 361 $keep_reading = ( 362 ! $body_started 363 || ! isset( $parsed_args['limit_response_size'] ) 364 || strlen( $response ) < ( $header_length + $parsed_args['limit_response_size'] ) 365 ); 366 } 367 368 $processed_response = WP_Http::processResponse( $response ); 369 unset( $response ); 370 371 } 372 373 fclose( $handle ); 374 375 $processed_headers = WP_Http::processHeaders( $processed_response['headers'], $url ); 376 377 $response = array( 378 'headers' => $processed_headers['headers'], 379 // Not yet processed. 380 'body' => null, 381 'response' => $processed_headers['response'], 382 'cookies' => $processed_headers['cookies'], 383 'filename' => $parsed_args['filename'], 384 ); 385 386 // Handle redirects. 387 $redirect_response = WP_Http::handle_redirects( $url, $parsed_args, $response ); 388 if ( false !== $redirect_response ) { 389 return $redirect_response; 390 } 391 392 // If the body was chunk encoded, then decode it. 393 if ( ! empty( $processed_response['body'] ) 394 && isset( $processed_headers['headers']['transfer-encoding'] ) 395 && 'chunked' === $processed_headers['headers']['transfer-encoding'] 396 ) { 397 $processed_response['body'] = WP_Http::chunkTransferDecode( $processed_response['body'] ); 398 } 399 400 if ( true === $parsed_args['decompress'] 401 && true === WP_Http_Encoding::should_decode( $processed_headers['headers'] ) 402 ) { 403 $processed_response['body'] = WP_Http_Encoding::decompress( $processed_response['body'] ); 404 } 405 406 if ( isset( $parsed_args['limit_response_size'] ) 407 && strlen( $processed_response['body'] ) > $parsed_args['limit_response_size'] 408 ) { 409 $processed_response['body'] = substr( $processed_response['body'], 0, $parsed_args['limit_response_size'] ); 410 } 411 412 $response['body'] = $processed_response['body']; 413 414 return $response; 415 } 416 417 /** 418 * Verifies the received SSL certificate against its Common Names and subjectAltName fields. 419 * 420 * PHP's SSL verifications only verify that it's a valid Certificate, it doesn't verify if 421 * the certificate is valid for the hostname which was requested. 422 * This function verifies the requested hostname against certificate's subjectAltName field, 423 * if that is empty, or contains no DNS entries, a fallback to the Common Name field is used. 424 * 425 * IP Address support is included if the request is being made to an IP address. 426 * 427 * @since 3.7.0 428 * 429 * @param resource $stream The PHP Stream which the SSL request is being made over 430 * @param string $host The hostname being requested 431 * @return bool If the certificate presented in $stream is valid for $host 432 */ 433 public static function verify_ssl_certificate( $stream, $host ) { 434 $context_options = stream_context_get_options( $stream ); 435 436 if ( empty( $context_options['ssl']['peer_certificate'] ) ) { 437 return false; 438 } 439 440 $cert = openssl_x509_parse( $context_options['ssl']['peer_certificate'] ); 441 if ( ! $cert ) { 442 return false; 443 } 444 445 /* 446 * If the request is being made to an IP address, we'll validate against IP fields 447 * in the cert (if they exist) 448 */ 449 $host_type = ( WP_Http::is_ip_address( $host ) ? 'ip' : 'dns' ); 450 451 $certificate_hostnames = array(); 452 if ( ! empty( $cert['extensions']['subjectAltName'] ) ) { 453 $match_against = preg_split( '/,\s*/', $cert['extensions']['subjectAltName'] ); 454 foreach ( $match_against as $match ) { 455 list( $match_type, $match_host ) = explode( ':', $match ); 456 if ( strtolower( trim( $match_type ) ) === $host_type ) { // IP: or DNS: 457 $certificate_hostnames[] = strtolower( trim( $match_host ) ); 458 } 459 } 460 } elseif ( ! empty( $cert['subject']['CN'] ) ) { 461 // Only use the CN when the certificate includes no subjectAltName extension. 462 $certificate_hostnames[] = strtolower( $cert['subject']['CN'] ); 463 } 464 465 // Exact hostname/IP matches. 466 if ( in_array( strtolower( $host ), $certificate_hostnames, true ) ) { 467 return true; 468 } 469 470 // IP's can't be wildcards, Stop processing. 471 if ( 'ip' === $host_type ) { 472 return false; 473 } 474 475 // Test to see if the domain is at least 2 deep for wildcard support. 476 if ( substr_count( $host, '.' ) < 2 ) { 477 return false; 478 } 479 480 // Wildcard subdomains certs (*.example.com) are valid for a.example.com but not a.b.example.com. 481 $wildcard_host = preg_replace( '/^[^.]+\./', '*.', $host ); 482 483 return in_array( strtolower( $wildcard_host ), $certificate_hostnames, true ); 484 } 485 486 /** 487 * Determines whether this class can be used for retrieving a URL. 488 * 489 * @since 2.7.0 490 * @since 3.7.0 Combined with the fsockopen transport and switched to stream_socket_client(). 491 * 492 * @param array $args Optional. Array of request arguments. Default empty array. 493 * @return bool False means this class can not be used, true means it can. 494 */ 495 public static function test( $args = array() ) { 496 if ( ! function_exists( 'stream_socket_client' ) ) { 497 return false; 498 } 499 500 $is_ssl = isset( $args['ssl'] ) && $args['ssl']; 501 502 if ( $is_ssl ) { 503 if ( ! extension_loaded( 'openssl' ) ) { 504 return false; 505 } 506 if ( ! function_exists( 'openssl_x509_parse' ) ) { 507 return false; 508 } 509 } 510 511 /** 512 * Filters whether streams can be used as a transport for retrieving a URL. 513 * 514 * @since 2.7.0 515 * 516 * @param bool $use_class Whether the class can be used. Default true. 517 * @param array $args Request arguments. 518 */ 519 return apply_filters( 'use_streams_transport', true, $args ); 520 } 521 } 522 523 /** 524 * Deprecated HTTP Transport method which used fsockopen. 525 * 526 * This class is not used, and is included for backward compatibility only. 527 * All code should make use of WP_Http directly through its API. 528 * 529 * @see WP_HTTP::request 530 * 531 * @since 2.7.0 532 * @deprecated 3.7.0 Please use WP_HTTP::request() directly 533 */ 534 class WP_HTTP_Fsockopen extends WP_Http_Streams { 535 // For backward compatibility for users who are using the class directly. 536 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Thu Nov 21 08:20:01 2024 | Cross-referenced by PHPXref |