[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-http-streams.php (source)

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


Generated : Sun May 3 08:20:14 2026 Cross-referenced by PHPXref