PageRenderTime 26ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/htdocs/wp-includes/class-wp-http-streams.php

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