WP_Http_Streams::test()   B
last analyzed

Complexity

Conditions 6
Paths 9

Size

Total Lines 23
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 10
nc 9
nop 1
dl 0
loc 23
rs 8.5906
c 0
b 0
f 0
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
	 * @access public
26
	 * @param string $url The request URL.
27
	 * @param string|array $args Optional. Override the defaults.
28
	 * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error
29
	 */
30
	public function request($url, $args = array()) {
31
		$defaults = array(
32
			'method' => 'GET', 'timeout' => 5,
33
			'redirection' => 5, 'httpversion' => '1.0',
34
			'blocking' => true,
35
			'headers' => array(), 'body' => null, 'cookies' => array()
36
		);
37
38
		$r = wp_parse_args( $args, $defaults );
39
40 View Code Duplication
		if ( isset( $r['headers']['User-Agent'] ) ) {
41
			$r['user-agent'] = $r['headers']['User-Agent'];
42
			unset( $r['headers']['User-Agent'] );
43
		} elseif ( isset( $r['headers']['user-agent'] ) ) {
44
			$r['user-agent'] = $r['headers']['user-agent'];
45
			unset( $r['headers']['user-agent'] );
46
		}
47
48
		// Construct Cookie: header if any cookies are set.
49
		WP_Http::buildCookieHeader( $r );
50
51
		$arrURL = parse_url($url);
52
53
		$connect_host = $arrURL['host'];
54
55
		$secure_transport = ( $arrURL['scheme'] == 'ssl' || $arrURL['scheme'] == 'https' );
56
		if ( ! isset( $arrURL['port'] ) ) {
57
			if ( $arrURL['scheme'] == 'ssl' || $arrURL['scheme'] == 'https' ) {
58
				$arrURL['port'] = 443;
59
				$secure_transport = true;
60
			} else {
61
				$arrURL['port'] = 80;
62
			}
63
		}
64
65
		// Always pass a Path, defaulting to the root in cases such as http://example.com
66
		if ( ! isset( $arrURL['path'] ) ) {
67
			$arrURL['path'] = '/';
68
		}
69
70
		if ( isset( $r['headers']['Host'] ) || isset( $r['headers']['host'] ) ) {
71
			if ( isset( $r['headers']['Host'] ) )
72
				$arrURL['host'] = $r['headers']['Host'];
73
			else
74
				$arrURL['host'] = $r['headers']['host'];
75
			unset( $r['headers']['Host'], $r['headers']['host'] );
76
		}
77
78
		/*
79
		 * Certain versions of PHP have issues with 'localhost' and IPv6, It attempts to connect
80
		 * to ::1, which fails when the server is not set up for it. For compatibility, always
81
		 * connect to the IPv4 address.
82
		 */
83
		if ( 'localhost' == strtolower( $connect_host ) )
84
			$connect_host = '127.0.0.1';
85
86
		$connect_host = $secure_transport ? 'ssl://' . $connect_host : 'tcp://' . $connect_host;
87
88
		$is_local = isset( $r['local'] ) && $r['local'];
89
		$ssl_verify = isset( $r['sslverify'] ) && $r['sslverify'];
90 View Code Duplication
		if ( $is_local ) {
91
			/**
92
			 * Filters whether SSL should be verified for local requests.
93
			 *
94
			 * @since 2.8.0
95
			 *
96
			 * @param bool $ssl_verify Whether to verify the SSL connection. Default true.
97
			 */
98
			$ssl_verify = apply_filters( 'https_local_ssl_verify', $ssl_verify );
99
		} elseif ( ! $is_local ) {
100
			/**
101
			 * Filters whether SSL should be verified for non-local requests.
102
			 *
103
			 * @since 2.8.0
104
			 *
105
			 * @param bool $ssl_verify Whether to verify the SSL connection. Default true.
106
			 */
107
			$ssl_verify = apply_filters( 'https_ssl_verify', $ssl_verify );
108
		}
109
110
		$proxy = new WP_HTTP_Proxy();
111
112
		$context = stream_context_create( array(
113
			'ssl' => array(
114
				'verify_peer' => $ssl_verify,
115
				//'CN_match' => $arrURL['host'], // This is handled by self::verify_ssl_certificate()
116
				'capture_peer_cert' => $ssl_verify,
117
				'SNI_enabled' => true,
118
				'cafile' => $r['sslcertificates'],
119
				'allow_self_signed' => ! $ssl_verify,
120
			)
121
		) );
122
123
		$timeout = (int) floor( $r['timeout'] );
124
		$utimeout = $timeout == $r['timeout'] ? 0 : 1000000 * $r['timeout'] % 1000000;
125
		$connect_timeout = max( $timeout, 1 );
126
127
		// Store error number.
128
		$connection_error = null;
129
130
		// Store error string.
131
		$connection_error_str = null;
132
133
		if ( !WP_DEBUG ) {
134
			// In the event that the SSL connection fails, silence the many PHP Warnings.
135
			if ( $secure_transport )
136
				$error_reporting = error_reporting(0);
137
138 View Code Duplication
			if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) )
139
				$handle = @stream_socket_client( 'tcp://' . $proxy->host() . ':' . $proxy->port(), $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context );
140
			else
141
				$handle = @stream_socket_client( $connect_host . ':' . $arrURL['port'], $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context );
142
143
			if ( $secure_transport )
144
				error_reporting( $error_reporting );
0 ignored issues
show
Bug introduced by
The variable $error_reporting does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
145
146 View Code Duplication
		} else {
147
			if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) )
148
				$handle = stream_socket_client( 'tcp://' . $proxy->host() . ':' . $proxy->port(), $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context );
149
			else
150
				$handle = stream_socket_client( $connect_host . ':' . $arrURL['port'], $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context );
151
		}
152
153
		if ( false === $handle ) {
154
			// SSL connection failed due to expired/invalid cert, or, OpenSSL configuration is broken.
155
			if ( $secure_transport && 0 === $connection_error && '' === $connection_error_str )
156
				return new WP_Error( 'http_request_failed', __( 'The SSL certificate for the host could not be verified.' ) );
157
158
			return new WP_Error('http_request_failed', $connection_error . ': ' . $connection_error_str );
159
		}
160
161
		// Verify that the SSL certificate is valid for this request.
162
		if ( $secure_transport && $ssl_verify && ! $proxy->is_enabled() ) {
163
			if ( ! self::verify_ssl_certificate( $handle, $arrURL['host'] ) )
164
				return new WP_Error( 'http_request_failed', __( 'The SSL certificate for the host could not be verified.' ) );
165
		}
166
167
		stream_set_timeout( $handle, $timeout, $utimeout );
168
169
		if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) //Some proxies require full URL in this field.
170
			$requestPath = $url;
171
		else
172
			$requestPath = $arrURL['path'] . ( isset($arrURL['query']) ? '?' . $arrURL['query'] : '' );
173
174
		$strHeaders = strtoupper($r['method']) . ' ' . $requestPath . ' HTTP/' . $r['httpversion'] . "\r\n";
175
176
		$include_port_in_host_header = (
177
			( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) ||
178
			( 'http'  == $arrURL['scheme'] && 80  != $arrURL['port'] ) ||
179
			( 'https' == $arrURL['scheme'] && 443 != $arrURL['port'] )
180
		);
181
182
		if ( $include_port_in_host_header ) {
183
			$strHeaders .= 'Host: ' . $arrURL['host'] . ':' . $arrURL['port'] . "\r\n";
184
		} else {
185
			$strHeaders .= 'Host: ' . $arrURL['host'] . "\r\n";
186
		}
187
188
		if ( isset($r['user-agent']) )
189
			$strHeaders .= 'User-agent: ' . $r['user-agent'] . "\r\n";
190
191
		if ( is_array($r['headers']) ) {
192
			foreach ( (array) $r['headers'] as $header => $headerValue )
193
				$strHeaders .= $header . ': ' . $headerValue . "\r\n";
194
		} else {
195
			$strHeaders .= $r['headers'];
196
		}
197
198
		if ( $proxy->use_authentication() )
199
			$strHeaders .= $proxy->authentication_header() . "\r\n";
200
201
		$strHeaders .= "\r\n";
202
203
		if ( ! is_null($r['body']) )
204
			$strHeaders .= $r['body'];
205
206
		fwrite($handle, $strHeaders);
207
208
		if ( ! $r['blocking'] ) {
209
			stream_set_blocking( $handle, 0 );
210
			fclose( $handle );
211
			return array( 'headers' => array(), 'body' => '', 'response' => array('code' => false, 'message' => false), 'cookies' => array() );
212
		}
213
214
		$strResponse = '';
215
		$bodyStarted = false;
216
		$keep_reading = true;
217
		$block_size = 4096;
218
		if ( isset( $r['limit_response_size'] ) )
219
			$block_size = min( $block_size, $r['limit_response_size'] );
220
221
		// If streaming to a file setup the file handle.
222
		if ( $r['stream'] ) {
223
			if ( ! WP_DEBUG )
224
				$stream_handle = @fopen( $r['filename'], 'w+' );
225
			else
226
				$stream_handle = fopen( $r['filename'], 'w+' );
227
			if ( ! $stream_handle )
228
				return new WP_Error( 'http_request_failed', sprintf( __( 'Could not open handle for fopen() to %s' ), $r['filename'] ) );
229
230
			$bytes_written = 0;
231
			while ( ! feof($handle) && $keep_reading ) {
232
				$block = fread( $handle, $block_size );
233
				if ( ! $bodyStarted ) {
234
					$strResponse .= $block;
235
					if ( strpos( $strResponse, "\r\n\r\n" ) ) {
236
						$process = WP_Http::processResponse( $strResponse );
237
						$bodyStarted = true;
238
						$block = $process['body'];
239
						unset( $strResponse );
240
						$process['body'] = '';
241
					}
242
				}
243
244
				$this_block_size = strlen( $block );
245
246
				if ( isset( $r['limit_response_size'] ) && ( $bytes_written + $this_block_size ) > $r['limit_response_size'] ) {
247
					$this_block_size = ( $r['limit_response_size'] - $bytes_written );
248
					$block = substr( $block, 0, $this_block_size );
249
				}
250
251
				$bytes_written_to_file = fwrite( $stream_handle, $block );
252
253
				if ( $bytes_written_to_file != $this_block_size ) {
254
					fclose( $handle );
255
					fclose( $stream_handle );
256
					return new WP_Error( 'http_request_failed', __( 'Failed to write request to temporary file.' ) );
257
				}
258
259
				$bytes_written += $bytes_written_to_file;
260
261
				$keep_reading = !isset( $r['limit_response_size'] ) || $bytes_written < $r['limit_response_size'];
262
			}
263
264
			fclose( $stream_handle );
265
266
		} else {
267
			$header_length = 0;
268
			while ( ! feof( $handle ) && $keep_reading ) {
269
				$block = fread( $handle, $block_size );
270
				$strResponse .= $block;
271
				if ( ! $bodyStarted && strpos( $strResponse, "\r\n\r\n" ) ) {
272
					$header_length = strpos( $strResponse, "\r\n\r\n" ) + 4;
273
					$bodyStarted = true;
274
				}
275
				$keep_reading = ( ! $bodyStarted || !isset( $r['limit_response_size'] ) || strlen( $strResponse ) < ( $header_length + $r['limit_response_size'] ) );
276
			}
277
278
			$process = WP_Http::processResponse( $strResponse );
279
			unset( $strResponse );
280
281
		}
282
283
		fclose( $handle );
284
285
		$arrHeaders = WP_Http::processHeaders( $process['headers'], $url );
0 ignored issues
show
Bug introduced by
The variable $process does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
286
287
		$response = array(
288
			'headers' => $arrHeaders['headers'],
289
			// Not yet processed.
290
			'body' => null,
291
			'response' => $arrHeaders['response'],
292
			'cookies' => $arrHeaders['cookies'],
293
			'filename' => $r['filename']
294
		);
295
296
		// Handle redirects.
297
		if ( false !== ( $redirect_response = WP_Http::handle_redirects( $url, $r, $response ) ) )
298
			return $redirect_response;
299
300
		// If the body was chunk encoded, then decode it.
301
		if ( ! empty( $process['body'] ) && isset( $arrHeaders['headers']['transfer-encoding'] ) && 'chunked' == $arrHeaders['headers']['transfer-encoding'] )
302
			$process['body'] = WP_Http::chunkTransferDecode($process['body']);
303
304 View Code Duplication
		if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode($arrHeaders['headers']) )
305
			$process['body'] = WP_Http_Encoding::decompress( $process['body'] );
306
307
		if ( isset( $r['limit_response_size'] ) && strlen( $process['body'] ) > $r['limit_response_size'] )
308
			$process['body'] = substr( $process['body'], 0, $r['limit_response_size'] );
309
310
		$response['body'] = $process['body'];
311
312
		return $response;
313
	}
314
315
	/**
316
	 * Verifies the received SSL certificate against its Common Names and subjectAltName fields.
317
	 *
318
	 * PHP's SSL verifications only verify that it's a valid Certificate, it doesn't verify if
319
	 * the certificate is valid for the hostname which was requested.
320
	 * This function verifies the requested hostname against certificate's subjectAltName field,
321
	 * if that is empty, or contains no DNS entries, a fallback to the Common Name field is used.
322
	 *
323
	 * IP Address support is included if the request is being made to an IP address.
324
	 *
325
	 * @since 3.7.0
326
	 * @static
327
	 *
328
	 * @param stream $stream The PHP Stream which the SSL request is being made over
329
	 * @param string $host The hostname being requested
330
	 * @return bool If the cerficiate presented in $stream is valid for $host
331
	 */
332
	public static function verify_ssl_certificate( $stream, $host ) {
333
		$context_options = stream_context_get_options( $stream );
334
335
		if ( empty( $context_options['ssl']['peer_certificate'] ) )
336
			return false;
337
338
		$cert = openssl_x509_parse( $context_options['ssl']['peer_certificate'] );
339
		if ( ! $cert )
340
			return false;
341
342
		/*
343
		 * If the request is being made to an IP address, we'll validate against IP fields
344
		 * in the cert (if they exist)
345
		 */
346
		$host_type = ( WP_Http::is_ip_address( $host ) ? 'ip' : 'dns' );
347
348
		$certificate_hostnames = array();
349
		if ( ! empty( $cert['extensions']['subjectAltName'] ) ) {
350
			$match_against = preg_split( '/,\s*/', $cert['extensions']['subjectAltName'] );
351
			foreach ( $match_against as $match ) {
352
				list( $match_type, $match_host ) = explode( ':', $match );
353
				if ( $host_type == strtolower( trim( $match_type ) ) ) // IP: or DNS:
354
					$certificate_hostnames[] = strtolower( trim( $match_host ) );
355
			}
356
		} elseif ( !empty( $cert['subject']['CN'] ) ) {
357
			// Only use the CN when the certificate includes no subjectAltName extension.
358
			$certificate_hostnames[] = strtolower( $cert['subject']['CN'] );
359
		}
360
361
		// Exact hostname/IP matches.
362
		if ( in_array( strtolower( $host ), $certificate_hostnames ) )
363
			return true;
364
365
		// IP's can't be wildcards, Stop processing.
366
		if ( 'ip' == $host_type )
367
			return false;
368
369
		// Test to see if the domain is at least 2 deep for wildcard support.
370
		if ( substr_count( $host, '.' ) < 2 )
371
			return false;
372
373
		// Wildcard subdomains certs (*.example.com) are valid for a.example.com but not a.b.example.com.
374
		$wildcard_host = preg_replace( '/^[^.]+\./', '*.', $host );
375
376
		return in_array( strtolower( $wildcard_host ), $certificate_hostnames );
377
	}
378
379
	/**
380
	 * Determines whether this class can be used for retrieving a URL.
381
	 *
382
	 * @static
383
	 * @access public
384
	 * @since 2.7.0
385
	 * @since 3.7.0 Combined with the fsockopen transport and switched to stream_socket_client().
386
	 *
387
	 * @param array $args Optional. Array of request arguments. Default empty array.
388
	 * @return bool False means this class can not be used, true means it can.
389
	 */
390
	public static function test( $args = array() ) {
391
		if ( ! function_exists( 'stream_socket_client' ) )
392
			return false;
393
394
		$is_ssl = isset( $args['ssl'] ) && $args['ssl'];
395
396
		if ( $is_ssl ) {
397
			if ( ! extension_loaded( 'openssl' ) )
398
				return false;
399
			if ( ! function_exists( 'openssl_x509_parse' ) )
400
				return false;
401
		}
402
403
		/**
404
		 * Filters whether streams can be used as a transport for retrieving a URL.
405
		 *
406
		 * @since 2.7.0
407
		 *
408
		 * @param bool  $use_class Whether the class can be used. Default true.
409
		 * @param array $args      Request arguments.
410
		 */
411
		return apply_filters( 'use_streams_transport', true, $args );
412
	}
413
}
414
415
/**
416
 * Deprecated HTTP Transport method which used fsockopen.
417
 *
418
 * This class is not used, and is included for backward compatibility only.
419
 * All code should make use of WP_Http directly through its API.
420
 *
421
 * @see WP_HTTP::request
422
 *
423
 * @since 2.7.0
424
 * @deprecated 3.7.0 Please use WP_HTTP::request() directly
425
 */
426
class WP_HTTP_Fsockopen extends WP_HTTP_Streams {
427
	// For backward compatibility for users who are using the class directly.
428
}
429