MediaWiki  REL1_24
MultiHttpClient.php
Go to the documentation of this file.
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 }