Index: src/wp-includes/class-http.php
===================================================================
--- src/wp-includes/class-http.php	(revision 33336)
+++ src/wp-includes/class-http.php	(working copy)
@@ -83,215 +83,92 @@
 	 *                        A WP_Error instance upon error.
 	 */
 	public function request( $url, $args = array() ) {
-		global $wp_version;
+		if ( is_array( $url ) ) {
+			return $this->request_multi( $url );
+		}
 
-		$defaults = array(
-			'method' => 'GET',
-			/**
-			 * Filter the timeout value for an HTTP request.
-			 *
-			 * @since 2.7.0
-			 *
-			 * @param int $timeout_value Time in seconds until a request times out.
-			 *                           Default 5.
-			 */
-			'timeout' => apply_filters( 'http_request_timeout', 5 ),
-			/**
-			 * Filter the number of redirects allowed during an HTTP request.
-			 *
-			 * @since 2.7.0
-			 *
-			 * @param int $redirect_count Number of redirects allowed. Default 5.
-			 */
-			'redirection' => apply_filters( 'http_request_redirection_count', 5 ),
-			/**
-			 * Filter the version of the HTTP protocol used in a request.
-			 *
-			 * @since 2.7.0
-			 *
-			 * @param string $version Version of HTTP used. Accepts '1.0' and '1.1'.
-			 *                        Default '1.0'.
-			 */
-			'httpversion' => apply_filters( 'http_request_version', '1.0' ),
-			/**
-			 * Filter the user agent value sent with an HTTP request.
-			 *
-			 * @since 2.7.0
-			 *
-			 * @param string $user_agent WordPress user agent string.
-			 */
-			'user-agent' => apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' ) ),
-			/**
-			 * Filter whether to pass URLs through wp_http_validate_url() in an HTTP request.
-			 *
-			 * @since 3.6.0
-			 *
-			 * @param bool $pass_url Whether to pass URLs through wp_http_validate_url().
-			 *                       Default false.
-			 */
-			'reject_unsafe_urls' => apply_filters( 'http_request_reject_unsafe_urls', false ),
-			'blocking' => true,
-			'headers' => array(),
-			'cookies' => array(),
-			'body' => null,
-			'compress' => false,
-			'decompress' => true,
-			'sslverify' => true,
-			'sslcertificates' => ABSPATH . WPINC . '/certificates/ca-bundle.crt',
-			'stream' => false,
-			'filename' => null,
-			'limit_response_size' => null,
-		);
+		$request = new WP_Http_Request( $url, $args  );
+		if ( false !== $request->pre ) {
+			return $request->pre;
+		}
 
-		// Pre-parse for the HEAD checks.
-		$args = wp_parse_args( $args );
+		$response = $this->_dispatch_request( $request->url, $request->args );
 
-		// By default, Head requests do not cause redirections.
-		if ( isset($args['method']) && 'HEAD' == $args['method'] )
-			$defaults['redirection'] = 0;
+		reset_mbstring_encoding();
 
-		$r = wp_parse_args( $args, $defaults );
-		/**
-		 * Filter the arguments used in an HTTP request.
-		 *
-		 * @since 2.7.0
-		 *
-		 * @param array  $r   An array of HTTP request arguments.
-		 * @param string $url The request URL.
-		 */
-		$r = apply_filters( 'http_request_args', $r, $url );
-
-		// The transports decrement this, store a copy of the original value for loop purposes.
-		if ( ! isset( $r['_redirection'] ) )
-			$r['_redirection'] = $r['redirection'];
-
-		/**
-		 * Filter whether to preempt an HTTP request's return.
-		 *
-		 * Returning a truthy value to the filter will short-circuit
-		 * the HTTP request and return early with that value.
-		 *
-		 * @since 2.9.0
-		 *
-		 * @param bool   $preempt Whether to preempt an HTTP request return. Default false.
-		 * @param array  $r       HTTP request arguments.
-		 * @param string $url     The request URL.
-		 */
-		$pre = apply_filters( 'pre_http_request', false, $r, $url );
-		if ( false !== $pre )
-			return $pre;
-
-		if ( function_exists( 'wp_kses_bad_protocol' ) ) {
-			if ( $r['reject_unsafe_urls'] )
-				$url = wp_http_validate_url( $url );
-			if ( $url ) {
-				$url = wp_kses_bad_protocol( $url, array( 'http', 'https', 'ssl' ) );
-			}
+		if ( is_wp_error( $response ) ) {
+			return $response;
 		}
 
-		$arrURL = @parse_url( $url );
+		$this->add_cookies( $response, $request );
 
-		if ( empty( $url ) || empty( $arrURL['scheme'] ) )
-			return new WP_Error('http_request_failed', __('A valid URL was not provided.'));
+		return $response;
+	}
 
-		if ( $this->block_request( $url ) )
-			return new WP_Error( 'http_request_failed', __( 'User has blocked requests through HTTP.' ) );
+	public static function hash_request( $url, $args ) {
+		return md5( $url . serialize( $args ) );
+	}
 
-		/*
-		 * Determine if this is a https call and pass that on to the transport functions
-		 * so that we can blacklist the transports that do not support ssl verification
-		 */
-		$r['ssl'] = $arrURL['scheme'] == 'https' || $arrURL['scheme'] == 'ssl';
-
-		// Determine if this request is to OUR install of WordPress.
-		$homeURL = parse_url( get_bloginfo( 'url' ) );
-		$r['local'] = 'localhost' == $arrURL['host'] || ( isset( $homeURL['host'] ) && $homeURL['host'] == $arrURL['host'] );
-		unset( $homeURL );
-
-		/*
-		 * If we are streaming to a file but no filename was given drop it in the WP temp dir
-		 * and pick its name using the basename of the $url.
-		 */
-		if ( $r['stream']  && empty( $r['filename'] ) ) {
-			$r['filename'] = get_temp_dir() . wp_unique_filename( get_temp_dir(), basename( $url ) );
+	private function add_cookies( &$response, $request ) {
+		// Append cookies that were used in this request to the response
+		if ( empty( $request->args['cookies'] ) ) {
+			return;
 		}
 
-		/*
-		 * Force some settings if we are streaming to a file and check for existence and perms
-		 * of destination directory.
-		 */
-		if ( $r['stream'] ) {
-			$r['blocking'] = true;
-			if ( ! wp_is_writable( dirname( $r['filename'] ) ) )
-				return new WP_Error( 'http_request_failed', __( 'Destination directory for file streaming does not exist or is not writable.' ) );
+		$cookies_set = wp_list_pluck( $response['cookies'], 'name' );
+		foreach ( $request->args['cookies'] as $cookie ) {
+			if ( ! in_array( $cookie->name, $cookies_set ) && $cookie->test( $request->url ) ) {
+				$response['cookies'][] = $cookie;
+			}
 		}
+	}
 
-		if ( is_null( $r['headers'] ) )
-			$r['headers'] = array();
+	public function request_multi( $tuples ) {
+		$requests = array();
+		$responses = array();
 
-		if ( ! is_array( $r['headers'] ) ) {
-			$processedHeaders = self::processHeaders( $r['headers'], $url );
-			$r['headers'] = $processedHeaders['headers'];
+		foreach ( $tuples as $tuple ) {
+			@list( $url, $args, $callback ) = $tuple;
+			$request = new WP_Http_Request( $url, $args );
+			$hash = self::hash_request( $url, $args );
+			if ( false !== $request->pre ) {
+				$responses[ $hash ] = $request->pre;
+			} else {
+				$requests[ $hash ] = $request;
+			}
 		}
 
-		if ( isset( $r['headers']['User-Agent'] ) ) {
-			$r['user-agent'] = $r['headers']['User-Agent'];
-			unset( $r['headers']['User-Agent'] );
+		if ( ! $requests ) {
+			reset_mbstring_encoding();
+			return $responses;
 		}
 
-		if ( isset( $r['headers']['user-agent'] ) ) {
-			$r['user-agent'] = $r['headers']['user-agent'];
-			unset( $r['headers']['user-agent'] );
-		}
+		if ( 1 === count( $requests ) ) {
+			$responses[ $hash ] = $this->_dispatch_request( $request->url, $request->args );
+			reset_mbstring_encoding();
 
-		if ( '1.1' == $r['httpversion'] && !isset( $r['headers']['connection'] ) ) {
-			$r['headers']['connection'] = 'close';
-		}
+			if ( is_wp_error( $responses[ $hash ] ) ) {
+				return $responses[ $hash ];
+			}
 
-		// Construct Cookie: header if any cookies are set.
-		self::buildCookieHeader( $r );
-
-		// Avoid issues where mbstring.func_overload is enabled.
-		mbstring_binary_safe_encoding();
-
-		if ( ! isset( $r['headers']['Accept-Encoding'] ) ) {
-			if ( $encoding = WP_Http_Encoding::accept_encoding( $url, $r ) )
-				$r['headers']['Accept-Encoding'] = $encoding;
+			$this->add_cookies( $responses[ $hash ], $request );
+			return $responses[ $hash ];
 		}
 
-		if ( ( ! is_null( $r['body'] ) && '' != $r['body'] ) || 'POST' == $r['method'] || 'PUT' == $r['method'] ) {
-			if ( is_array( $r['body'] ) || is_object( $r['body'] ) ) {
-				$r['body'] = http_build_query( $r['body'], null, '&' );
+		$multi = $this->_dispatch_request( $requests );
 
-				if ( ! isset( $r['headers']['Content-Type'] ) )
-					$r['headers']['Content-Type'] = 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' );
+		foreach ( $requests as $hash => $request ) {
+			$responses[ $hash ] = $multi[ $hash ];
+			if ( is_wp_error( $responses[ $hash ] ) ) {
+				continue;
 			}
 
-			if ( '' === $r['body'] )
-				$r['body'] = null;
-
-			if ( ! isset( $r['headers']['Content-Length'] ) && ! isset( $r['headers']['content-length'] ) )
-				$r['headers']['Content-Length'] = strlen( $r['body'] );
+			$this->add_cookies( $responses[ $hash ], $request );
 		}
 
-		$response = $this->_dispatch_request( $url, $r );
-
 		reset_mbstring_encoding();
 
-		if ( is_wp_error( $response ) )
-			return $response;
-
-		// Append cookies that were used in this request to the response
-		if ( ! empty( $r['cookies'] ) ) {
-			$cookies_set = wp_list_pluck( $response['cookies'], 'name' );
-			foreach ( $r['cookies'] as $cookie ) {
-				if ( ! in_array( $cookie->name, $cookies_set ) && $cookie->test( $url ) ) {
-					$response['cookies'][] = $cookie;
-				}
-			}
-		}
-
-		return $response;
+		return $responses;
 	}
 
 	/**
@@ -332,36 +209,34 @@
 		return false;
 	}
 
-	/**
-	 * Dispatches a HTTP request to a supporting transport.
-	 *
-	 * Tests each transport in order to find a transport which matches the request arguments.
-	 * Also caches the transport instance to be used later.
-	 *
-	 * The order for requests is cURL, and then PHP Streams.
-	 *
-	 * @since 3.2.0
-	 *
-	 * @static
-	 * @access private
-	 *
-	 * @param string $url URL to Request
-	 * @param array $args Request arguments
-	 * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error
-	 */
-	private function _dispatch_request( $url, $args ) {
-		static $transports = array();
+	public function _get_first_available_multi_transport( $args, $url = null ) {
+		/**
+		 * Filter which HTTP transports are available and in what order.
+		 *
+		 * @since 4.4.0
+		 *
+		 * @param array  $value Array of HTTP transports to check. Default array contains
+		 *                      'curl', and 'streams', in that order.
+		 * @param array  $args  HTTP request arguments.
+		 * @param string $url   The URL to request.
+		 */
+		$request_order = apply_filters( 'http_api_multi_transports', array( 'Curl' ), $args, $url );
 
-		$class = $this->_get_first_available_transport( $args, $url );
-		if ( !$class )
-			return new WP_Error( 'http_failure', __( 'There are no HTTP transports available which can complete the requested request.' ) );
+		// Loop over each transport on each HTTP request looking for one which will serve this request's needs.
+		foreach ( $request_order as $transport ) {
+			$class = 'WP_HTTP_' . $transport;
 
-		// Transport claims to support request, instantiate it and give it a whirl.
-		if ( empty( $transports[$class] ) )
-			$transports[$class] = new $class;
+			// Check to see if this transport is a possibility, calls the transport statically.
+			if ( !call_user_func( array( $class, 'test' ), $args, $url ) )
+				continue;
 
-		$response = $transports[$class]->request( $url, $args );
+			return $class;
+		}
 
+		return false;
+	}
+
+	private function _filter_response( $response, $class, $args, $url ) {
 		/**
 		 * Fires after an HTTP API response is received and before the response is returned.
 		 *
@@ -375,8 +250,9 @@
 		 */
 		do_action( 'http_api_debug', $response, 'response', $class, $args, $url );
 
-		if ( is_wp_error( $response ) )
+		if ( is_wp_error( $response ) ) {
 			return $response;
+		}
 
 		/**
 		 * Filter the HTTP API response immediately before the response is returned.
@@ -391,6 +267,54 @@
 	}
 
 	/**
+	 * Dispatches a HTTP request to a supporting transport.
+	 *
+	 * Tests each transport in order to find a transport which matches the request arguments.
+	 * Also caches the transport instance to be used later.
+	 *
+	 * The order for requests is cURL, and then PHP Streams.
+	 *
+	 * @since 3.2.0
+	 *
+	 * @static
+	 * @access private
+	 *
+	 * @param string $url URL to Request
+	 * @param array $args Request arguments
+	 * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error
+	 */
+	private function _dispatch_request( $url, $args = array() ) {
+		static $transports = array();
+		$requests = false;
+
+		if ( is_array( $url ) ) {
+			$requests = $url;
+			$class = $this->_get_first_available_multi_transport( $requests );
+		} else {
+			$class = $this->_get_first_available_transport( $args, $url );
+		}
+
+		if ( ! $class ) {
+			return new WP_Error( 'http_failure', __( 'There are no HTTP transports available which can complete the requested request.' ) );
+		}
+
+		// Transport claims to support request, instantiate it and give it a whirl.
+		if ( empty( $transports[$class] ) )
+			$transports[$class] = new $class;
+
+		if ( $requests ) {
+			$multi = $transports[$class]->request_multi( $requests );
+			foreach ( $requests as $hash => $request ) {
+				$multi[ $hash ] = $this->_filter_response( $multi[ $hash ], $class, $request->args, $request->url );
+			}
+			return $multi;
+		}
+
+		$response = $transports[$class]->request( $url, $args );
+		return $this->_filter_response( $response, $class, $args, $url );
+	}
+
+	/**
 	 * Uses the POST HTTP method.
 	 *
 	 * Used for sending data that is expected to be in the body.
@@ -1316,6 +1240,7 @@
 	 * @var string
 	 */
 	private $headers = '';
+	private $multi_headers = array();
 
 	/**
 	 * Temporary body storage for during requests.
@@ -1325,6 +1250,7 @@
 	 * @var string
 	 */
 	private $body = '';
+	private $multi_body = array();
 
 	/**
 	 * The maximum amount of data to receive from the remote server.
@@ -1334,6 +1260,7 @@
 	 * @var int
 	 */
 	private $max_body_length = false;
+	private $multi_max_body_length = array();
 
 	/**
 	 * The file resource used for streaming to file.
@@ -1343,7 +1270,11 @@
 	 * @var resource
 	 */
 	private $stream_handle = false;
+	private $multi_stream_handle = array();
 
+	private $multi_handles = array();
+	private $multi_opts = array();
+
 	/**
 	 * The total bytes written in the current request.
 	 *
@@ -1352,22 +1283,38 @@
 	 * @var int
 	 */
 	private $bytes_written_total = 0;
+	private $multi_bytes_written_total = array();
 
-	/**
-	 * Send a HTTP request to a URI using cURL extension.
-	 *
-	 * @access public
-	 * @since 2.7.0
-	 *
-	 * @param string $url The request URL.
-	 * @param string|array $args Optional. Override the defaults.
-	 * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error
-	 */
-	public function request($url, $args = array()) {
+	public function __call( $name, $arguments ) {
+		$header_prefix = '_stream_headers_for_';
+		if ( 0 === strpos( $name, $header_prefix ) ) {
+			$hash = str_replace( $header_prefix, '', $name );
+			if ( ! isset( $this->multi_headers[ $hash ] ) ) {
+				$this->multi_headers[ $hash ] = '';
+			}
+			list( $handler, $headers ) = $arguments;
+			$this->multi_headers[ $hash ] .= $headers;
+			return strlen( $headers );
+		}
+
+		$body_prefix = '_stream_body_for_';
+		if ( 0 === strpos( $name, $body_prefix ) ) {
+			$hash = str_replace( $body_prefix, '', $name );
+			if ( ! isset( $this->multi_body[ $hash ] ) ) {
+				$this->multi_body[ $hash ] = '';
+			}
+			list( $handler, $body ) = $arguments;
+			$this->multi_stream_body( $hash, $body );
+			return strlen( $body );
+		}
+	}
+
+	private function get_handle_with_opts( $url, $args ) {
 		$defaults = array(
 			'method' => 'GET', 'timeout' => 5,
 			'redirection' => 5, 'httpversion' => '1.0',
 			'blocking' => true,
+			'multi' => false,
 			'headers' => array(), 'body' => null, 'cookies' => array()
 		);
 
@@ -1431,8 +1378,10 @@
 		 * a bug #17490 with redirected POST requests, so handle redirections outside Curl.
 		 */
 		curl_setopt( $handle, CURLOPT_FOLLOWLOCATION, false );
-		if ( defined( 'CURLOPT_PROTOCOLS' ) ) // PHP 5.2.10 / cURL 7.19.4
+		// PHP 5.2.10 / cURL 7.19.4
+		if ( defined( 'CURLOPT_PROTOCOLS' ) ) {
 			curl_setopt( $handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS );
+		}
 
 		switch ( $r['method'] ) {
 			case 'HEAD':
@@ -1448,12 +1397,16 @@
 				break;
 			default:
 				curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, $r['method'] );
-				if ( ! is_null( $r['body'] ) )
+				if ( ! is_null( $r['body'] ) ) {
 					curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] );
+				}
 				break;
 		}
 
-		if ( true === $r['blocking'] ) {
+		if ( $r['multi'] ) {
+			curl_setopt( $handle, CURLOPT_HEADERFUNCTION, array( $this, '_stream_headers_for_' . $r['multi'] ) );
+			curl_setopt( $handle, CURLOPT_WRITEFUNCTION, array( $this, '_stream_body_for_' . $r['multi'] ) );
+		} elseif ( true === $r['blocking'] ) {
 			curl_setopt( $handle, CURLOPT_HEADERFUNCTION, array( $this, 'stream_headers' ) );
 			curl_setopt( $handle, CURLOPT_WRITEFUNCTION, array( $this, 'stream_body' ) );
 		}
@@ -1460,37 +1413,94 @@
 
 		curl_setopt( $handle, CURLOPT_HEADER, false );
 
-		if ( isset( $r['limit_response_size'] ) )
+		if ( !empty( $r['headers'] ) ) {
+			// cURL expects full header strings in each element.
+			$headers = array();
+			foreach ( $r['headers'] as $name => $value ) {
+				$headers[] = "{$name}: $value";
+			}
+			curl_setopt( $handle, CURLOPT_HTTPHEADER, $headers );
+		}
+
+		if ( $r['httpversion'] == '1.0' ) {
+			curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0 );
+		} else {
+			curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 );
+		}
+
+		if ( $r['multi'] ) {
+			curl_setopt( $handle, CURLOPT_ENCODING, 'identity' );
+
+			$this->multi_handles[ $r['multi'] ] =& $handle;
+			$this->multi_opts[ $r['multi'] ] =& $r;
+		}
+
+		return array( $handle, $r );
+	}
+
+	private function set_stream_options( $r ) {
+		if ( isset( $r['limit_response_size'] ) ) {
 			$this->max_body_length = intval( $r['limit_response_size'] );
-		else
+		} else {
 			$this->max_body_length = false;
+		}
 
 		// If streaming to a file open a file handle, and setup our curl streaming handler.
 		if ( $r['stream'] ) {
-			if ( ! WP_DEBUG )
+			if ( ! WP_DEBUG ) {
 				$this->stream_handle = @fopen( $r['filename'], 'w+' );
-			else
+			} else {
 				$this->stream_handle = fopen( $r['filename'], 'w+' );
-			if ( ! $this->stream_handle )
+			}
+
+			if ( ! $this->stream_handle ) {
 				return new WP_Error( 'http_request_failed', sprintf( __( 'Could not open handle for fopen() to %s' ), $r['filename'] ) );
-		} else {
-			$this->stream_handle = false;
+			}
 		}
 
-		if ( !empty( $r['headers'] ) ) {
-			// cURL expects full header strings in each element.
-			$headers = array();
-			foreach ( $r['headers'] as $name => $value ) {
-				$headers[] = "{$name}: $value";
+		$this->stream_handle = false;
+	}
+
+	private function set_multi_stream_options( $r ) {
+		$hash = $r['multi'];
+
+		if ( isset( $r['limit_response_size'] ) ) {
+			$this->multi_max_body_length[ $hash ] = intval( $r['limit_response_size'] );
+		}
+
+		// If streaming to a file open a file handle, and setup our curl streaming handler.
+		if ( $r['stream'] ) {
+			if ( ! WP_DEBUG ) {
+				$this->multi_stream_handle[ $hash ] = @fopen( $r['filename'], 'w+' );
+			} else {
+				$this->multi_stream_handle[ $hash ] = fopen( $r['filename'], 'w+' );
 			}
-			curl_setopt( $handle, CURLOPT_HTTPHEADER, $headers );
+
+			if ( ! $this->multi_stream_handle[ $hash ] ) {
+				return new WP_Error( 'http_request_failed', sprintf( __( 'Could not open handle for fopen() to %s' ), $r['filename'] ) );
+			}
 		}
 
-		if ( $r['httpversion'] == '1.0' )
-			curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0 );
-		else
-			curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 );
+		$this->multi_stream_handle[ $hash ] = false;
+	}
 
+	/**
+	 * Send a HTTP request to a URI using cURL extension.
+	 *
+	 * @access public
+	 * @since 2.7.0
+	 *
+	 * @param string $url The request URL.
+	 * @param string|array $args Optional. Override the defaults.
+	 * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error
+	 */
+	public function request($url, $args = array()) {
+		list( $handle, $r ) = $this->get_handle_with_opts( $url, $args );
+		$stream_options = $this->set_stream_options( $r );
+		if ( is_wp_error( $stream_options ) ) {
+			return $stream_options;
+		}
+
 		/**
 		 * Fires before the cURL request is executed.
 		 *
@@ -1583,6 +1593,112 @@
 		return $response;
 	}
 
+	public function request_multi( $requests ) {
+		$mh = curl_multi_init();
+
+		foreach ( $requests as $hash => $request ) {
+			$request->args['multi'] = $hash;
+			$this->get_handle_with_opts( $request->url, $request->args );
+			$stream_options = $this->set_multi_stream_options( $this->multi_opts[ $hash ] );
+			if ( is_wp_error( $stream_options ) ) {
+				$responses[ $hash ] = $stream_options;
+				continue;
+			}
+
+			/** This filter is documented in wp-includes/wp-http.php */
+			do_action_ref_array( 'http_api_curl', array(
+				&$this->multi_handles[ $hash ],
+				 $this->multi_opts[ $hash ],
+				$request->url
+			) );
+
+			curl_multi_add_handle( $mh, $this->multi_handles[ $hash ] );
+		}
+
+		$active = null;
+
+		do {
+			$mrc = curl_multi_exec($mh, $active);
+		} while ($mrc == CURLM_CALL_MULTI_PERFORM || $active);
+
+		$responses = array();
+		foreach ( $this->multi_handles as $hash => &$handle ) {
+			$url = $requests[ $hash ]->url;
+			$r = $this->multi_opts[ $hash ];
+			$headers = WP_Http::processHeaders( $this->multi_headers[ $hash ], $url );
+			$body = $this->multi_body[ $hash ];
+			$bytes_written_total = $this->multi_bytes_written_total[ $hash ];
+
+			$this->multi_headers[ $hash ] = '';
+			$this->multi_body[ $hash ] = '';
+			$this->multi_bytes_written_total[ $hash ] = 0;
+
+			$curl_error = curl_errno( $handle );
+
+			// If an error occurred, or, no response.
+			if ( $curl_error || ( 0 === strlen( $body ) && empty( $headers['headers'] ) ) ) {
+				if ( CURLE_WRITE_ERROR /* 23 */ == $curl_error ) {
+					if ( ! $this->multi_max_body_length[ $hash ]
+						|| $this->multi_max_body_length[ $hash ] != $bytes_written_total ) {
+						if ( $r['stream'] ) {
+							curl_multi_remove_handle( $mh, $handle );
+							fclose( $this->stream_handle );
+							$responses[ $hash ] = new WP_Error( 'http_request_failed', __( 'Failed to write request to temporary file.' ) );
+						} else {
+							curl_multi_remove_handle( $mh, $handle );
+							$responses[ $hash ] = new WP_Error( 'http_request_failed', curl_error( $handle ) );
+						}
+						continue;
+					}
+				} else {
+					$curl_error = curl_error( $handle );
+					if ( $curl_error ) {
+						curl_multi_remove_handle( $mh, $handle );
+						$responses[ $hash ] = new WP_Error( 'http_request_failed', $curl_error );
+						continue;
+					}
+				}
+
+				if ( in_array( curl_getinfo( $handle, CURLINFO_HTTP_CODE ), array( 301, 302 ) ) ) {
+					curl_multi_remove_handle( $mh, $handle );
+					$responses[ $hash ] = new WP_Error( 'http_request_failed', __( 'Too many redirects.' ) );
+					continue;
+				}
+			}
+
+			curl_multi_remove_handle( $mh, $handle );
+
+			if ( $r['stream'] ) {
+				fclose( $this->multi_stream_handle[ $hash ] );
+			}
+
+			$response = array(
+				'headers' => $headers['headers'],
+				'body' => null,
+				'response' => $headers['response'],
+				'cookies' => $headers['cookies'],
+				'filename' => $r['filename']
+			);
+
+			// Handle redirects.
+			if ( false !== ( $redirect_response = WP_HTTP::handle_redirects( $url, $r, $response ) ) ) {
+				$responses[ $hash ] = $redirect_response;
+				continue;
+			}
+
+			if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode( $headers['headers'] ) ) {
+				$body = WP_Http_Encoding::decompress( $body );
+			}
+
+			$response['body'] = $body;
+			$responses[ $hash ] = $response;
+		}
+
+		curl_multi_close( $mh );
+
+		return $responses;
+	}
+
 	/**
 	 * Grab the headers of the cURL request
 	 *
@@ -1628,6 +1744,35 @@
 		return $bytes_written;
 	}
 
+	private function multi_stream_body( $hash, $data ) {
+		$data_length = strlen( $data );
+
+		if ( ! isset( $this->multi_bytes_written_total[ $hash ] ) ) {
+			$this->multi_bytes_written_total[ $hash ] = 0;
+		}
+		$total = $this->multi_bytes_written_total[ $hash ];
+		if ( ! isset( $this->multi_max_body_length[ $hash ] ) ) {
+			$this->multi_max_body_length[ $hash ] = 0;
+		}
+		$max = $this->multi_max_body_length[ $hash ];
+		if ( $max && ( $total + $data_length ) > $max ) {
+			$data_length = ( $max - $total );
+			$data = substr( $data, 0, $data_length );
+		}
+
+		if ( isset( $this->multi_stream_handle[ $hash ] ) && $this->multi_stream_handle[ $hash ] ) {
+			$bytes_written = fwrite( $this->multi_stream_handle[ $hash ], $data );
+		} else {
+			$this->multi_body[ $hash ] .= $data;
+			$bytes_written = $data_length;
+		}
+
+		$this->multi_bytes_written_total[ $hash ] += $bytes_written;
+
+		// Upon event of this function returning less than strlen( $data ) curl will error with CURLE_WRITE_ERROR.
+		return $bytes_written;
+	}
+
 	/**
 	 * Whether this class can be used for retrieving an URL.
 	 *
@@ -2304,3 +2449,266 @@
 		return ( function_exists('gzuncompress') || function_exists('gzdeflate') || function_exists('gzinflate') );
 	}
 }
+
+class WP_Http_Request {
+	public $url;
+	public $args;
+	public $defaults;
+
+	public $pre = false;
+
+	/**
+	 *
+	 * @global string $wp_version
+	 *
+	 * @param string       $url  The request URL.
+	 * @param string|array $args {
+	 *     Optional. Array or string of HTTP request arguments.
+	 *
+	 *     @type string       $method              Request method. Accepts 'GET', 'POST', 'HEAD', or 'PUT'.
+	 *                                             Some transports technically allow others, but should not be
+	 *                                             assumed. Default 'GET'.
+	 *     @type int          $timeout             How long the connection should stay open in seconds. Default 5.
+	 *     @type int          $redirection         Number of allowed redirects. Not supported by all transports
+	 *                                             Default 5.
+	 *     @type string       $httpversion         Version of the HTTP protocol to use. Accepts '1.0' and '1.1'.
+	 *                                             Default '1.0'.
+	 *     @type string       $user-agent          User-agent value sent.
+	 *                                             Default WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' ).
+	 *     @type bool         $reject_unsafe_urls  Whether to pass URLs through {@see wp_http_validate_url()}.
+	 *                                             Default false.
+	 *     @type bool         $blocking            Whether the calling code requires the result of the request.
+	 *                                             If set to false, the request will be sent to the remote server,
+	 *                                             and processing returned to the calling code immediately, the caller
+	 *                                             will know if the request succeeded or failed, but will not receive
+	 *                                             any response from the remote server. Default true.
+	 *     @type string|array $headers             Array or string of headers to send with the request.
+	 *                                             Default empty array.
+	 *     @type array        $cookies             List of cookies to send with the request. Default empty array.
+	 *     @type string|array $body                Body to send with the request. Default null.
+	 *     @type bool         $compress            Whether to compress the $body when sending the request.
+	 *                                             Default false.
+	 *     @type bool         $decompress          Whether to decompress a compressed response. If set to false and
+	 *                                             compressed content is returned in the response anyway, it will
+	 *                                             need to be separately decompressed. Default true.
+	 *     @type bool         $sslverify           Whether to verify SSL for the request. Default true.
+	 *     @type string       sslcertificates      Absolute path to an SSL certificate .crt file.
+	 *                                             Default ABSPATH . WPINC . '/certificates/ca-bundle.crt'.
+	 *     @type bool         $stream              Whether to stream to a file. If set to true and no filename was
+	 *                                             given, it will be droped it in the WP temp dir and its name will
+	 *                                             be set using the basename of the URL. Default false.
+	 *     @type string       $filename            Filename of the file to write to when streaming. $stream must be
+	 *                                             set to true. Default null.
+	 *     @type int          $limit_response_size Size in bytes to limit the response to. Default null.
+	 *
+	 * }
+	 */
+	public function __construct( $url, $args = array() ) {
+		global $wp_version;
+
+		$this->url = $url;
+
+		$this->defaults = array(
+			'method' => 'GET',
+			/**
+			 * Filter the timeout value for an HTTP request.
+			 *
+			 * @since 2.7.0
+			 *
+			 * @param int $timeout_value Time in seconds until a request times out.
+			 *                           Default 5.
+			 */
+			'timeout' => apply_filters( 'http_request_timeout', 5 ),
+			/**
+			 * Filter the number of redirects allowed during an HTTP request.
+			 *
+			 * @since 2.7.0
+			 *
+			 * @param int $redirect_count Number of redirects allowed. Default 5.
+			 */
+			'redirection' => apply_filters( 'http_request_redirection_count', 5 ),
+			/**
+			 * Filter the version of the HTTP protocol used in a request.
+			 *
+			 * @since 2.7.0
+			 *
+			 * @param string $version Version of HTTP used. Accepts '1.0' and '1.1'.
+			 *                        Default '1.0'.
+			 */
+			'httpversion' => apply_filters( 'http_request_version', '1.0' ),
+			/**
+			 * Filter the user agent value sent with an HTTP request.
+			 *
+			 * @since 2.7.0
+			 *
+			 * @param string $user_agent WordPress user agent string.
+			 */
+			'user-agent' => apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' ) ),
+			/**
+			 * Filter whether to pass URLs through wp_http_validate_url() in an HTTP request.
+			 *
+			 * @since 3.6.0
+			 *
+			 * @param bool $pass_url Whether to pass URLs through wp_http_validate_url().
+			 *                       Default false.
+			 */
+			'reject_unsafe_urls' => apply_filters( 'http_request_reject_unsafe_urls', false ),
+			'blocking' => true,
+			'headers' => array(),
+			'cookies' => array(),
+			'body' => null,
+			'compress' => false,
+			'decompress' => true,
+			'sslverify' => true,
+			'sslcertificates' => ABSPATH . WPINC . '/certificates/ca-bundle.crt',
+			'stream' => false,
+			'filename' => null,
+			'limit_response_size' => null,
+		);
+
+		// Pre-parse for the HEAD checks.
+		$args = wp_parse_args( $args );
+
+		// By default, Head requests do not cause redirections.
+		if ( isset($args['method']) && 'HEAD' == $args['method'] ) {
+			$defaults['redirection'] = 0;
+		}
+
+		$r = wp_parse_args( $args, $this->defaults );
+		/**
+		 * Filter the arguments used in an HTTP request.
+		 *
+		 * @since 2.7.0
+		 *
+		 * @param array  $r   An array of HTTP request arguments.
+		 * @param string $url The request URL.
+		 */
+		$this->args = apply_filters( 'http_request_args', $r, $url );
+
+		// The transports decrement this, store a copy of the original value for loop purposes.
+		if ( ! isset( $this->args['_redirection'] ) )
+			$this->args['_redirection'] = $this->args['redirection'];
+
+		/**
+		 * Filter whether to preempt an HTTP request's return.
+		 *
+		 * Returning a truthy value to the filter will short-circuit
+		 * the HTTP request and return early with that value.
+		 *
+		 * @since 2.9.0
+		 *
+		 * @param bool   $preempt Whether to preempt an HTTP request return. Default false.
+		 * @param array  $r       HTTP request arguments.
+		 * @param string $url     The request URL.
+		 */
+		$this->pre = apply_filters( 'pre_http_request', false, $this->args, $this->url );
+		if ( false !== $this->pre ) {
+			$this->parse();
+		}
+	}
+
+	protected function parse() {
+		if ( function_exists( 'wp_kses_bad_protocol' ) ) {
+			if ( $this->args['reject_unsafe_urls'] ) {
+				$this->url = wp_http_validate_url( $this->url );
+			}
+
+			if ( $this->url ) {
+				$this->url = wp_kses_bad_protocol( $this->url, array( 'http', 'https', 'ssl' ) );
+			}
+		}
+
+		$arrURL = @parse_url( $this->url );
+
+		if ( empty( $this->url ) || empty( $arrURL['scheme'] ) ) {
+			return new WP_Error('http_request_failed', __('A valid URL was not provided.'));
+		}
+
+		if ( $this->block_request( $this->url ) ) {
+			return new WP_Error( 'http_request_failed', __( 'User has blocked requests through HTTP.' ) );
+		}
+		/*
+		 * Determine if this is a https call and pass that on to the transport functions
+		 * so that we can blacklist the transports that do not support ssl verification
+		 */
+		$this->args['ssl'] = $arrURL['scheme'] == 'https' || $arrURL['scheme'] == 'ssl';
+
+		// Determine if this request is to OUR install of WordPress.
+		$homeURL = parse_url( get_bloginfo( 'url' ) );
+		$this->args['local'] = 'localhost' == $arrURL['host'] || ( isset( $homeURL['host'] ) && $homeURL['host'] == $arrURL['host'] );
+		unset( $homeURL );
+
+		/*
+		 * If we are streaming to a file but no filename was given drop it in the WP temp dir
+		 * and pick its name using the basename of the $url.
+		 */
+		if ( $this->args['stream']  && empty( $this->args['filename'] ) ) {
+			$this->args['filename'] = get_temp_dir() . wp_unique_filename( get_temp_dir(), basename( $this->url ) );
+		}
+
+		/*
+		 * Force some settings if we are streaming to a file and check for existence and perms
+		 * of destination directory.
+		 */
+		if ( $this->args['stream'] ) {
+			$this->args['blocking'] = true;
+			if ( ! wp_is_writable( dirname( $this->args['filename'] ) ) ) {
+				return new WP_Error( 'http_request_failed', __( 'Destination directory for file streaming does not exist or is not writable.' ) );
+			}
+		}
+
+		if ( is_null( $this->args['headers'] ) ) {
+			$this->args['headers'] = array();
+		}
+
+		if ( ! is_array( $this->args['headers'] ) ) {
+			$processedHeaders = WP_Http::processHeaders( $this->args['headers'], $this->url );
+			$this->args['headers'] = $processedHeaders['headers'];
+		}
+
+		if ( isset( $this->args['headers']['User-Agent'] ) ) {
+			$this->args['user-agent'] = $this->args['headers']['User-Agent'];
+			unset( $this->args['headers']['User-Agent'] );
+		}
+
+		if ( isset( $this->args['headers']['user-agent'] ) ) {
+			$this->args['user-agent'] = $this->args['headers']['user-agent'];
+			unset( $this->args['headers']['user-agent'] );
+		}
+
+		if ( '1.1' == $this->args['httpversion'] && !isset( $this->args['headers']['connection'] ) ) {
+			$this->args['headers']['connection'] = 'close';
+		}
+
+		// Construct Cookie: header if any cookies are set.
+		WP_Http::buildCookieHeader( $this->url );
+
+		// Avoid issues where mbstring.func_overload is enabled.
+		mbstring_binary_safe_encoding();
+
+		if ( ! isset( $this->args['headers']['Accept-Encoding'] ) ) {
+			$encoding = WP_Http_Encoding::accept_encoding( $this->url, $this->args );
+			if ( $encoding ) {
+				$this->args['headers']['Accept-Encoding'] = $encoding;
+			}
+		}
+
+		if ( ( ! is_null( $this->args['body'] ) && '' != $this->args['body'] ) || 'POST' == $this->args['method'] || 'PUT' == $this->args['method'] ) {
+			if ( is_array( $this->args['body'] ) || is_object( $this->args['body'] ) ) {
+				$this->args['body'] = http_build_query( $this->args['body'], null, '&' );
+
+				if ( ! isset( $this->args['headers']['Content-Type'] ) ) {
+					$this->args['headers']['Content-Type'] = 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' );
+				}
+			}
+
+			if ( '' === $this->args['body'] ) {
+				$this->args['body'] = null;
+			}
+
+			if ( ! isset( $this->args['headers']['Content-Length'] ) && ! isset( $this->args['headers']['content-length'] ) ) {
+				$this->args['headers']['Content-Length'] = strlen( $this->args['body'] );
+			}
+		}
+	}
+}
\ No newline at end of file
