[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

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


Generated: Tue Oct 22 08:20:01 2019 Cross-referenced by PHPXref 0.7