|
1 <?php |
|
2 /** |
|
3 * Simple and uniform HTTP request API. |
|
4 * |
|
5 * Standardizes the HTTP requests for WordPress. Handles cookies, gzip encoding and decoding, chunk |
|
6 * decoding, if HTTP 1.1 and various other difficult HTTP protocol implementations. |
|
7 * |
|
8 * @link http://trac.wordpress.org/ticket/4779 HTTP API Proposal |
|
9 * |
|
10 * @package WordPress |
|
11 * @subpackage HTTP |
|
12 * @since 2.7.0 |
|
13 */ |
|
14 |
|
15 /** |
|
16 * WordPress HTTP Class for managing HTTP Transports and making HTTP requests. |
|
17 * |
|
18 * This class is used to consistently make outgoing HTTP requests easy for developers |
|
19 * while still being compatible with the many PHP configurations under which |
|
20 * WordPress runs. |
|
21 * |
|
22 * Debugging includes several actions, which pass different variables for debugging the HTTP API. |
|
23 * |
|
24 * @package WordPress |
|
25 * @subpackage HTTP |
|
26 * @since 2.7.0 |
|
27 */ |
|
28 class WP_Http { |
|
29 |
|
30 /** |
|
31 * Send a HTTP request to a URI. |
|
32 * |
|
33 * The body and headers are part of the arguments. The 'body' argument is for the body and will |
|
34 * accept either a string or an array. The 'headers' argument should be an array, but a string |
|
35 * is acceptable. If the 'body' argument is an array, then it will automatically be escaped |
|
36 * using http_build_query(). |
|
37 * |
|
38 * The only URI that are supported in the HTTP Transport implementation are the HTTP and HTTPS |
|
39 * protocols. |
|
40 * |
|
41 * The defaults are 'method', 'timeout', 'redirection', 'httpversion', 'blocking' and |
|
42 * 'user-agent'. |
|
43 * |
|
44 * Accepted 'method' values are 'GET', 'POST', and 'HEAD', some transports technically allow |
|
45 * others, but should not be assumed. The 'timeout' is used to sent how long the connection |
|
46 * should stay open before failing when no response. 'redirection' is used to track how many |
|
47 * redirects were taken and used to sent the amount for other transports, but not all transports |
|
48 * accept setting that value. |
|
49 * |
|
50 * The 'httpversion' option is used to sent the HTTP version and accepted values are '1.0', and |
|
51 * '1.1' and should be a string. The 'user-agent' option is the user-agent and is used to |
|
52 * replace the default user-agent, which is 'WordPress/WP_Version', where WP_Version is the |
|
53 * value from $wp_version. |
|
54 * |
|
55 * The 'blocking' parameter can be used to specify if the calling code requires the result of |
|
56 * the HTTP request. If set to false, the request will be sent to the remote server, and |
|
57 * processing returned to the calling code immediately, the caller will know if the request |
|
58 * suceeded or failed, but will not receive any response from the remote server. |
|
59 * |
|
60 * @access public |
|
61 * @since 2.7.0 |
|
62 * |
|
63 * @param string $url URI resource. |
|
64 * @param str|array $args Optional. Override the defaults. |
|
65 * @return array|object Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error |
|
66 */ |
|
67 function request( $url, $args = array() ) { |
|
68 global $wp_version; |
|
69 |
|
70 $defaults = array( |
|
71 'method' => 'GET', |
|
72 'timeout' => apply_filters( 'http_request_timeout', 5), |
|
73 'redirection' => apply_filters( 'http_request_redirection_count', 5), |
|
74 'httpversion' => apply_filters( 'http_request_version', '1.0'), |
|
75 'user-agent' => apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' ) ), |
|
76 'reject_unsafe_urls' => apply_filters( 'http_request_reject_unsafe_urls', false ), |
|
77 'blocking' => true, |
|
78 'headers' => array(), |
|
79 'cookies' => array(), |
|
80 'body' => null, |
|
81 'compress' => false, |
|
82 'decompress' => true, |
|
83 'sslverify' => true, |
|
84 'sslcertificates' => ABSPATH . WPINC . '/certificates/ca-bundle.crt', |
|
85 'stream' => false, |
|
86 'filename' => null, |
|
87 'limit_response_size' => null, |
|
88 ); |
|
89 |
|
90 // Pre-parse for the HEAD checks. |
|
91 $args = wp_parse_args( $args ); |
|
92 |
|
93 // By default, Head requests do not cause redirections. |
|
94 if ( isset($args['method']) && 'HEAD' == $args['method'] ) |
|
95 $defaults['redirection'] = 0; |
|
96 |
|
97 $r = wp_parse_args( $args, $defaults ); |
|
98 $r = apply_filters( 'http_request_args', $r, $url ); |
|
99 |
|
100 // The transports decrement this, store a copy of the original value for loop purposes. |
|
101 if ( ! isset( $r['_redirection'] ) ) |
|
102 $r['_redirection'] = $r['redirection']; |
|
103 |
|
104 // Allow plugins to short-circuit the request |
|
105 $pre = apply_filters( 'pre_http_request', false, $r, $url ); |
|
106 if ( false !== $pre ) |
|
107 return $pre; |
|
108 |
|
109 if ( function_exists( 'wp_kses_bad_protocol' ) ) { |
|
110 if ( $r['reject_unsafe_urls'] ) |
|
111 $url = wp_http_validate_url( $url ); |
|
112 $url = wp_kses_bad_protocol( $url, array( 'http', 'https', 'ssl' ) ); |
|
113 } |
|
114 |
|
115 $arrURL = @parse_url( $url ); |
|
116 |
|
117 if ( empty( $url ) || empty( $arrURL['scheme'] ) ) |
|
118 return new WP_Error('http_request_failed', __('A valid URL was not provided.')); |
|
119 |
|
120 if ( $this->block_request( $url ) ) |
|
121 return new WP_Error( 'http_request_failed', __( 'User has blocked requests through HTTP.' ) ); |
|
122 |
|
123 // Determine if this is a https call and pass that on to the transport functions |
|
124 // so that we can blacklist the transports that do not support ssl verification |
|
125 $r['ssl'] = $arrURL['scheme'] == 'https' || $arrURL['scheme'] == 'ssl'; |
|
126 |
|
127 // Determine if this request is to OUR install of WordPress |
|
128 $homeURL = parse_url( get_bloginfo( 'url' ) ); |
|
129 $r['local'] = $homeURL['host'] == $arrURL['host'] || 'localhost' == $arrURL['host']; |
|
130 unset( $homeURL ); |
|
131 |
|
132 // If we are streaming to a file but no filename was given drop it in the WP temp dir |
|
133 // and pick its name using the basename of the $url |
|
134 if ( $r['stream'] && empty( $r['filename'] ) ) |
|
135 $r['filename'] = get_temp_dir() . basename( $url ); |
|
136 |
|
137 // Force some settings if we are streaming to a file and check for existence and perms of destination directory |
|
138 if ( $r['stream'] ) { |
|
139 $r['blocking'] = true; |
|
140 if ( ! wp_is_writable( dirname( $r['filename'] ) ) ) |
|
141 return new WP_Error( 'http_request_failed', __( 'Destination directory for file streaming does not exist or is not writable.' ) ); |
|
142 } |
|
143 |
|
144 if ( is_null( $r['headers'] ) ) |
|
145 $r['headers'] = array(); |
|
146 |
|
147 if ( ! is_array( $r['headers'] ) ) { |
|
148 $processedHeaders = WP_Http::processHeaders( $r['headers'], $url ); |
|
149 $r['headers'] = $processedHeaders['headers']; |
|
150 } |
|
151 |
|
152 if ( isset( $r['headers']['User-Agent'] ) ) { |
|
153 $r['user-agent'] = $r['headers']['User-Agent']; |
|
154 unset( $r['headers']['User-Agent'] ); |
|
155 } |
|
156 |
|
157 if ( isset( $r['headers']['user-agent'] ) ) { |
|
158 $r['user-agent'] = $r['headers']['user-agent']; |
|
159 unset( $r['headers']['user-agent'] ); |
|
160 } |
|
161 |
|
162 if ( '1.1' == $r['httpversion'] && !isset( $r['headers']['connection'] ) ) { |
|
163 $r['headers']['connection'] = 'close'; |
|
164 } |
|
165 |
|
166 // Construct Cookie: header if any cookies are set |
|
167 WP_Http::buildCookieHeader( $r ); |
|
168 |
|
169 // Avoid issues where mbstring.func_overload is enabled |
|
170 mbstring_binary_safe_encoding(); |
|
171 |
|
172 if ( ! isset( $r['headers']['Accept-Encoding'] ) ) { |
|
173 if ( $encoding = WP_Http_Encoding::accept_encoding( $url, $r ) ) |
|
174 $r['headers']['Accept-Encoding'] = $encoding; |
|
175 } |
|
176 |
|
177 if ( ( ! is_null( $r['body'] ) && '' != $r['body'] ) || 'POST' == $r['method'] || 'PUT' == $r['method'] ) { |
|
178 if ( is_array( $r['body'] ) || is_object( $r['body'] ) ) { |
|
179 $r['body'] = http_build_query( $r['body'], null, '&' ); |
|
180 |
|
181 if ( ! isset( $r['headers']['Content-Type'] ) ) |
|
182 $r['headers']['Content-Type'] = 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' ); |
|
183 } |
|
184 |
|
185 if ( '' === $r['body'] ) |
|
186 $r['body'] = null; |
|
187 |
|
188 if ( ! isset( $r['headers']['Content-Length'] ) && ! isset( $r['headers']['content-length'] ) ) |
|
189 $r['headers']['Content-Length'] = strlen( $r['body'] ); |
|
190 } |
|
191 |
|
192 $response = $this->_dispatch_request( $url, $r ); |
|
193 |
|
194 reset_mbstring_encoding(); |
|
195 |
|
196 if ( is_wp_error( $response ) ) |
|
197 return $response; |
|
198 |
|
199 // Append cookies that were used in this request to the response |
|
200 if ( ! empty( $r['cookies'] ) ) { |
|
201 $cookies_set = wp_list_pluck( $response['cookies'], 'name' ); |
|
202 foreach ( $r['cookies'] as $cookie ) { |
|
203 if ( ! in_array( $cookie->name, $cookies_set ) && $cookie->test( $url ) ) { |
|
204 $response['cookies'][] = $cookie; |
|
205 } |
|
206 } |
|
207 } |
|
208 |
|
209 return $response; |
|
210 } |
|
211 |
|
212 /** |
|
213 * Tests which transports are capable of supporting the request. |
|
214 * |
|
215 * @since 3.2.0 |
|
216 * @access private |
|
217 * |
|
218 * @param array $args Request arguments |
|
219 * @param string $url URL to Request |
|
220 * |
|
221 * @return string|bool Class name for the first transport that claims to support the request. False if no transport claims to support the request. |
|
222 */ |
|
223 public function _get_first_available_transport( $args, $url = null ) { |
|
224 $request_order = apply_filters( 'http_api_transports', array( 'curl', 'streams' ), $args, $url ); |
|
225 |
|
226 // Loop over each transport on each HTTP request looking for one which will serve this request's needs |
|
227 foreach ( $request_order as $transport ) { |
|
228 $class = 'WP_HTTP_' . $transport; |
|
229 |
|
230 // Check to see if this transport is a possibility, calls the transport statically |
|
231 if ( !call_user_func( array( $class, 'test' ), $args, $url ) ) |
|
232 continue; |
|
233 |
|
234 return $class; |
|
235 } |
|
236 |
|
237 return false; |
|
238 } |
|
239 |
|
240 /** |
|
241 * Dispatches a HTTP request to a supporting transport. |
|
242 * |
|
243 * Tests each transport in order to find a transport which matches the request arguments. |
|
244 * Also caches the transport instance to be used later. |
|
245 * |
|
246 * The order for requests is cURL, and then PHP Streams. |
|
247 * |
|
248 * @since 3.2.0 |
|
249 * @access private |
|
250 * |
|
251 * @param string $url URL to Request |
|
252 * @param array $args Request arguments |
|
253 * @return array|object Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error |
|
254 */ |
|
255 private function _dispatch_request( $url, $args ) { |
|
256 static $transports = array(); |
|
257 |
|
258 $class = $this->_get_first_available_transport( $args, $url ); |
|
259 if ( !$class ) |
|
260 return new WP_Error( 'http_failure', __( 'There are no HTTP transports available which can complete the requested request.' ) ); |
|
261 |
|
262 // Transport claims to support request, instantiate it and give it a whirl. |
|
263 if ( empty( $transports[$class] ) ) |
|
264 $transports[$class] = new $class; |
|
265 |
|
266 $response = $transports[$class]->request( $url, $args ); |
|
267 |
|
268 do_action( 'http_api_debug', $response, 'response', $class, $args, $url ); |
|
269 |
|
270 if ( is_wp_error( $response ) ) |
|
271 return $response; |
|
272 |
|
273 return apply_filters( 'http_response', $response, $args, $url ); |
|
274 } |
|
275 |
|
276 /** |
|
277 * Uses the POST HTTP method. |
|
278 * |
|
279 * Used for sending data that is expected to be in the body. |
|
280 * |
|
281 * @access public |
|
282 * @since 2.7.0 |
|
283 * |
|
284 * @param string $url URI resource. |
|
285 * @param str|array $args Optional. Override the defaults. |
|
286 * @return array|object Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error |
|
287 */ |
|
288 function post($url, $args = array()) { |
|
289 $defaults = array('method' => 'POST'); |
|
290 $r = wp_parse_args( $args, $defaults ); |
|
291 return $this->request($url, $r); |
|
292 } |
|
293 |
|
294 /** |
|
295 * Uses the GET HTTP method. |
|
296 * |
|
297 * Used for sending data that is expected to be in the body. |
|
298 * |
|
299 * @access public |
|
300 * @since 2.7.0 |
|
301 * |
|
302 * @param string $url URI resource. |
|
303 * @param str|array $args Optional. Override the defaults. |
|
304 * @return array|object Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error |
|
305 */ |
|
306 function get($url, $args = array()) { |
|
307 $defaults = array('method' => 'GET'); |
|
308 $r = wp_parse_args( $args, $defaults ); |
|
309 return $this->request($url, $r); |
|
310 } |
|
311 |
|
312 /** |
|
313 * Uses the HEAD HTTP method. |
|
314 * |
|
315 * Used for sending data that is expected to be in the body. |
|
316 * |
|
317 * @access public |
|
318 * @since 2.7.0 |
|
319 * |
|
320 * @param string $url URI resource. |
|
321 * @param str|array $args Optional. Override the defaults. |
|
322 * @return array|object Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error |
|
323 */ |
|
324 function head($url, $args = array()) { |
|
325 $defaults = array('method' => 'HEAD'); |
|
326 $r = wp_parse_args( $args, $defaults ); |
|
327 return $this->request($url, $r); |
|
328 } |
|
329 |
|
330 /** |
|
331 * Parses the responses and splits the parts into headers and body. |
|
332 * |
|
333 * @access public |
|
334 * @static |
|
335 * @since 2.7.0 |
|
336 * |
|
337 * @param string $strResponse The full response string |
|
338 * @return array Array with 'headers' and 'body' keys. |
|
339 */ |
|
340 public static function processResponse($strResponse) { |
|
341 $res = explode("\r\n\r\n", $strResponse, 2); |
|
342 |
|
343 return array('headers' => $res[0], 'body' => isset($res[1]) ? $res[1] : ''); |
|
344 } |
|
345 |
|
346 /** |
|
347 * Transform header string into an array. |
|
348 * |
|
349 * If an array is given then it is assumed to be raw header data with numeric keys with the |
|
350 * headers as the values. No headers must be passed that were already processed. |
|
351 * |
|
352 * @access public |
|
353 * @static |
|
354 * @since 2.7.0 |
|
355 * |
|
356 * @param string|array $headers |
|
357 * @param string $url The URL that was requested |
|
358 * @return array Processed string headers. If duplicate headers are encountered, |
|
359 * Then a numbered array is returned as the value of that header-key. |
|
360 */ |
|
361 public static function processHeaders( $headers, $url = '' ) { |
|
362 // split headers, one per array element |
|
363 if ( is_string($headers) ) { |
|
364 // tolerate line terminator: CRLF = LF (RFC 2616 19.3) |
|
365 $headers = str_replace("\r\n", "\n", $headers); |
|
366 // unfold folded header fields. LWS = [CRLF] 1*( SP | HT ) <US-ASCII SP, space (32)>, <US-ASCII HT, horizontal-tab (9)> (RFC 2616 2.2) |
|
367 $headers = preg_replace('/\n[ \t]/', ' ', $headers); |
|
368 // create the headers array |
|
369 $headers = explode("\n", $headers); |
|
370 } |
|
371 |
|
372 $response = array('code' => 0, 'message' => ''); |
|
373 |
|
374 // If a redirection has taken place, The headers for each page request may have been passed. |
|
375 // In this case, determine the final HTTP header and parse from there. |
|
376 for ( $i = count($headers)-1; $i >= 0; $i-- ) { |
|
377 if ( !empty($headers[$i]) && false === strpos($headers[$i], ':') ) { |
|
378 $headers = array_splice($headers, $i); |
|
379 break; |
|
380 } |
|
381 } |
|
382 |
|
383 $cookies = array(); |
|
384 $newheaders = array(); |
|
385 foreach ( (array) $headers as $tempheader ) { |
|
386 if ( empty($tempheader) ) |
|
387 continue; |
|
388 |
|
389 if ( false === strpos($tempheader, ':') ) { |
|
390 $stack = explode(' ', $tempheader, 3); |
|
391 $stack[] = ''; |
|
392 list( , $response['code'], $response['message']) = $stack; |
|
393 continue; |
|
394 } |
|
395 |
|
396 list($key, $value) = explode(':', $tempheader, 2); |
|
397 |
|
398 $key = strtolower( $key ); |
|
399 $value = trim( $value ); |
|
400 |
|
401 if ( isset( $newheaders[ $key ] ) ) { |
|
402 if ( ! is_array( $newheaders[ $key ] ) ) |
|
403 $newheaders[$key] = array( $newheaders[ $key ] ); |
|
404 $newheaders[ $key ][] = $value; |
|
405 } else { |
|
406 $newheaders[ $key ] = $value; |
|
407 } |
|
408 if ( 'set-cookie' == $key ) |
|
409 $cookies[] = new WP_Http_Cookie( $value, $url ); |
|
410 } |
|
411 |
|
412 return array('response' => $response, 'headers' => $newheaders, 'cookies' => $cookies); |
|
413 } |
|
414 |
|
415 /** |
|
416 * Takes the arguments for a ::request() and checks for the cookie array. |
|
417 * |
|
418 * If it's found, then it upgrades any basic name => value pairs to WP_Http_Cookie instances, |
|
419 * which are each parsed into strings and added to the Cookie: header (within the arguments array). |
|
420 * Edits the array by reference. |
|
421 * |
|
422 * @access public |
|
423 * @version 2.8.0 |
|
424 * @static |
|
425 * |
|
426 * @param array $r Full array of args passed into ::request() |
|
427 */ |
|
428 public static function buildCookieHeader( &$r ) { |
|
429 if ( ! empty($r['cookies']) ) { |
|
430 // Upgrade any name => value cookie pairs to WP_HTTP_Cookie instances |
|
431 foreach ( $r['cookies'] as $name => $value ) { |
|
432 if ( ! is_object( $value ) ) |
|
433 $r['cookies'][ $name ] = new WP_HTTP_Cookie( array( 'name' => $name, 'value' => $value ) ); |
|
434 } |
|
435 |
|
436 $cookies_header = ''; |
|
437 foreach ( (array) $r['cookies'] as $cookie ) { |
|
438 $cookies_header .= $cookie->getHeaderValue() . '; '; |
|
439 } |
|
440 |
|
441 $cookies_header = substr( $cookies_header, 0, -2 ); |
|
442 $r['headers']['cookie'] = $cookies_header; |
|
443 } |
|
444 } |
|
445 |
|
446 /** |
|
447 * Decodes chunk transfer-encoding, based off the HTTP 1.1 specification. |
|
448 * |
|
449 * Based off the HTTP http_encoding_dechunk function. |
|
450 * |
|
451 * @link http://tools.ietf.org/html/rfc2616#section-19.4.6 Process for chunked decoding. |
|
452 * |
|
453 * @access public |
|
454 * @since 2.7.0 |
|
455 * @static |
|
456 * |
|
457 * @param string $body Body content |
|
458 * @return string Chunked decoded body on success or raw body on failure. |
|
459 */ |
|
460 public static function chunkTransferDecode( $body ) { |
|
461 // The body is not chunked encoded or is malformed. |
|
462 if ( ! preg_match( '/^([0-9a-f]+)[^\r\n]*\r\n/i', trim( $body ) ) ) |
|
463 return $body; |
|
464 |
|
465 $parsed_body = ''; |
|
466 $body_original = $body; // We'll be altering $body, so need a backup in case of error |
|
467 |
|
468 while ( true ) { |
|
469 $has_chunk = (bool) preg_match( '/^([0-9a-f]+)[^\r\n]*\r\n/i', $body, $match ); |
|
470 if ( ! $has_chunk || empty( $match[1] ) ) |
|
471 return $body_original; |
|
472 |
|
473 $length = hexdec( $match[1] ); |
|
474 $chunk_length = strlen( $match[0] ); |
|
475 |
|
476 // Parse out the chunk of data |
|
477 $parsed_body .= substr( $body, $chunk_length, $length ); |
|
478 |
|
479 // Remove the chunk from the raw data |
|
480 $body = substr( $body, $length + $chunk_length ); |
|
481 |
|
482 // End of document |
|
483 if ( '0' === trim( $body ) ) |
|
484 return $parsed_body; |
|
485 } |
|
486 } |
|
487 |
|
488 /** |
|
489 * Block requests through the proxy. |
|
490 * |
|
491 * Those who are behind a proxy and want to prevent access to certain hosts may do so. This will |
|
492 * prevent plugins from working and core functionality, if you don't include api.wordpress.org. |
|
493 * |
|
494 * You block external URL requests by defining WP_HTTP_BLOCK_EXTERNAL as true in your wp-config.php |
|
495 * file and this will only allow localhost and your blog to make requests. The constant |
|
496 * WP_ACCESSIBLE_HOSTS will allow additional hosts to go through for requests. The format of the |
|
497 * WP_ACCESSIBLE_HOSTS constant is a comma separated list of hostnames to allow, wildcard domains |
|
498 * are supported, eg *.wordpress.org will allow for all subdomains of wordpress.org to be contacted. |
|
499 * |
|
500 * @since 2.8.0 |
|
501 * @link http://core.trac.wordpress.org/ticket/8927 Allow preventing external requests. |
|
502 * @link http://core.trac.wordpress.org/ticket/14636 Allow wildcard domains in WP_ACCESSIBLE_HOSTS |
|
503 * |
|
504 * @param string $uri URI of url. |
|
505 * @return bool True to block, false to allow. |
|
506 */ |
|
507 function block_request($uri) { |
|
508 // We don't need to block requests, because nothing is blocked. |
|
509 if ( ! defined( 'WP_HTTP_BLOCK_EXTERNAL' ) || ! WP_HTTP_BLOCK_EXTERNAL ) |
|
510 return false; |
|
511 |
|
512 $check = parse_url($uri); |
|
513 if ( ! $check ) |
|
514 return true; |
|
515 |
|
516 $home = parse_url( get_option('siteurl') ); |
|
517 |
|
518 // Don't block requests back to ourselves by default |
|
519 if ( $check['host'] == 'localhost' || $check['host'] == $home['host'] ) |
|
520 return apply_filters('block_local_requests', false); |
|
521 |
|
522 if ( !defined('WP_ACCESSIBLE_HOSTS') ) |
|
523 return true; |
|
524 |
|
525 static $accessible_hosts; |
|
526 static $wildcard_regex = false; |
|
527 if ( null == $accessible_hosts ) { |
|
528 $accessible_hosts = preg_split('|,\s*|', WP_ACCESSIBLE_HOSTS); |
|
529 |
|
530 if ( false !== strpos(WP_ACCESSIBLE_HOSTS, '*') ) { |
|
531 $wildcard_regex = array(); |
|
532 foreach ( $accessible_hosts as $host ) |
|
533 $wildcard_regex[] = str_replace( '\*', '.+', preg_quote( $host, '/' ) ); |
|
534 $wildcard_regex = '/^(' . implode('|', $wildcard_regex) . ')$/i'; |
|
535 } |
|
536 } |
|
537 |
|
538 if ( !empty($wildcard_regex) ) |
|
539 return !preg_match($wildcard_regex, $check['host']); |
|
540 else |
|
541 return !in_array( $check['host'], $accessible_hosts ); //Inverse logic, If it's in the array, then we can't access it. |
|
542 |
|
543 } |
|
544 |
|
545 static function make_absolute_url( $maybe_relative_path, $url ) { |
|
546 if ( empty( $url ) ) |
|
547 return $maybe_relative_path; |
|
548 |
|
549 // Check for a scheme |
|
550 if ( false !== strpos( $maybe_relative_path, '://' ) ) |
|
551 return $maybe_relative_path; |
|
552 |
|
553 if ( ! $url_parts = @parse_url( $url ) ) |
|
554 return $maybe_relative_path; |
|
555 |
|
556 if ( ! $relative_url_parts = @parse_url( $maybe_relative_path ) ) |
|
557 return $maybe_relative_path; |
|
558 |
|
559 $absolute_path = $url_parts['scheme'] . '://' . $url_parts['host']; |
|
560 if ( isset( $url_parts['port'] ) ) |
|
561 $absolute_path .= ':' . $url_parts['port']; |
|
562 |
|
563 // Start off with the Absolute URL path |
|
564 $path = ! empty( $url_parts['path'] ) ? $url_parts['path'] : '/'; |
|
565 |
|
566 // If it's a root-relative path, then great |
|
567 if ( ! empty( $relative_url_parts['path'] ) && '/' == $relative_url_parts['path'][0] ) { |
|
568 $path = $relative_url_parts['path']; |
|
569 |
|
570 // Else it's a relative path |
|
571 } elseif ( ! empty( $relative_url_parts['path'] ) ) { |
|
572 // Strip off any file components from the absolute path |
|
573 $path = substr( $path, 0, strrpos( $path, '/' ) + 1 ); |
|
574 |
|
575 // Build the new path |
|
576 $path .= $relative_url_parts['path']; |
|
577 |
|
578 // Strip all /path/../ out of the path |
|
579 while ( strpos( $path, '../' ) > 1 ) { |
|
580 $path = preg_replace( '![^/]+/\.\./!', '', $path ); |
|
581 } |
|
582 |
|
583 // Strip any final leading ../ from the path |
|
584 $path = preg_replace( '!^/(\.\./)+!', '', $path ); |
|
585 } |
|
586 |
|
587 // Add the Query string |
|
588 if ( ! empty( $relative_url_parts['query'] ) ) |
|
589 $path .= '?' . $relative_url_parts['query']; |
|
590 |
|
591 return $absolute_path . '/' . ltrim( $path, '/' ); |
|
592 } |
|
593 |
|
594 /** |
|
595 * Handles HTTP Redirects and follows them if appropriate. |
|
596 * |
|
597 * @since 3.7.0 |
|
598 * |
|
599 * @param string $url The URL which was requested. |
|
600 * @param array $args The Arguements which were used to make the request. |
|
601 * @param array $response The Response of the HTTP request. |
|
602 * @return false|object False if no redirect is present, a WP_HTTP or WP_Error result otherwise. |
|
603 */ |
|
604 static function handle_redirects( $url, $args, $response ) { |
|
605 // If no redirects are present, or, redirects were not requested, perform no action. |
|
606 if ( ! isset( $response['headers']['location'] ) || 0 === $args['_redirection'] ) |
|
607 return false; |
|
608 |
|
609 // Only perform redirections on redirection http codes |
|
610 if ( $response['response']['code'] > 399 || $response['response']['code'] < 300 ) |
|
611 return false; |
|
612 |
|
613 // Don't redirect if we've run out of redirects |
|
614 if ( $args['redirection']-- <= 0 ) |
|
615 return new WP_Error( 'http_request_failed', __('Too many redirects.') ); |
|
616 |
|
617 $redirect_location = $response['headers']['location']; |
|
618 |
|
619 // If there were multiple Location headers, use the last header specified |
|
620 if ( is_array( $redirect_location ) ) |
|
621 $redirect_location = array_pop( $redirect_location ); |
|
622 |
|
623 $redirect_location = WP_HTTP::make_absolute_url( $redirect_location, $url ); |
|
624 |
|
625 // POST requests should not POST to a redirected location |
|
626 if ( 'POST' == $args['method'] ) { |
|
627 if ( in_array( $response['response']['code'], array( 302, 303 ) ) ) |
|
628 $args['method'] = 'GET'; |
|
629 } |
|
630 |
|
631 // Include valid cookies in the redirect process |
|
632 if ( ! empty( $response['cookies'] ) ) { |
|
633 foreach ( $response['cookies'] as $cookie ) { |
|
634 if ( $cookie->test( $redirect_location ) ) |
|
635 $args['cookies'][] = $cookie; |
|
636 } |
|
637 } |
|
638 |
|
639 return wp_remote_request( $redirect_location, $args ); |
|
640 } |
|
641 |
|
642 /** |
|
643 * Determines if a specified string represents an IP address or not. |
|
644 * |
|
645 * This function also detects the type of the IP address, returning either |
|
646 * '4' or '6' to represent a IPv4 and IPv6 address respectively. |
|
647 * This does not verify if the IP is a valid IP, only that it appears to be |
|
648 * an IP address. |
|
649 * |
|
650 * @see http://home.deds.nl/~aeron/regex/ for IPv6 regex |
|
651 * |
|
652 * @since 3.7.0 |
|
653 * @static |
|
654 * |
|
655 * @param string $maybe_ip A suspected IP address |
|
656 * @return integer|bool Upon success, '4' or '6' to represent a IPv4 or IPv6 address, false upon failure |
|
657 */ |
|
658 static function is_ip_address( $maybe_ip ) { |
|
659 if ( preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $maybe_ip ) ) |
|
660 return 4; |
|
661 |
|
662 if ( false !== strpos( $maybe_ip, ':' ) && preg_match( '/^(((?=.*(::))(?!.*\3.+\3))\3?|([\dA-F]{1,4}(\3|:\b|$)|\2))(?4){5}((?4){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})$/i', trim( $maybe_ip, ' []' ) ) ) |
|
663 return 6; |
|
664 |
|
665 return false; |
|
666 } |
|
667 |
|
668 } |
|
669 |
|
670 /** |
|
671 * HTTP request method uses PHP Streams to retrieve the url. |
|
672 * |
|
673 * @package WordPress |
|
674 * @subpackage HTTP |
|
675 * |
|
676 * @since 2.7.0 |
|
677 * @since 3.7.0 Combined with the fsockopen transport and switched to stream_socket_client(). |
|
678 */ |
|
679 class WP_Http_Streams { |
|
680 /** |
|
681 * Send a HTTP request to a URI using PHP Streams. |
|
682 * |
|
683 * @see WP_Http::request For default options descriptions. |
|
684 * |
|
685 * @since 2.7.0 |
|
686 * @since 3.7.0 Combined with the fsockopen transport and switched to stream_socket_client(). |
|
687 * |
|
688 * @access public |
|
689 * @param string $url URI resource. |
|
690 * @param string|array $args Optional. Override the defaults. |
|
691 * @return array 'headers', 'body', 'response', 'cookies' and 'filename' keys. |
|
692 */ |
|
693 function request($url, $args = array()) { |
|
694 $defaults = array( |
|
695 'method' => 'GET', 'timeout' => 5, |
|
696 'redirection' => 5, 'httpversion' => '1.0', |
|
697 'blocking' => true, |
|
698 'headers' => array(), 'body' => null, 'cookies' => array() |
|
699 ); |
|
700 |
|
701 $r = wp_parse_args( $args, $defaults ); |
|
702 |
|
703 if ( isset($r['headers']['User-Agent']) ) { |
|
704 $r['user-agent'] = $r['headers']['User-Agent']; |
|
705 unset($r['headers']['User-Agent']); |
|
706 } else if ( isset($r['headers']['user-agent']) ) { |
|
707 $r['user-agent'] = $r['headers']['user-agent']; |
|
708 unset($r['headers']['user-agent']); |
|
709 } |
|
710 |
|
711 // Construct Cookie: header if any cookies are set |
|
712 WP_Http::buildCookieHeader( $r ); |
|
713 |
|
714 $arrURL = parse_url($url); |
|
715 |
|
716 $connect_host = $arrURL['host']; |
|
717 |
|
718 $secure_transport = ( $arrURL['scheme'] == 'ssl' || $arrURL['scheme'] == 'https' ); |
|
719 if ( ! isset( $arrURL['port'] ) ) { |
|
720 if ( $arrURL['scheme'] == 'ssl' || $arrURL['scheme'] == 'https' ) { |
|
721 $arrURL['port'] = 443; |
|
722 $secure_transport = true; |
|
723 } else { |
|
724 $arrURL['port'] = 80; |
|
725 } |
|
726 } |
|
727 |
|
728 if ( isset( $r['headers']['Host'] ) || isset( $r['headers']['host'] ) ) { |
|
729 if ( isset( $r['headers']['Host'] ) ) |
|
730 $arrURL['host'] = $r['headers']['Host']; |
|
731 else |
|
732 $arrURL['host'] = $r['headers']['host']; |
|
733 unset( $r['headers']['Host'], $r['headers']['host'] ); |
|
734 } |
|
735 |
|
736 // Certain versions of PHP have issues with 'localhost' and IPv6, It attempts to connect to ::1, |
|
737 // which fails when the server is not set up for it. For compatibility, always connect to the IPv4 address. |
|
738 if ( 'localhost' == strtolower( $connect_host ) ) |
|
739 $connect_host = '127.0.0.1'; |
|
740 |
|
741 $connect_host = $secure_transport ? 'ssl://' . $connect_host : 'tcp://' . $connect_host; |
|
742 |
|
743 $is_local = isset( $r['local'] ) && $r['local']; |
|
744 $ssl_verify = isset( $r['sslverify'] ) && $r['sslverify']; |
|
745 if ( $is_local ) |
|
746 $ssl_verify = apply_filters( 'https_local_ssl_verify', $ssl_verify ); |
|
747 elseif ( ! $is_local ) |
|
748 $ssl_verify = apply_filters( 'https_ssl_verify', $ssl_verify ); |
|
749 |
|
750 $proxy = new WP_HTTP_Proxy(); |
|
751 |
|
752 $context = stream_context_create( array( |
|
753 'ssl' => array( |
|
754 'verify_peer' => $ssl_verify, |
|
755 //'CN_match' => $arrURL['host'], // This is handled by self::verify_ssl_certificate() |
|
756 'capture_peer_cert' => $ssl_verify, |
|
757 'SNI_enabled' => true, |
|
758 'cafile' => $r['sslcertificates'], |
|
759 'allow_self_signed' => ! $ssl_verify, |
|
760 ) |
|
761 ) ); |
|
762 |
|
763 $timeout = (int) floor( $r['timeout'] ); |
|
764 $utimeout = $timeout == $r['timeout'] ? 0 : 1000000 * $r['timeout'] % 1000000; |
|
765 $connect_timeout = max( $timeout, 1 ); |
|
766 |
|
767 $connection_error = null; // Store error number |
|
768 $connection_error_str = null; // Store error string |
|
769 |
|
770 if ( !WP_DEBUG ) { |
|
771 // In the event that the SSL connection fails, silence the many PHP Warnings |
|
772 if ( $secure_transport ) |
|
773 $error_reporting = error_reporting(0); |
|
774 |
|
775 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) |
|
776 $handle = @stream_socket_client( 'tcp://' . $proxy->host() . ':' . $proxy->port(), $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context ); |
|
777 else |
|
778 $handle = @stream_socket_client( $connect_host . ':' . $arrURL['port'], $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context ); |
|
779 |
|
780 if ( $secure_transport ) |
|
781 error_reporting( $error_reporting ); |
|
782 |
|
783 } else { |
|
784 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) |
|
785 $handle = stream_socket_client( 'tcp://' . $proxy->host() . ':' . $proxy->port(), $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context ); |
|
786 else |
|
787 $handle = stream_socket_client( $connect_host . ':' . $arrURL['port'], $connection_error, $connection_error_str, $connect_timeout, STREAM_CLIENT_CONNECT, $context ); |
|
788 } |
|
789 |
|
790 if ( false === $handle ) { |
|
791 // SSL connection failed due to expired/invalid cert, or, OpenSSL configuration is broken |
|
792 if ( $secure_transport && 0 === $connection_error && '' === $connection_error_str ) |
|
793 return new WP_Error( 'http_request_failed', __( 'The SSL certificate for the host could not be verified.' ) ); |
|
794 |
|
795 return new WP_Error('http_request_failed', $connection_error . ': ' . $connection_error_str ); |
|
796 } |
|
797 |
|
798 // Verify that the SSL certificate is valid for this request |
|
799 if ( $secure_transport && $ssl_verify && ! $proxy->is_enabled() ) { |
|
800 if ( ! self::verify_ssl_certificate( $handle, $arrURL['host'] ) ) |
|
801 return new WP_Error( 'http_request_failed', __( 'The SSL certificate for the host could not be verified.' ) ); |
|
802 } |
|
803 |
|
804 stream_set_timeout( $handle, $timeout, $utimeout ); |
|
805 |
|
806 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) //Some proxies require full URL in this field. |
|
807 $requestPath = $url; |
|
808 else |
|
809 $requestPath = $arrURL['path'] . ( isset($arrURL['query']) ? '?' . $arrURL['query'] : '' ); |
|
810 |
|
811 if ( empty($requestPath) ) |
|
812 $requestPath .= '/'; |
|
813 |
|
814 $strHeaders = strtoupper($r['method']) . ' ' . $requestPath . ' HTTP/' . $r['httpversion'] . "\r\n"; |
|
815 |
|
816 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) |
|
817 $strHeaders .= 'Host: ' . $arrURL['host'] . ':' . $arrURL['port'] . "\r\n"; |
|
818 else |
|
819 $strHeaders .= 'Host: ' . $arrURL['host'] . "\r\n"; |
|
820 |
|
821 if ( isset($r['user-agent']) ) |
|
822 $strHeaders .= 'User-agent: ' . $r['user-agent'] . "\r\n"; |
|
823 |
|
824 if ( is_array($r['headers']) ) { |
|
825 foreach ( (array) $r['headers'] as $header => $headerValue ) |
|
826 $strHeaders .= $header . ': ' . $headerValue . "\r\n"; |
|
827 } else { |
|
828 $strHeaders .= $r['headers']; |
|
829 } |
|
830 |
|
831 if ( $proxy->use_authentication() ) |
|
832 $strHeaders .= $proxy->authentication_header() . "\r\n"; |
|
833 |
|
834 $strHeaders .= "\r\n"; |
|
835 |
|
836 if ( ! is_null($r['body']) ) |
|
837 $strHeaders .= $r['body']; |
|
838 |
|
839 fwrite($handle, $strHeaders); |
|
840 |
|
841 if ( ! $r['blocking'] ) { |
|
842 stream_set_blocking( $handle, 0 ); |
|
843 fclose( $handle ); |
|
844 return array( 'headers' => array(), 'body' => '', 'response' => array('code' => false, 'message' => false), 'cookies' => array() ); |
|
845 } |
|
846 |
|
847 $strResponse = ''; |
|
848 $bodyStarted = false; |
|
849 $keep_reading = true; |
|
850 $block_size = 4096; |
|
851 if ( isset( $r['limit_response_size'] ) ) |
|
852 $block_size = min( $block_size, $r['limit_response_size'] ); |
|
853 |
|
854 // If streaming to a file setup the file handle |
|
855 if ( $r['stream'] ) { |
|
856 if ( ! WP_DEBUG ) |
|
857 $stream_handle = @fopen( $r['filename'], 'w+' ); |
|
858 else |
|
859 $stream_handle = fopen( $r['filename'], 'w+' ); |
|
860 if ( ! $stream_handle ) |
|
861 return new WP_Error( 'http_request_failed', sprintf( __( 'Could not open handle for fopen() to %s' ), $r['filename'] ) ); |
|
862 |
|
863 $bytes_written = 0; |
|
864 while ( ! feof($handle) && $keep_reading ) { |
|
865 $block = fread( $handle, $block_size ); |
|
866 if ( ! $bodyStarted ) { |
|
867 $strResponse .= $block; |
|
868 if ( strpos( $strResponse, "\r\n\r\n" ) ) { |
|
869 $process = WP_Http::processResponse( $strResponse ); |
|
870 $bodyStarted = true; |
|
871 $block = $process['body']; |
|
872 unset( $strResponse ); |
|
873 $process['body'] = ''; |
|
874 } |
|
875 } |
|
876 |
|
877 $this_block_size = strlen( $block ); |
|
878 |
|
879 if ( isset( $r['limit_response_size'] ) && ( $bytes_written + $this_block_size ) > $r['limit_response_size'] ) |
|
880 $block = substr( $block, 0, ( $r['limit_response_size'] - $bytes_written ) ); |
|
881 |
|
882 $bytes_written_to_file = fwrite( $stream_handle, $block ); |
|
883 |
|
884 if ( $bytes_written_to_file != $this_block_size ) { |
|
885 fclose( $handle ); |
|
886 fclose( $stream_handle ); |
|
887 return new WP_Error( 'http_request_failed', __( 'Failed to write request to temporary file.' ) ); |
|
888 } |
|
889 |
|
890 $bytes_written += $bytes_written_to_file; |
|
891 |
|
892 $keep_reading = !isset( $r['limit_response_size'] ) || $bytes_written < $r['limit_response_size']; |
|
893 } |
|
894 |
|
895 fclose( $stream_handle ); |
|
896 |
|
897 } else { |
|
898 $header_length = 0; |
|
899 while ( ! feof( $handle ) && $keep_reading ) { |
|
900 $block = fread( $handle, $block_size ); |
|
901 $strResponse .= $block; |
|
902 if ( ! $bodyStarted && strpos( $strResponse, "\r\n\r\n" ) ) { |
|
903 $header_length = strpos( $strResponse, "\r\n\r\n" ) + 4; |
|
904 $bodyStarted = true; |
|
905 } |
|
906 $keep_reading = ( ! $bodyStarted || !isset( $r['limit_response_size'] ) || strlen( $strResponse ) < ( $header_length + $r['limit_response_size'] ) ); |
|
907 } |
|
908 |
|
909 $process = WP_Http::processResponse( $strResponse ); |
|
910 unset( $strResponse ); |
|
911 |
|
912 } |
|
913 |
|
914 fclose( $handle ); |
|
915 |
|
916 $arrHeaders = WP_Http::processHeaders( $process['headers'], $url ); |
|
917 |
|
918 $response = array( |
|
919 'headers' => $arrHeaders['headers'], |
|
920 'body' => null, // Not yet processed |
|
921 'response' => $arrHeaders['response'], |
|
922 'cookies' => $arrHeaders['cookies'], |
|
923 'filename' => $r['filename'] |
|
924 ); |
|
925 |
|
926 // Handle redirects |
|
927 if ( false !== ( $redirect_response = WP_HTTP::handle_redirects( $url, $r, $response ) ) ) |
|
928 return $redirect_response; |
|
929 |
|
930 // If the body was chunk encoded, then decode it. |
|
931 if ( ! empty( $process['body'] ) && isset( $arrHeaders['headers']['transfer-encoding'] ) && 'chunked' == $arrHeaders['headers']['transfer-encoding'] ) |
|
932 $process['body'] = WP_Http::chunkTransferDecode($process['body']); |
|
933 |
|
934 if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode($arrHeaders['headers']) ) |
|
935 $process['body'] = WP_Http_Encoding::decompress( $process['body'] ); |
|
936 |
|
937 if ( isset( $r['limit_response_size'] ) && strlen( $process['body'] ) > $r['limit_response_size'] ) |
|
938 $process['body'] = substr( $process['body'], 0, $r['limit_response_size'] ); |
|
939 |
|
940 $response['body'] = $process['body']; |
|
941 |
|
942 return $response; |
|
943 } |
|
944 |
|
945 /** |
|
946 * Verifies the received SSL certificate against it's Common Names and subjectAltName fields |
|
947 * |
|
948 * PHP's SSL verifications only verify that it's a valid Certificate, it doesn't verify if |
|
949 * the certificate is valid for the hostname which was requested. |
|
950 * This function verifies the requested hostname against certificate's subjectAltName field, |
|
951 * if that is empty, or contains no DNS entries, a fallback to the Common Name field is used. |
|
952 * |
|
953 * IP Address support is included if the request is being made to an IP address. |
|
954 * |
|
955 * @since 3.7.0 |
|
956 * @static |
|
957 * |
|
958 * @param stream $stream The PHP Stream which the SSL request is being made over |
|
959 * @param string $host The hostname being requested |
|
960 * @return bool If the cerficiate presented in $stream is valid for $host |
|
961 */ |
|
962 static function verify_ssl_certificate( $stream, $host ) { |
|
963 $context_options = stream_context_get_options( $stream ); |
|
964 |
|
965 if ( empty( $context_options['ssl']['peer_certificate'] ) ) |
|
966 return false; |
|
967 |
|
968 $cert = openssl_x509_parse( $context_options['ssl']['peer_certificate'] ); |
|
969 if ( ! $cert ) |
|
970 return false; |
|
971 |
|
972 // If the request is being made to an IP address, we'll validate against IP fields in the cert (if they exist) |
|
973 $host_type = ( WP_HTTP::is_ip_address( $host ) ? 'ip' : 'dns' ); |
|
974 |
|
975 $certificate_hostnames = array(); |
|
976 if ( ! empty( $cert['extensions']['subjectAltName'] ) ) { |
|
977 $match_against = preg_split( '/,\s*/', $cert['extensions']['subjectAltName'] ); |
|
978 foreach ( $match_against as $match ) { |
|
979 list( $match_type, $match_host ) = explode( ':', $match ); |
|
980 if ( $host_type == strtolower( trim( $match_type ) ) ) // IP: or DNS: |
|
981 $certificate_hostnames[] = strtolower( trim( $match_host ) ); |
|
982 } |
|
983 } elseif ( !empty( $cert['subject']['CN'] ) ) { |
|
984 // Only use the CN when the certificate includes no subjectAltName extension |
|
985 $certificate_hostnames[] = strtolower( $cert['subject']['CN'] ); |
|
986 } |
|
987 |
|
988 // Exact hostname/IP matches |
|
989 if ( in_array( strtolower( $host ), $certificate_hostnames ) ) |
|
990 return true; |
|
991 |
|
992 // IP's can't be wildcards, Stop processing |
|
993 if ( 'ip' == $host_type ) |
|
994 return false; |
|
995 |
|
996 // Test to see if the domain is at least 2 deep for wildcard support |
|
997 if ( substr_count( $host, '.' ) < 2 ) |
|
998 return false; |
|
999 |
|
1000 // Wildcard subdomains certs (*.example.com) are valid for a.example.com but not a.b.example.com |
|
1001 $wildcard_host = preg_replace( '/^[^.]+\./', '*.', $host ); |
|
1002 |
|
1003 return in_array( strtolower( $wildcard_host ), $certificate_hostnames ); |
|
1004 } |
|
1005 |
|
1006 /** |
|
1007 * Whether this class can be used for retrieving an URL. |
|
1008 * |
|
1009 * @static |
|
1010 * @access public |
|
1011 * @since 2.7.0 |
|
1012 * @since 3.7.0 Combined with the fsockopen transport and switched to stream_socket_client(). |
|
1013 * |
|
1014 * @return boolean False means this class can not be used, true means it can. |
|
1015 */ |
|
1016 public static function test( $args = array() ) { |
|
1017 if ( ! function_exists( 'stream_socket_client' ) ) |
|
1018 return false; |
|
1019 |
|
1020 $is_ssl = isset( $args['ssl'] ) && $args['ssl']; |
|
1021 |
|
1022 if ( $is_ssl ) { |
|
1023 if ( ! extension_loaded( 'openssl' ) ) |
|
1024 return false; |
|
1025 if ( ! function_exists( 'openssl_x509_parse' ) ) |
|
1026 return false; |
|
1027 } |
|
1028 |
|
1029 return apply_filters( 'use_streams_transport', true, $args ); |
|
1030 } |
|
1031 } |
|
1032 |
|
1033 /** |
|
1034 * Deprecated HTTP Transport method which used fsockopen. |
|
1035 * |
|
1036 * This class is not used, and is included for backwards compatibility only. |
|
1037 * All code should make use of WP_HTTP directly through it's API. |
|
1038 * |
|
1039 * @see WP_HTTP::request |
|
1040 * |
|
1041 * @package WordPress |
|
1042 * @subpackage HTTP |
|
1043 * |
|
1044 * @since 2.7.0 |
|
1045 * @deprecated 3.7.0 Please use WP_HTTP::request() directly |
|
1046 */ |
|
1047 class WP_HTTP_Fsockopen extends WP_HTTP_Streams { |
|
1048 // For backwards compatibility for users who are using the class directly |
|
1049 } |
|
1050 |
|
1051 /** |
|
1052 * HTTP request method uses Curl extension to retrieve the url. |
|
1053 * |
|
1054 * Requires the Curl extension to be installed. |
|
1055 * |
|
1056 * @package WordPress |
|
1057 * @subpackage HTTP |
|
1058 * @since 2.7.0 |
|
1059 */ |
|
1060 class WP_Http_Curl { |
|
1061 |
|
1062 /** |
|
1063 * Temporary header storage for during requests. |
|
1064 * |
|
1065 * @since 3.2.0 |
|
1066 * @access private |
|
1067 * @var string |
|
1068 */ |
|
1069 private $headers = ''; |
|
1070 |
|
1071 /** |
|
1072 * Temporary body storage for during requests. |
|
1073 * |
|
1074 * @since 3.6.0 |
|
1075 * @access private |
|
1076 * @var string |
|
1077 */ |
|
1078 private $body = ''; |
|
1079 |
|
1080 /** |
|
1081 * The maximum amount of data to recieve from the remote server |
|
1082 * |
|
1083 * @since 3.6.0 |
|
1084 * @access private |
|
1085 * @var int |
|
1086 */ |
|
1087 private $max_body_length = false; |
|
1088 |
|
1089 /** |
|
1090 * The file resource used for streaming to file. |
|
1091 * |
|
1092 * @since 3.6.0 |
|
1093 * @access private |
|
1094 * @var resource |
|
1095 */ |
|
1096 private $stream_handle = false; |
|
1097 |
|
1098 /** |
|
1099 * Send a HTTP request to a URI using cURL extension. |
|
1100 * |
|
1101 * @access public |
|
1102 * @since 2.7.0 |
|
1103 * |
|
1104 * @param string $url |
|
1105 * @param str|array $args Optional. Override the defaults. |
|
1106 * @return array 'headers', 'body', 'response', 'cookies' and 'filename' keys. |
|
1107 */ |
|
1108 function request($url, $args = array()) { |
|
1109 $defaults = array( |
|
1110 'method' => 'GET', 'timeout' => 5, |
|
1111 'redirection' => 5, 'httpversion' => '1.0', |
|
1112 'blocking' => true, |
|
1113 'headers' => array(), 'body' => null, 'cookies' => array() |
|
1114 ); |
|
1115 |
|
1116 $r = wp_parse_args( $args, $defaults ); |
|
1117 |
|
1118 if ( isset($r['headers']['User-Agent']) ) { |
|
1119 $r['user-agent'] = $r['headers']['User-Agent']; |
|
1120 unset($r['headers']['User-Agent']); |
|
1121 } else if ( isset($r['headers']['user-agent']) ) { |
|
1122 $r['user-agent'] = $r['headers']['user-agent']; |
|
1123 unset($r['headers']['user-agent']); |
|
1124 } |
|
1125 |
|
1126 // Construct Cookie: header if any cookies are set. |
|
1127 WP_Http::buildCookieHeader( $r ); |
|
1128 |
|
1129 $handle = curl_init(); |
|
1130 |
|
1131 // cURL offers really easy proxy support. |
|
1132 $proxy = new WP_HTTP_Proxy(); |
|
1133 |
|
1134 if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) { |
|
1135 |
|
1136 curl_setopt( $handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP ); |
|
1137 curl_setopt( $handle, CURLOPT_PROXY, $proxy->host() ); |
|
1138 curl_setopt( $handle, CURLOPT_PROXYPORT, $proxy->port() ); |
|
1139 |
|
1140 if ( $proxy->use_authentication() ) { |
|
1141 curl_setopt( $handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY ); |
|
1142 curl_setopt( $handle, CURLOPT_PROXYUSERPWD, $proxy->authentication() ); |
|
1143 } |
|
1144 } |
|
1145 |
|
1146 $is_local = isset($r['local']) && $r['local']; |
|
1147 $ssl_verify = isset($r['sslverify']) && $r['sslverify']; |
|
1148 if ( $is_local ) |
|
1149 $ssl_verify = apply_filters('https_local_ssl_verify', $ssl_verify); |
|
1150 elseif ( ! $is_local ) |
|
1151 $ssl_verify = apply_filters('https_ssl_verify', $ssl_verify); |
|
1152 |
|
1153 // CURLOPT_TIMEOUT and CURLOPT_CONNECTTIMEOUT expect integers. Have to use ceil since |
|
1154 // a value of 0 will allow an unlimited timeout. |
|
1155 $timeout = (int) ceil( $r['timeout'] ); |
|
1156 curl_setopt( $handle, CURLOPT_CONNECTTIMEOUT, $timeout ); |
|
1157 curl_setopt( $handle, CURLOPT_TIMEOUT, $timeout ); |
|
1158 |
|
1159 curl_setopt( $handle, CURLOPT_URL, $url); |
|
1160 curl_setopt( $handle, CURLOPT_RETURNTRANSFER, true ); |
|
1161 curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, ( $ssl_verify === true ) ? 2 : false ); |
|
1162 curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, $ssl_verify ); |
|
1163 curl_setopt( $handle, CURLOPT_CAINFO, $r['sslcertificates'] ); |
|
1164 curl_setopt( $handle, CURLOPT_USERAGENT, $r['user-agent'] ); |
|
1165 // The option doesn't work with safe mode or when open_basedir is set, and there's a |
|
1166 // bug #17490 with redirected POST requests, so handle redirections outside Curl. |
|
1167 curl_setopt( $handle, CURLOPT_FOLLOWLOCATION, false ); |
|
1168 if ( defined( 'CURLOPT_PROTOCOLS' ) ) // PHP 5.2.10 / cURL 7.19.4 |
|
1169 curl_setopt( $handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS ); |
|
1170 |
|
1171 switch ( $r['method'] ) { |
|
1172 case 'HEAD': |
|
1173 curl_setopt( $handle, CURLOPT_NOBODY, true ); |
|
1174 break; |
|
1175 case 'POST': |
|
1176 curl_setopt( $handle, CURLOPT_POST, true ); |
|
1177 curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] ); |
|
1178 break; |
|
1179 case 'PUT': |
|
1180 curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, 'PUT' ); |
|
1181 curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] ); |
|
1182 break; |
|
1183 default: |
|
1184 curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, $r['method'] ); |
|
1185 if ( ! is_null( $r['body'] ) ) |
|
1186 curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] ); |
|
1187 break; |
|
1188 } |
|
1189 |
|
1190 if ( true === $r['blocking'] ) { |
|
1191 curl_setopt( $handle, CURLOPT_HEADERFUNCTION, array( $this, 'stream_headers' ) ); |
|
1192 curl_setopt( $handle, CURLOPT_WRITEFUNCTION, array( $this, 'stream_body' ) ); |
|
1193 } |
|
1194 |
|
1195 curl_setopt( $handle, CURLOPT_HEADER, false ); |
|
1196 |
|
1197 if ( isset( $r['limit_response_size'] ) ) |
|
1198 $this->max_body_length = intval( $r['limit_response_size'] ); |
|
1199 else |
|
1200 $this->max_body_length = false; |
|
1201 |
|
1202 // If streaming to a file open a file handle, and setup our curl streaming handler |
|
1203 if ( $r['stream'] ) { |
|
1204 if ( ! WP_DEBUG ) |
|
1205 $this->stream_handle = @fopen( $r['filename'], 'w+' ); |
|
1206 else |
|
1207 $this->stream_handle = fopen( $r['filename'], 'w+' ); |
|
1208 if ( ! $this->stream_handle ) |
|
1209 return new WP_Error( 'http_request_failed', sprintf( __( 'Could not open handle for fopen() to %s' ), $r['filename'] ) ); |
|
1210 } else { |
|
1211 $this->stream_handle = false; |
|
1212 } |
|
1213 |
|
1214 if ( !empty( $r['headers'] ) ) { |
|
1215 // cURL expects full header strings in each element |
|
1216 $headers = array(); |
|
1217 foreach ( $r['headers'] as $name => $value ) { |
|
1218 $headers[] = "{$name}: $value"; |
|
1219 } |
|
1220 curl_setopt( $handle, CURLOPT_HTTPHEADER, $headers ); |
|
1221 } |
|
1222 |
|
1223 if ( $r['httpversion'] == '1.0' ) |
|
1224 curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0 ); |
|
1225 else |
|
1226 curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 ); |
|
1227 |
|
1228 // Cookies are not handled by the HTTP API currently. Allow for plugin authors to handle it |
|
1229 // themselves... Although, it is somewhat pointless without some reference. |
|
1230 do_action_ref_array( 'http_api_curl', array(&$handle) ); |
|
1231 |
|
1232 // We don't need to return the body, so don't. Just execute request and return. |
|
1233 if ( ! $r['blocking'] ) { |
|
1234 curl_exec( $handle ); |
|
1235 |
|
1236 if ( $curl_error = curl_error( $handle ) ) { |
|
1237 curl_close( $handle ); |
|
1238 return new WP_Error( 'http_request_failed', $curl_error ); |
|
1239 } |
|
1240 if ( in_array( curl_getinfo( $handle, CURLINFO_HTTP_CODE ), array( 301, 302 ) ) ) { |
|
1241 curl_close( $handle ); |
|
1242 return new WP_Error( 'http_request_failed', __( 'Too many redirects.' ) ); |
|
1243 } |
|
1244 |
|
1245 curl_close( $handle ); |
|
1246 return array( 'headers' => array(), 'body' => '', 'response' => array('code' => false, 'message' => false), 'cookies' => array() ); |
|
1247 } |
|
1248 |
|
1249 $theResponse = curl_exec( $handle ); |
|
1250 $theHeaders = WP_Http::processHeaders( $this->headers, $url ); |
|
1251 $theBody = $this->body; |
|
1252 |
|
1253 $this->headers = ''; |
|
1254 $this->body = ''; |
|
1255 |
|
1256 $curl_error = curl_errno( $handle ); |
|
1257 |
|
1258 // If an error occured, or, no response |
|
1259 if ( $curl_error || ( 0 == strlen( $theBody ) && empty( $theHeaders['headers'] ) ) ) { |
|
1260 if ( CURLE_WRITE_ERROR /* 23 */ == $curl_error && $r['stream'] ) { |
|
1261 fclose( $this->stream_handle ); |
|
1262 return new WP_Error( 'http_request_failed', __( 'Failed to write request to temporary file.' ) ); |
|
1263 } |
|
1264 if ( $curl_error = curl_error( $handle ) ) { |
|
1265 curl_close( $handle ); |
|
1266 return new WP_Error( 'http_request_failed', $curl_error ); |
|
1267 } |
|
1268 if ( in_array( curl_getinfo( $handle, CURLINFO_HTTP_CODE ), array( 301, 302 ) ) ) { |
|
1269 curl_close( $handle ); |
|
1270 return new WP_Error( 'http_request_failed', __( 'Too many redirects.' ) ); |
|
1271 } |
|
1272 } |
|
1273 |
|
1274 $response = array(); |
|
1275 $response['code'] = curl_getinfo( $handle, CURLINFO_HTTP_CODE ); |
|
1276 $response['message'] = get_status_header_desc($response['code']); |
|
1277 |
|
1278 curl_close( $handle ); |
|
1279 |
|
1280 if ( $r['stream'] ) |
|
1281 fclose( $this->stream_handle ); |
|
1282 |
|
1283 $response = array( |
|
1284 'headers' => $theHeaders['headers'], |
|
1285 'body' => null, |
|
1286 'response' => $response, |
|
1287 'cookies' => $theHeaders['cookies'], |
|
1288 'filename' => $r['filename'] |
|
1289 ); |
|
1290 |
|
1291 // Handle redirects |
|
1292 if ( false !== ( $redirect_response = WP_HTTP::handle_redirects( $url, $r, $response ) ) ) |
|
1293 return $redirect_response; |
|
1294 |
|
1295 if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode($theHeaders['headers']) ) |
|
1296 $theBody = WP_Http_Encoding::decompress( $theBody ); |
|
1297 |
|
1298 $response['body'] = $theBody; |
|
1299 |
|
1300 return $response; |
|
1301 } |
|
1302 |
|
1303 /** |
|
1304 * Grab the headers of the cURL request |
|
1305 * |
|
1306 * Each header is sent individually to this callback, so we append to the $header property for temporary storage |
|
1307 * |
|
1308 * @since 3.2.0 |
|
1309 * @access private |
|
1310 * @return int |
|
1311 */ |
|
1312 private function stream_headers( $handle, $headers ) { |
|
1313 $this->headers .= $headers; |
|
1314 return strlen( $headers ); |
|
1315 } |
|
1316 |
|
1317 /** |
|
1318 * Grab the body of the cURL request |
|
1319 * |
|
1320 * The contents of the document are passed in chunks, so we append to the $body property for temporary storage. |
|
1321 * Returning a length shorter than the length of $data passed in will cause cURL to abort the request as "completed" |
|
1322 * |
|
1323 * @since 3.6.0 |
|
1324 * @access private |
|
1325 * @return int |
|
1326 */ |
|
1327 private function stream_body( $handle, $data ) { |
|
1328 $data_length = strlen( $data ); |
|
1329 |
|
1330 if ( $this->max_body_length && ( strlen( $this->body ) + $data_length ) > $this->max_body_length ) |
|
1331 $data = substr( $data, 0, ( $this->max_body_length - $data_length ) ); |
|
1332 |
|
1333 if ( $this->stream_handle ) { |
|
1334 $bytes_written = fwrite( $this->stream_handle, $data ); |
|
1335 } else { |
|
1336 $this->body .= $data; |
|
1337 $bytes_written = $data_length; |
|
1338 } |
|
1339 |
|
1340 // Upon event of this function returning less than strlen( $data ) curl will error with CURLE_WRITE_ERROR |
|
1341 return $bytes_written; |
|
1342 } |
|
1343 |
|
1344 /** |
|
1345 * Whether this class can be used for retrieving an URL. |
|
1346 * |
|
1347 * @static |
|
1348 * @since 2.7.0 |
|
1349 * |
|
1350 * @return boolean False means this class can not be used, true means it can. |
|
1351 */ |
|
1352 public static function test( $args = array() ) { |
|
1353 if ( ! function_exists( 'curl_init' ) || ! function_exists( 'curl_exec' ) ) |
|
1354 return false; |
|
1355 |
|
1356 $is_ssl = isset( $args['ssl'] ) && $args['ssl']; |
|
1357 |
|
1358 if ( $is_ssl ) { |
|
1359 $curl_version = curl_version(); |
|
1360 if ( ! (CURL_VERSION_SSL & $curl_version['features']) ) // Does this cURL version support SSL requests? |
|
1361 return false; |
|
1362 } |
|
1363 |
|
1364 return apply_filters( 'use_curl_transport', true, $args ); |
|
1365 } |
|
1366 } |
|
1367 |
|
1368 /** |
|
1369 * Adds Proxy support to the WordPress HTTP API. |
|
1370 * |
|
1371 * There are caveats to proxy support. It requires that defines be made in the wp-config.php file to |
|
1372 * enable proxy support. There are also a few filters that plugins can hook into for some of the |
|
1373 * constants. |
|
1374 * |
|
1375 * Please note that only BASIC authentication is supported by most transports. |
|
1376 * cURL MAY support more methods (such as NTLM authentication) depending on your environment. |
|
1377 * |
|
1378 * The constants are as follows: |
|
1379 * <ol> |
|
1380 * <li>WP_PROXY_HOST - Enable proxy support and host for connecting.</li> |
|
1381 * <li>WP_PROXY_PORT - Proxy port for connection. No default, must be defined.</li> |
|
1382 * <li>WP_PROXY_USERNAME - Proxy username, if it requires authentication.</li> |
|
1383 * <li>WP_PROXY_PASSWORD - Proxy password, if it requires authentication.</li> |
|
1384 * <li>WP_PROXY_BYPASS_HOSTS - Will prevent the hosts in this list from going through the proxy. |
|
1385 * You do not need to have localhost and the blog host in this list, because they will not be passed |
|
1386 * through the proxy. The list should be presented in a comma separated list, wildcards using * are supported, eg. *.wordpress.org</li> |
|
1387 * </ol> |
|
1388 * |
|
1389 * An example can be as seen below. |
|
1390 * <code> |
|
1391 * define('WP_PROXY_HOST', '192.168.84.101'); |
|
1392 * define('WP_PROXY_PORT', '8080'); |
|
1393 * define('WP_PROXY_BYPASS_HOSTS', 'localhost, www.example.com, *.wordpress.org'); |
|
1394 * </code> |
|
1395 * |
|
1396 * @link http://core.trac.wordpress.org/ticket/4011 Proxy support ticket in WordPress. |
|
1397 * @link http://core.trac.wordpress.org/ticket/14636 Allow wildcard domains in WP_PROXY_BYPASS_HOSTS |
|
1398 * @since 2.8 |
|
1399 */ |
|
1400 class WP_HTTP_Proxy { |
|
1401 |
|
1402 /** |
|
1403 * Whether proxy connection should be used. |
|
1404 * |
|
1405 * @since 2.8 |
|
1406 * @use WP_PROXY_HOST |
|
1407 * @use WP_PROXY_PORT |
|
1408 * |
|
1409 * @return bool |
|
1410 */ |
|
1411 function is_enabled() { |
|
1412 return defined('WP_PROXY_HOST') && defined('WP_PROXY_PORT'); |
|
1413 } |
|
1414 |
|
1415 /** |
|
1416 * Whether authentication should be used. |
|
1417 * |
|
1418 * @since 2.8 |
|
1419 * @use WP_PROXY_USERNAME |
|
1420 * @use WP_PROXY_PASSWORD |
|
1421 * |
|
1422 * @return bool |
|
1423 */ |
|
1424 function use_authentication() { |
|
1425 return defined('WP_PROXY_USERNAME') && defined('WP_PROXY_PASSWORD'); |
|
1426 } |
|
1427 |
|
1428 /** |
|
1429 * Retrieve the host for the proxy server. |
|
1430 * |
|
1431 * @since 2.8 |
|
1432 * |
|
1433 * @return string |
|
1434 */ |
|
1435 function host() { |
|
1436 if ( defined('WP_PROXY_HOST') ) |
|
1437 return WP_PROXY_HOST; |
|
1438 |
|
1439 return ''; |
|
1440 } |
|
1441 |
|
1442 /** |
|
1443 * Retrieve the port for the proxy server. |
|
1444 * |
|
1445 * @since 2.8 |
|
1446 * |
|
1447 * @return string |
|
1448 */ |
|
1449 function port() { |
|
1450 if ( defined('WP_PROXY_PORT') ) |
|
1451 return WP_PROXY_PORT; |
|
1452 |
|
1453 return ''; |
|
1454 } |
|
1455 |
|
1456 /** |
|
1457 * Retrieve the username for proxy authentication. |
|
1458 * |
|
1459 * @since 2.8 |
|
1460 * |
|
1461 * @return string |
|
1462 */ |
|
1463 function username() { |
|
1464 if ( defined('WP_PROXY_USERNAME') ) |
|
1465 return WP_PROXY_USERNAME; |
|
1466 |
|
1467 return ''; |
|
1468 } |
|
1469 |
|
1470 /** |
|
1471 * Retrieve the password for proxy authentication. |
|
1472 * |
|
1473 * @since 2.8 |
|
1474 * |
|
1475 * @return string |
|
1476 */ |
|
1477 function password() { |
|
1478 if ( defined('WP_PROXY_PASSWORD') ) |
|
1479 return WP_PROXY_PASSWORD; |
|
1480 |
|
1481 return ''; |
|
1482 } |
|
1483 |
|
1484 /** |
|
1485 * Retrieve authentication string for proxy authentication. |
|
1486 * |
|
1487 * @since 2.8 |
|
1488 * |
|
1489 * @return string |
|
1490 */ |
|
1491 function authentication() { |
|
1492 return $this->username() . ':' . $this->password(); |
|
1493 } |
|
1494 |
|
1495 /** |
|
1496 * Retrieve header string for proxy authentication. |
|
1497 * |
|
1498 * @since 2.8 |
|
1499 * |
|
1500 * @return string |
|
1501 */ |
|
1502 function authentication_header() { |
|
1503 return 'Proxy-Authorization: Basic ' . base64_encode( $this->authentication() ); |
|
1504 } |
|
1505 |
|
1506 /** |
|
1507 * Whether URL should be sent through the proxy server. |
|
1508 * |
|
1509 * We want to keep localhost and the blog URL from being sent through the proxy server, because |
|
1510 * some proxies can not handle this. We also have the constant available for defining other |
|
1511 * hosts that won't be sent through the proxy. |
|
1512 * |
|
1513 * @uses WP_PROXY_BYPASS_HOSTS |
|
1514 * @since 2.8.0 |
|
1515 * |
|
1516 * @param string $uri URI to check. |
|
1517 * @return bool True, to send through the proxy and false if, the proxy should not be used. |
|
1518 */ |
|
1519 function send_through_proxy( $uri ) { |
|
1520 // parse_url() only handles http, https type URLs, and will emit E_WARNING on failure. |
|
1521 // This will be displayed on blogs, which is not reasonable. |
|
1522 $check = @parse_url($uri); |
|
1523 |
|
1524 // Malformed URL, can not process, but this could mean ssl, so let through anyway. |
|
1525 if ( $check === false ) |
|
1526 return true; |
|
1527 |
|
1528 $home = parse_url( get_option('siteurl') ); |
|
1529 |
|
1530 $result = apply_filters( 'pre_http_send_through_proxy', null, $uri, $check, $home ); |
|
1531 if ( ! is_null( $result ) ) |
|
1532 return $result; |
|
1533 |
|
1534 if ( $check['host'] == 'localhost' || $check['host'] == $home['host'] ) |
|
1535 return false; |
|
1536 |
|
1537 if ( !defined('WP_PROXY_BYPASS_HOSTS') ) |
|
1538 return true; |
|
1539 |
|
1540 static $bypass_hosts; |
|
1541 static $wildcard_regex = false; |
|
1542 if ( null == $bypass_hosts ) { |
|
1543 $bypass_hosts = preg_split('|,\s*|', WP_PROXY_BYPASS_HOSTS); |
|
1544 |
|
1545 if ( false !== strpos(WP_PROXY_BYPASS_HOSTS, '*') ) { |
|
1546 $wildcard_regex = array(); |
|
1547 foreach ( $bypass_hosts as $host ) |
|
1548 $wildcard_regex[] = str_replace( '\*', '.+', preg_quote( $host, '/' ) ); |
|
1549 $wildcard_regex = '/^(' . implode('|', $wildcard_regex) . ')$/i'; |
|
1550 } |
|
1551 } |
|
1552 |
|
1553 if ( !empty($wildcard_regex) ) |
|
1554 return !preg_match($wildcard_regex, $check['host']); |
|
1555 else |
|
1556 return !in_array( $check['host'], $bypass_hosts ); |
|
1557 } |
|
1558 } |
|
1559 /** |
|
1560 * Internal representation of a single cookie. |
|
1561 * |
|
1562 * Returned cookies are represented using this class, and when cookies are set, if they are not |
|
1563 * already a WP_Http_Cookie() object, then they are turned into one. |
|
1564 * |
|
1565 * @todo The WordPress convention is to use underscores instead of camelCase for function and method |
|
1566 * names. Need to switch to use underscores instead for the methods. |
|
1567 * |
|
1568 * @package WordPress |
|
1569 * @subpackage HTTP |
|
1570 * @since 2.8.0 |
|
1571 */ |
|
1572 class WP_Http_Cookie { |
|
1573 |
|
1574 /** |
|
1575 * Cookie name. |
|
1576 * |
|
1577 * @since 2.8.0 |
|
1578 * @var string |
|
1579 */ |
|
1580 var $name; |
|
1581 |
|
1582 /** |
|
1583 * Cookie value. |
|
1584 * |
|
1585 * @since 2.8.0 |
|
1586 * @var string |
|
1587 */ |
|
1588 var $value; |
|
1589 |
|
1590 /** |
|
1591 * When the cookie expires. |
|
1592 * |
|
1593 * @since 2.8.0 |
|
1594 * @var string |
|
1595 */ |
|
1596 var $expires; |
|
1597 |
|
1598 /** |
|
1599 * Cookie URL path. |
|
1600 * |
|
1601 * @since 2.8.0 |
|
1602 * @var string |
|
1603 */ |
|
1604 var $path; |
|
1605 |
|
1606 /** |
|
1607 * Cookie Domain. |
|
1608 * |
|
1609 * @since 2.8.0 |
|
1610 * @var string |
|
1611 */ |
|
1612 var $domain; |
|
1613 |
|
1614 /** |
|
1615 * Sets up this cookie object. |
|
1616 * |
|
1617 * The parameter $data should be either an associative array containing the indices names below |
|
1618 * or a header string detailing it. |
|
1619 * |
|
1620 * If it's an array, it should include the following elements: |
|
1621 * <ol> |
|
1622 * <li>Name</li> |
|
1623 * <li>Value - should NOT be urlencoded already.</li> |
|
1624 * <li>Expires - (optional) String or int (UNIX timestamp).</li> |
|
1625 * <li>Path (optional)</li> |
|
1626 * <li>Domain (optional)</li> |
|
1627 * <li>Port (optional)</li> |
|
1628 * </ol> |
|
1629 * |
|
1630 * @access public |
|
1631 * @since 2.8.0 |
|
1632 * |
|
1633 * @param string|array $data Raw cookie data. |
|
1634 * @param string $requested_url The URL which the cookie was set on, used for default 'domain' and 'port' values |
|
1635 */ |
|
1636 function __construct( $data, $requested_url = '' ) { |
|
1637 if ( $requested_url ) |
|
1638 $arrURL = @parse_url( $requested_url ); |
|
1639 if ( isset( $arrURL['host'] ) ) |
|
1640 $this->domain = $arrURL['host']; |
|
1641 $this->path = isset( $arrURL['path'] ) ? $arrURL['path'] : '/'; |
|
1642 if ( '/' != substr( $this->path, -1 ) ) |
|
1643 $this->path = dirname( $this->path ) . '/'; |
|
1644 |
|
1645 if ( is_string( $data ) ) { |
|
1646 // Assume it's a header string direct from a previous request |
|
1647 $pairs = explode( ';', $data ); |
|
1648 |
|
1649 // Special handling for first pair; name=value. Also be careful of "=" in value |
|
1650 $name = trim( substr( $pairs[0], 0, strpos( $pairs[0], '=' ) ) ); |
|
1651 $value = substr( $pairs[0], strpos( $pairs[0], '=' ) + 1 ); |
|
1652 $this->name = $name; |
|
1653 $this->value = urldecode( $value ); |
|
1654 array_shift( $pairs ); //Removes name=value from items. |
|
1655 |
|
1656 // Set everything else as a property |
|
1657 foreach ( $pairs as $pair ) { |
|
1658 $pair = rtrim($pair); |
|
1659 if ( empty($pair) ) //Handles the cookie ending in ; which results in a empty final pair |
|
1660 continue; |
|
1661 |
|
1662 list( $key, $val ) = strpos( $pair, '=' ) ? explode( '=', $pair ) : array( $pair, '' ); |
|
1663 $key = strtolower( trim( $key ) ); |
|
1664 if ( 'expires' == $key ) |
|
1665 $val = strtotime( $val ); |
|
1666 $this->$key = $val; |
|
1667 } |
|
1668 } else { |
|
1669 if ( !isset( $data['name'] ) ) |
|
1670 return false; |
|
1671 |
|
1672 // Set properties based directly on parameters |
|
1673 foreach ( array( 'name', 'value', 'path', 'domain', 'port' ) as $field ) { |
|
1674 if ( isset( $data[ $field ] ) ) |
|
1675 $this->$field = $data[ $field ]; |
|
1676 } |
|
1677 |
|
1678 if ( isset( $data['expires'] ) ) |
|
1679 $this->expires = is_int( $data['expires'] ) ? $data['expires'] : strtotime( $data['expires'] ); |
|
1680 else |
|
1681 $this->expires = null; |
|
1682 } |
|
1683 } |
|
1684 |
|
1685 /** |
|
1686 * Confirms that it's OK to send this cookie to the URL checked against. |
|
1687 * |
|
1688 * Decision is based on RFC 2109/2965, so look there for details on validity. |
|
1689 * |
|
1690 * @access public |
|
1691 * @since 2.8.0 |
|
1692 * |
|
1693 * @param string $url URL you intend to send this cookie to |
|
1694 * @return boolean true if allowed, false otherwise. |
|
1695 */ |
|
1696 function test( $url ) { |
|
1697 if ( is_null( $this->name ) ) |
|
1698 return false; |
|
1699 |
|
1700 // Expires - if expired then nothing else matters |
|
1701 if ( isset( $this->expires ) && time() > $this->expires ) |
|
1702 return false; |
|
1703 |
|
1704 // Get details on the URL we're thinking about sending to |
|
1705 $url = parse_url( $url ); |
|
1706 $url['port'] = isset( $url['port'] ) ? $url['port'] : ( 'https' == $url['scheme'] ? 443 : 80 ); |
|
1707 $url['path'] = isset( $url['path'] ) ? $url['path'] : '/'; |
|
1708 |
|
1709 // Values to use for comparison against the URL |
|
1710 $path = isset( $this->path ) ? $this->path : '/'; |
|
1711 $port = isset( $this->port ) ? $this->port : null; |
|
1712 $domain = isset( $this->domain ) ? strtolower( $this->domain ) : strtolower( $url['host'] ); |
|
1713 if ( false === stripos( $domain, '.' ) ) |
|
1714 $domain .= '.local'; |
|
1715 |
|
1716 // Host - very basic check that the request URL ends with the domain restriction (minus leading dot) |
|
1717 $domain = substr( $domain, 0, 1 ) == '.' ? substr( $domain, 1 ) : $domain; |
|
1718 if ( substr( $url['host'], -strlen( $domain ) ) != $domain ) |
|
1719 return false; |
|
1720 |
|
1721 // Port - supports "port-lists" in the format: "80,8000,8080" |
|
1722 if ( !empty( $port ) && !in_array( $url['port'], explode( ',', $port) ) ) |
|
1723 return false; |
|
1724 |
|
1725 // Path - request path must start with path restriction |
|
1726 if ( substr( $url['path'], 0, strlen( $path ) ) != $path ) |
|
1727 return false; |
|
1728 |
|
1729 return true; |
|
1730 } |
|
1731 |
|
1732 /** |
|
1733 * Convert cookie name and value back to header string. |
|
1734 * |
|
1735 * @access public |
|
1736 * @since 2.8.0 |
|
1737 * |
|
1738 * @return string Header encoded cookie name and value. |
|
1739 */ |
|
1740 function getHeaderValue() { |
|
1741 if ( ! isset( $this->name ) || ! isset( $this->value ) ) |
|
1742 return ''; |
|
1743 |
|
1744 return $this->name . '=' . apply_filters( 'wp_http_cookie_value', $this->value, $this->name ); |
|
1745 } |
|
1746 |
|
1747 /** |
|
1748 * Retrieve cookie header for usage in the rest of the WordPress HTTP API. |
|
1749 * |
|
1750 * @access public |
|
1751 * @since 2.8.0 |
|
1752 * |
|
1753 * @return string |
|
1754 */ |
|
1755 function getFullHeader() { |
|
1756 return 'Cookie: ' . $this->getHeaderValue(); |
|
1757 } |
|
1758 } |
|
1759 |
|
1760 /** |
|
1761 * Implementation for deflate and gzip transfer encodings. |
|
1762 * |
|
1763 * Includes RFC 1950, RFC 1951, and RFC 1952. |
|
1764 * |
|
1765 * @since 2.8 |
|
1766 * @package WordPress |
|
1767 * @subpackage HTTP |
|
1768 */ |
|
1769 class WP_Http_Encoding { |
|
1770 |
|
1771 /** |
|
1772 * Compress raw string using the deflate format. |
|
1773 * |
|
1774 * Supports the RFC 1951 standard. |
|
1775 * |
|
1776 * @since 2.8 |
|
1777 * |
|
1778 * @param string $raw String to compress. |
|
1779 * @param int $level Optional, default is 9. Compression level, 9 is highest. |
|
1780 * @param string $supports Optional, not used. When implemented it will choose the right compression based on what the server supports. |
|
1781 * @return string|bool False on failure. |
|
1782 */ |
|
1783 public static function compress( $raw, $level = 9, $supports = null ) { |
|
1784 return gzdeflate( $raw, $level ); |
|
1785 } |
|
1786 |
|
1787 /** |
|
1788 * Decompression of deflated string. |
|
1789 * |
|
1790 * Will attempt to decompress using the RFC 1950 standard, and if that fails |
|
1791 * then the RFC 1951 standard deflate will be attempted. Finally, the RFC |
|
1792 * 1952 standard gzip decode will be attempted. If all fail, then the |
|
1793 * original compressed string will be returned. |
|
1794 * |
|
1795 * @since 2.8 |
|
1796 * |
|
1797 * @param string $compressed String to decompress. |
|
1798 * @param int $length The optional length of the compressed data. |
|
1799 * @return string|bool False on failure. |
|
1800 */ |
|
1801 public static function decompress( $compressed, $length = null ) { |
|
1802 |
|
1803 if ( empty($compressed) ) |
|
1804 return $compressed; |
|
1805 |
|
1806 if ( false !== ( $decompressed = @gzinflate( $compressed ) ) ) |
|
1807 return $decompressed; |
|
1808 |
|
1809 if ( false !== ( $decompressed = WP_Http_Encoding::compatible_gzinflate( $compressed ) ) ) |
|
1810 return $decompressed; |
|
1811 |
|
1812 if ( false !== ( $decompressed = @gzuncompress( $compressed ) ) ) |
|
1813 return $decompressed; |
|
1814 |
|
1815 if ( function_exists('gzdecode') ) { |
|
1816 $decompressed = @gzdecode( $compressed ); |
|
1817 |
|
1818 if ( false !== $decompressed ) |
|
1819 return $decompressed; |
|
1820 } |
|
1821 |
|
1822 return $compressed; |
|
1823 } |
|
1824 |
|
1825 /** |
|
1826 * Decompression of deflated string while staying compatible with the majority of servers. |
|
1827 * |
|
1828 * Certain Servers will return deflated data with headers which PHP's gzinflate() |
|
1829 * function cannot handle out of the box. The following function has been created from |
|
1830 * various snippets on the gzinflate() PHP documentation. |
|
1831 * |
|
1832 * Warning: Magic numbers within. Due to the potential different formats that the compressed |
|
1833 * data may be returned in, some "magic offsets" are needed to ensure proper decompression |
|
1834 * takes place. For a simple progmatic way to determine the magic offset in use, see: |
|
1835 * http://core.trac.wordpress.org/ticket/18273 |
|
1836 * |
|
1837 * @since 2.8.1 |
|
1838 * @link http://core.trac.wordpress.org/ticket/18273 |
|
1839 * @link http://au2.php.net/manual/en/function.gzinflate.php#70875 |
|
1840 * @link http://au2.php.net/manual/en/function.gzinflate.php#77336 |
|
1841 * |
|
1842 * @param string $gzData String to decompress. |
|
1843 * @return string|bool False on failure. |
|
1844 */ |
|
1845 public static function compatible_gzinflate($gzData) { |
|
1846 |
|
1847 // Compressed data might contain a full header, if so strip it for gzinflate() |
|
1848 if ( substr($gzData, 0, 3) == "\x1f\x8b\x08" ) { |
|
1849 $i = 10; |
|
1850 $flg = ord( substr($gzData, 3, 1) ); |
|
1851 if ( $flg > 0 ) { |
|
1852 if ( $flg & 4 ) { |
|
1853 list($xlen) = unpack('v', substr($gzData, $i, 2) ); |
|
1854 $i = $i + 2 + $xlen; |
|
1855 } |
|
1856 if ( $flg & 8 ) |
|
1857 $i = strpos($gzData, "\0", $i) + 1; |
|
1858 if ( $flg & 16 ) |
|
1859 $i = strpos($gzData, "\0", $i) + 1; |
|
1860 if ( $flg & 2 ) |
|
1861 $i = $i + 2; |
|
1862 } |
|
1863 $decompressed = @gzinflate( substr($gzData, $i, -8) ); |
|
1864 if ( false !== $decompressed ) |
|
1865 return $decompressed; |
|
1866 } |
|
1867 |
|
1868 // Compressed data from java.util.zip.Deflater amongst others. |
|
1869 $decompressed = @gzinflate( substr($gzData, 2) ); |
|
1870 if ( false !== $decompressed ) |
|
1871 return $decompressed; |
|
1872 |
|
1873 return false; |
|
1874 } |
|
1875 |
|
1876 /** |
|
1877 * What encoding types to accept and their priority values. |
|
1878 * |
|
1879 * @since 2.8 |
|
1880 * |
|
1881 * @return string Types of encoding to accept. |
|
1882 */ |
|
1883 public static function accept_encoding( $url, $args ) { |
|
1884 $type = array(); |
|
1885 $compression_enabled = WP_Http_Encoding::is_available(); |
|
1886 |
|
1887 if ( ! $args['decompress'] ) // decompression specifically disabled |
|
1888 $compression_enabled = false; |
|
1889 elseif ( $args['stream'] ) // disable when streaming to file |
|
1890 $compression_enabled = false; |
|
1891 elseif ( isset( $args['limit_response_size'] ) ) // If only partial content is being requested, we won't be able to decompress it |
|
1892 $compression_enabled = false; |
|
1893 |
|
1894 if ( $compression_enabled ) { |
|
1895 if ( function_exists( 'gzinflate' ) ) |
|
1896 $type[] = 'deflate;q=1.0'; |
|
1897 |
|
1898 if ( function_exists( 'gzuncompress' ) ) |
|
1899 $type[] = 'compress;q=0.5'; |
|
1900 |
|
1901 if ( function_exists( 'gzdecode' ) ) |
|
1902 $type[] = 'gzip;q=0.5'; |
|
1903 } |
|
1904 |
|
1905 $type = apply_filters( 'wp_http_accept_encoding', $type, $url, $args ); |
|
1906 |
|
1907 return implode(', ', $type); |
|
1908 } |
|
1909 |
|
1910 /** |
|
1911 * What encoding the content used when it was compressed to send in the headers. |
|
1912 * |
|
1913 * @since 2.8 |
|
1914 * |
|
1915 * @return string Content-Encoding string to send in the header. |
|
1916 */ |
|
1917 public static function content_encoding() { |
|
1918 return 'deflate'; |
|
1919 } |
|
1920 |
|
1921 /** |
|
1922 * Whether the content be decoded based on the headers. |
|
1923 * |
|
1924 * @since 2.8 |
|
1925 * |
|
1926 * @param array|string $headers All of the available headers. |
|
1927 * @return bool |
|
1928 */ |
|
1929 public static function should_decode($headers) { |
|
1930 if ( is_array( $headers ) ) { |
|
1931 if ( array_key_exists('content-encoding', $headers) && ! empty( $headers['content-encoding'] ) ) |
|
1932 return true; |
|
1933 } else if ( is_string( $headers ) ) { |
|
1934 return ( stripos($headers, 'content-encoding:') !== false ); |
|
1935 } |
|
1936 |
|
1937 return false; |
|
1938 } |
|
1939 |
|
1940 /** |
|
1941 * Whether decompression and compression are supported by the PHP version. |
|
1942 * |
|
1943 * Each function is tested instead of checking for the zlib extension, to |
|
1944 * ensure that the functions all exist in the PHP version and aren't |
|
1945 * disabled. |
|
1946 * |
|
1947 * @since 2.8 |
|
1948 * |
|
1949 * @return bool |
|
1950 */ |
|
1951 public static function is_available() { |
|
1952 return ( function_exists('gzuncompress') || function_exists('gzdeflate') || function_exists('gzinflate') ); |
|
1953 } |
|
1954 } |