MediaWiki
REL1_24
|
00001 <?php 00042 class MultiHttpClient { 00044 protected $multiHandle = null; // curl_multi handle 00046 protected $caBundlePath; 00048 protected $connTimeout = 10; 00050 protected $reqTimeout = 300; 00052 protected $usePipelining = false; 00054 protected $maxConnsPerHost = 50; 00055 00063 public function __construct( array $options ) { 00064 if ( isset( $options['caBundlePath'] ) ) { 00065 $this->caBundlePath = $options['caBundlePath']; 00066 if ( !file_exists( $this->caBundlePath ) ) { 00067 throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath ); 00068 } 00069 } 00070 static $opts = array( 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost' ); 00071 foreach ( $opts as $key ) { 00072 if ( isset( $options[$key] ) ) { 00073 $this->$key = $options[$key]; 00074 } 00075 } 00076 } 00077 00097 final public function run( array $req, array $opts = array() ) { 00098 $req = $this->runMulti( array( $req ), $opts ); 00099 return $req[0]['response']; 00100 } 00101 00127 public function runMulti( array $reqs, array $opts = array() ) { 00128 $chm = $this->getCurlMulti(); 00129 00130 // Normalize $reqs and add all of the required cURL handles... 00131 $handles = array(); 00132 foreach ( $reqs as $index => &$req ) { 00133 $req['response'] = array( 00134 'code' => 0, 00135 'reason' => '', 00136 'headers' => array(), 00137 'body' => '', 00138 'error' => '' 00139 ); 00140 if ( isset( $req[0] ) ) { 00141 $req['method'] = $req[0]; // short-form 00142 unset( $req[0] ); 00143 } 00144 if ( isset( $req[1] ) ) { 00145 $req['url'] = $req[1]; // short-form 00146 unset( $req[1] ); 00147 } 00148 if ( !isset( $req['method'] ) ) { 00149 throw new Exception( "Request has no 'method' field set." ); 00150 } elseif ( !isset( $req['url'] ) ) { 00151 throw new Exception( "Request has no 'url' field set." ); 00152 } 00153 $req['query'] = isset( $req['query'] ) ? $req['query'] : array(); 00154 $headers = array(); // normalized headers 00155 if ( isset( $req['headers'] ) ) { 00156 foreach ( $req['headers'] as $name => $value ) { 00157 $headers[strtolower( $name )] = $value; 00158 } 00159 } 00160 $req['headers'] = $headers; 00161 if ( !isset( $req['body'] ) ) { 00162 $req['body'] = ''; 00163 $req['headers']['content-length'] = 0; 00164 } 00165 $handles[$index] = $this->getCurlHandle( $req, $opts ); 00166 if ( count( $reqs ) > 1 ) { 00167 // https://github.com/guzzle/guzzle/issues/349 00168 curl_setopt( $handles[$index], CURLOPT_FORBID_REUSE, true ); 00169 } 00170 } 00171 unset( $req ); // don't assign over this by accident 00172 00173 $indexes = array_keys( $reqs ); 00174 if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5 00175 if ( isset( $opts['usePipelining'] ) ) { 00176 curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] ); 00177 } 00178 if ( isset( $opts['maxConnsPerHost'] ) ) { 00179 // Keep these sockets around as they may be needed later in the request 00180 curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] ); 00181 } 00182 } 00183 00184 // @TODO: use a per-host rolling handle window (e.g. CURLMOPT_MAX_HOST_CONNECTIONS) 00185 $batches = array_chunk( $indexes, $this->maxConnsPerHost ); 00186 00187 foreach ( $batches as $batch ) { 00188 // Attach all cURL handles for this batch 00189 foreach ( $batch as $index ) { 00190 curl_multi_add_handle( $chm, $handles[$index] ); 00191 } 00192 // Execute the cURL handles concurrently... 00193 $active = null; // handles still being processed 00194 do { 00195 // Do any available work... 00196 do { 00197 $mrc = curl_multi_exec( $chm, $active ); 00198 } while ( $mrc == CURLM_CALL_MULTI_PERFORM ); 00199 // Wait (if possible) for available work... 00200 if ( $active > 0 && $mrc == CURLM_OK ) { 00201 if ( curl_multi_select( $chm, 10 ) == -1 ) { 00202 // PHP bug 63411; http://curl.haxx.se/libcurl/c/curl_multi_fdset.html 00203 usleep( 5000 ); // 5ms 00204 } 00205 } 00206 } while ( $active > 0 && $mrc == CURLM_OK ); 00207 } 00208 00209 // Remove all of the added cURL handles and check for errors... 00210 foreach ( $reqs as $index => &$req ) { 00211 $ch = $handles[$index]; 00212 curl_multi_remove_handle( $chm, $ch ); 00213 if ( curl_errno( $ch ) !== 0 ) { 00214 $req['response']['error'] = "(curl error: " . 00215 curl_errno( $ch ) . ") " . curl_error( $ch ); 00216 } 00217 // For convenience with the list() operator 00218 $req['response'][0] = $req['response']['code']; 00219 $req['response'][1] = $req['response']['reason']; 00220 $req['response'][2] = $req['response']['headers']; 00221 $req['response'][3] = $req['response']['body']; 00222 $req['response'][4] = $req['response']['error']; 00223 curl_close( $ch ); 00224 // Close any string wrapper file handles 00225 if ( isset( $req['_closeHandle'] ) ) { 00226 fclose( $req['_closeHandle'] ); 00227 unset( $req['_closeHandle'] ); 00228 } 00229 } 00230 unset( $req ); // don't assign over this by accident 00231 00232 // Restore the default settings 00233 if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5 00234 curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining ); 00235 curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost ); 00236 } 00237 00238 return $reqs; 00239 } 00240 00248 protected function getCurlHandle( array &$req, array $opts = array() ) { 00249 $ch = curl_init(); 00250 00251 curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 00252 isset( $opts['connTimeout'] ) ? $opts['connTimeout'] : $this->connTimeout ); 00253 curl_setopt( $ch, CURLOPT_TIMEOUT, 00254 isset( $opts['reqTimeout'] ) ? $opts['reqTimeout'] : $this->reqTimeout ); 00255 curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 ); 00256 curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 ); 00257 curl_setopt( $ch, CURLOPT_HEADER, 0 ); 00258 if ( !is_null( $this->caBundlePath ) ) { 00259 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); 00260 curl_setopt( $ch, CURLOPT_CAINFO, $this->caBundlePath ); 00261 } 00262 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); 00263 00264 $url = $req['url']; 00265 // PHP_QUERY_RFC3986 is PHP 5.4+ only 00266 $query = str_replace( 00267 array( '+', '%7E' ), 00268 array( '%20', '~' ), 00269 http_build_query( $req['query'], '', '&' ) 00270 ); 00271 if ( $query != '' ) { 00272 $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query"; 00273 } 00274 curl_setopt( $ch, CURLOPT_URL, $url ); 00275 00276 curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req['method'] ); 00277 if ( $req['method'] === 'HEAD' ) { 00278 curl_setopt( $ch, CURLOPT_NOBODY, 1 ); 00279 } 00280 00281 if ( $req['method'] === 'PUT' ) { 00282 curl_setopt( $ch, CURLOPT_PUT, 1 ); 00283 if ( is_resource( $req['body'] ) ) { 00284 curl_setopt( $ch, CURLOPT_INFILE, $req['body'] ); 00285 if ( isset( $req['headers']['content-length'] ) ) { 00286 curl_setopt( $ch, CURLOPT_INFILESIZE, $req['headers']['content-length'] ); 00287 } elseif ( isset( $req['headers']['transfer-encoding'] ) && 00288 $req['headers']['transfer-encoding'] === 'chunks' 00289 ) { 00290 curl_setopt( $ch, CURLOPT_UPLOAD, true ); 00291 } else { 00292 throw new Exception( "Missing 'Content-Length' or 'Transfer-Encoding' header." ); 00293 } 00294 } elseif ( $req['body'] !== '' ) { 00295 $fp = fopen( "php://temp", "wb+" ); 00296 fwrite( $fp, $req['body'], strlen( $req['body'] ) ); 00297 rewind( $fp ); 00298 curl_setopt( $ch, CURLOPT_INFILE, $fp ); 00299 curl_setopt( $ch, CURLOPT_INFILESIZE, strlen( $req['body'] ) ); 00300 $req['_closeHandle'] = $fp; // remember to close this later 00301 } else { 00302 curl_setopt( $ch, CURLOPT_INFILESIZE, 0 ); 00303 } 00304 curl_setopt( $ch, CURLOPT_READFUNCTION, 00305 function ( $ch, $fd, $length ) { 00306 $data = fread( $fd, $length ); 00307 $len = strlen( $data ); 00308 return $data; 00309 } 00310 ); 00311 } elseif ( $req['method'] === 'POST' ) { 00312 curl_setopt( $ch, CURLOPT_POST, 1 ); 00313 curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] ); 00314 } else { 00315 if ( is_resource( $req['body'] ) || $req['body'] !== '' ) { 00316 throw new Exception( "HTTP body specified for a non PUT/POST request." ); 00317 } 00318 $req['headers']['content-length'] = 0; 00319 } 00320 00321 $headers = array(); 00322 foreach ( $req['headers'] as $name => $value ) { 00323 if ( strpos( $name, ': ' ) ) { 00324 throw new Exception( "Headers cannot have ':' in the name." ); 00325 } 00326 $headers[] = $name . ': ' . trim( $value ); 00327 } 00328 curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); 00329 00330 curl_setopt( $ch, CURLOPT_HEADERFUNCTION, 00331 function ( $ch, $header ) use ( &$req ) { 00332 $length = strlen( $header ); 00333 $matches = array(); 00334 if ( preg_match( "/^(HTTP\/1\.[01]) (\d{3}) (.*)/", $header, $matches ) ) { 00335 $req['response']['code'] = (int)$matches[2]; 00336 $req['response']['reason'] = trim( $matches[3] ); 00337 return $length; 00338 } 00339 if ( strpos( $header, ":" ) === false ) { 00340 return $length; 00341 } 00342 list( $name, $value ) = explode( ":", $header, 2 ); 00343 $req['response']['headers'][strtolower( $name )] = trim( $value ); 00344 return $length; 00345 } 00346 ); 00347 00348 if ( isset( $req['stream'] ) ) { 00349 // Don't just use CURLOPT_FILE as that might give: 00350 // curl_setopt(): cannot represent a stream of type Output as a STDIO FILE* 00351 // The callback here handles both normal files and php://temp handles. 00352 curl_setopt( $ch, CURLOPT_WRITEFUNCTION, 00353 function ( $ch, $data ) use ( &$req ) { 00354 return fwrite( $req['stream'], $data ); 00355 } 00356 ); 00357 } else { 00358 curl_setopt( $ch, CURLOPT_WRITEFUNCTION, 00359 function ( $ch, $data ) use ( &$req ) { 00360 $req['response']['body'] .= $data; 00361 return strlen( $data ); 00362 } 00363 ); 00364 } 00365 00366 return $ch; 00367 } 00368 00372 protected function getCurlMulti() { 00373 if ( !$this->multiHandle ) { 00374 $cmh = curl_multi_init(); 00375 if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5 00376 curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining ); 00377 curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost ); 00378 } 00379 $this->multiHandle = $cmh; 00380 } 00381 return $this->multiHandle; 00382 } 00383 00384 function __destruct() { 00385 if ( $this->multiHandle ) { 00386 curl_multi_close( $this->multiHandle ); 00387 } 00388 } 00389 }