MediaWiki  1.23.12
MultiHttpClient.php
Go to the documentation of this file.
1 <?php
44  protected $multiHandle = null; // curl_multi handle
46  protected $caBundlePath;
48  protected $connTimeout = 10;
50  protected $reqTimeout = 300;
52  protected $usePipelining = false;
54  protected $maxConnsPerHost = 50;
55 
63  public function __construct( array $options ) {
64  if ( isset( $options['caBundlePath'] ) ) {
65  $this->caBundlePath = $options['caBundlePath'];
66  if ( !file_exists( $this->caBundlePath ) ) {
67  throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath );
68  }
69  }
70  static $opts = array( 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost' );
71  foreach ( $opts as $key ) {
72  if ( isset( $options[$key] ) ) {
73  $this->$key = $options[$key];
74  }
75  }
76  }
77 
97  final public function run( array $req, array $opts = array() ) {
98  $req = $this->runMulti( array( $req ), $opts );
99  return $req[0]['response'];
100  }
101 
127  public function runMulti( array $reqs, array $opts = array() ) {
128  $chm = $this->getCurlMulti();
129 
130  // Normalize $reqs and add all of the required cURL handles...
131  $handles = array();
132  foreach ( $reqs as $index => &$req ) {
133  $req['response'] = array(
134  'code' => 0,
135  'reason' => '',
136  'headers' => array(),
137  'body' => '',
138  'error' => ''
139  );
140  if ( isset( $req[0] ) ) {
141  $req['method'] = $req[0]; // short-form
142  unset( $req[0] );
143  }
144  if ( isset( $req[1] ) ) {
145  $req['url'] = $req[1]; // short-form
146  unset( $req[1] );
147  }
148  if ( !isset( $req['method'] ) ) {
149  throw new Exception( "Request has no 'method' field set." );
150  } elseif ( !isset( $req['url'] ) ) {
151  throw new Exception( "Request has no 'url' field set." );
152  }
153  $req['query'] = isset( $req['query'] ) ? $req['query'] : array();
154  $headers = array(); // normalized headers
155  if ( isset( $req['headers'] ) ) {
156  foreach ( $req['headers'] as $name => $value ) {
157  $headers[strtolower( $name )] = $value;
158  }
159  }
160  $req['headers'] = $headers;
161  if ( !isset( $req['body'] ) ) {
162  $req['body'] = '';
163  $req['headers']['content-length'] = 0;
164  }
165  $handles[$index] = $this->getCurlHandle( $req, $opts );
166  if ( count( $reqs ) > 1 ) {
167  // https://github.com/guzzle/guzzle/issues/349
168  curl_setopt( $handles[$index], CURLOPT_FORBID_REUSE, true );
169  }
170  }
171  unset( $req ); // don't assign over this by accident
172 
173  $indexes = array_keys( $reqs );
174  if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
175  if ( isset( $opts['usePipelining'] ) ) {
176  curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] );
177  }
178  if ( isset( $opts['maxConnsPerHost'] ) ) {
179  // Keep these sockets around as they may be needed later in the request
180  curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] );
181  }
182  }
183 
184  // @TODO: use a per-host rolling handle window (e.g. CURLMOPT_MAX_HOST_CONNECTIONS)
185  $batches = array_chunk( $indexes, $this->maxConnsPerHost );
186 
187  foreach ( $batches as $batch ) {
188  // Attach all cURL handles for this batch
189  foreach ( $batch as $index ) {
190  curl_multi_add_handle( $chm, $handles[$index] );
191  }
192  // Execute the cURL handles concurrently...
193  $active = null; // handles still being processed
194  do {
195  // Do any available work...
196  do {
197  $mrc = curl_multi_exec( $chm, $active );
198  } while ( $mrc == CURLM_CALL_MULTI_PERFORM );
199  // Wait (if possible) for available work...
200  if ( $active > 0 && $mrc == CURLM_OK ) {
201  if ( curl_multi_select( $chm, 10 ) == -1 ) {
202  // PHP bug 63411; http://curl.haxx.se/libcurl/c/curl_multi_fdset.html
203  usleep( 5000 ); // 5ms
204  }
205  }
206  } while ( $active > 0 && $mrc == CURLM_OK );
207  }
208 
209  // Remove all of the added cURL handles and check for errors...
210  foreach ( $reqs as $index => &$req ) {
211  $ch = $handles[$index];
212  curl_multi_remove_handle( $chm, $ch );
213  if ( curl_errno( $ch ) !== 0 ) {
214  $req['response']['error'] = "(curl error: " .
215  curl_errno( $ch ) . ") " . curl_error( $ch );
216  }
217  // For convenience with the list() operator
218  $req['response'][0] = $req['response']['code'];
219  $req['response'][1] = $req['response']['reason'];
220  $req['response'][2] = $req['response']['headers'];
221  $req['response'][3] = $req['response']['body'];
222  $req['response'][4] = $req['response']['error'];
223  curl_close( $ch );
224  // Close any string wrapper file handles
225  if ( isset( $req['_closeHandle'] ) ) {
226  fclose( $req['_closeHandle'] );
227  unset( $req['_closeHandle'] );
228  }
229  }
230  unset( $req ); // don't assign over this by accident
231 
232  // Restore the default settings
233  if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
234  curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining );
235  curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
236  }
237 
238  return $reqs;
239  }
240 
248  protected function getCurlHandle( array &$req, array $opts = array() ) {
249  $ch = curl_init();
250 
251  curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT,
252  isset( $opts['connTimeout'] ) ? $opts['connTimeout'] : $this->connTimeout );
253  curl_setopt( $ch, CURLOPT_TIMEOUT,
254  isset( $opts['reqTimeout'] ) ? $opts['reqTimeout'] : $this->reqTimeout );
255  curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
256  curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 );
257  curl_setopt( $ch, CURLOPT_HEADER, 0 );
258  if ( !is_null( $this->caBundlePath ) ) {
259  curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
260  curl_setopt( $ch, CURLOPT_CAINFO, $this->caBundlePath );
261  }
262  curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
263 
264  $url = $req['url'];
265  // PHP_QUERY_RFC3986 is PHP 5.4+ only
266  $query = str_replace(
267  array( '+', '%7E' ),
268  array( '%20', '~' ),
269  http_build_query( $req['query'], '', '&' )
270  );
271  if ( $query != '' ) {
272  $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
273  }
274  curl_setopt( $ch, CURLOPT_URL, $url );
275 
276  curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req['method'] );
277  if ( $req['method'] === 'HEAD' ) {
278  curl_setopt( $ch, CURLOPT_NOBODY, 1 );
279  }
280 
281  if ( $req['method'] === 'PUT' ) {
282  curl_setopt( $ch, CURLOPT_PUT, 1 );
283  if ( is_resource( $req['body'] ) ) {
284  curl_setopt( $ch, CURLOPT_INFILE, $req['body'] );
285  if ( isset( $req['headers']['content-length'] ) ) {
286  curl_setopt( $ch, CURLOPT_INFILESIZE, $req['headers']['content-length'] );
287  } elseif ( isset( $req['headers']['transfer-encoding'] ) &&
288  $req['headers']['transfer-encoding'] === 'chunks'
289  ) {
290  curl_setopt( $ch, CURLOPT_UPLOAD, true );
291  } else {
292  throw new Exception( "Missing 'Content-Length' or 'Transfer-Encoding' header." );
293  }
294  } elseif ( $req['body'] !== '' ) {
295  $fp = fopen( "php://temp", "wb+" );
296  fwrite( $fp, $req['body'], strlen( $req['body'] ) );
297  rewind( $fp );
298  curl_setopt( $ch, CURLOPT_INFILE, $fp );
299  curl_setopt( $ch, CURLOPT_INFILESIZE, strlen( $req['body'] ) );
300  $req['_closeHandle'] = $fp; // remember to close this later
301  } else {
302  curl_setopt( $ch, CURLOPT_INFILESIZE, 0 );
303  }
304  curl_setopt( $ch, CURLOPT_READFUNCTION,
305  function ( $ch, $fd, $length ) {
306  $data = fread( $fd, $length );
307  $len = strlen( $data );
308  return $data;
309  }
310  );
311  } elseif ( $req['method'] === 'POST' ) {
312  curl_setopt( $ch, CURLOPT_POST, 1 );
313  // Don't interpret POST parameters starting with '@' as file uploads, because this
314  // makes it impossible to POST plain values starting with '@' (and causes security
315  // issues potentially exposing the contents of local files).
316  // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6,
317  // but we support lower versions, and the option doesn't exist in HHVM 5.6.99.
318  if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) {
319  curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true );
320  } else if ( is_array( $req['body'] ) ) {
321  // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS
322  // is an array, but not if it's a string. So convert $req['body'] to a string
323  // for safety.
324  $req['body'] = wfArrayToCgi( $req['body'] );
325  }
326  curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
327  } else {
328  if ( is_resource( $req['body'] ) || $req['body'] !== '' ) {
329  throw new Exception( "HTTP body specified for a non PUT/POST request." );
330  }
331  $req['headers']['content-length'] = 0;
332  }
333 
334  $headers = array();
335  foreach ( $req['headers'] as $name => $value ) {
336  if ( strpos( $name, ': ' ) ) {
337  throw new Exception( "Headers cannot have ':' in the name." );
338  }
339  $headers[] = $name . ': ' . trim( $value );
340  }
341  curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
342 
343  curl_setopt( $ch, CURLOPT_HEADERFUNCTION,
344  function ( $ch, $header ) use ( &$req ) {
345  $length = strlen( $header );
346  $matches = array();
347  if ( preg_match( "/^(HTTP\/1\.[01]) (\d{3}) (.*)/", $header, $matches ) ) {
348  $req['response']['code'] = (int)$matches[2];
349  $req['response']['reason'] = trim( $matches[3] );
350  return $length;
351  }
352  if ( strpos( $header, ":" ) === false ) {
353  return $length;
354  }
355  list( $name, $value ) = explode( ":", $header, 2 );
356  $req['response']['headers'][strtolower( $name )] = trim( $value );
357  return $length;
358  }
359  );
360 
361  if ( isset( $req['stream'] ) ) {
362  // Don't just use CURLOPT_FILE as that might give:
363  // curl_setopt(): cannot represent a stream of type Output as a STDIO FILE*
364  // The callback here handles both normal files and php://temp handles.
365  curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
366  function ( $ch, $data ) use ( &$req ) {
367  return fwrite( $req['stream'], $data );
368  }
369  );
370  } else {
371  curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
372  function ( $ch, $data ) use ( &$req ) {
373  $req['response']['body'] .= $data;
374  return strlen( $data );
375  }
376  );
377  }
378 
379  return $ch;
380  }
381 
385  protected function getCurlMulti() {
386  if ( !$this->multiHandle ) {
387  $cmh = curl_multi_init();
388  if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
389  curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining );
390  curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
391  }
392  $this->multiHandle = $cmh;
393  }
394  return $this->multiHandle;
395  }
396 
397  function __destruct() {
398  if ( $this->multiHandle ) {
399  curl_multi_close( $this->multiHandle );
400  }
401  }
402 }
MultiHttpClient
Class to handle concurrent HTTP requests.
Definition: MultiHttpClient.php:42
MultiHttpClient\$usePipelining
bool $usePipelining
Definition: MultiHttpClient.php:47
php
skin txt MediaWiki includes four core it has been set as the default in MediaWiki since the replacing Monobook it had been been the default skin since before being replaced by Vector largely rewritten in while keeping its appearance Several legacy skins were removed in the as the burden of supporting them became too heavy to bear Those in etc for skin dependent CSS etc for skin dependent JavaScript These can also be customised on a per user by etc This feature has led to a wide variety of user styles becoming that gallery is a good place to ending in php
Definition: skin.txt:62
MultiHttpClient\$connTimeout
integer $connTimeout
Definition: MultiHttpClient.php:45
MultiHttpClient\__destruct
__destruct()
Definition: MultiHttpClient.php:391
MultiHttpClient\getCurlHandle
getCurlHandle(array &$req, array $opts=array())
Definition: MultiHttpClient.php:242
MultiHttpClient\$reqTimeout
integer $reqTimeout
Definition: MultiHttpClient.php:46
MultiHttpClient\$caBundlePath
string null $caBundlePath
SSL certificates path *.
Definition: MultiHttpClient.php:44
MultiHttpClient\runMulti
runMulti(array $reqs, array $opts=array())
Execute a set of HTTP(S) requests concurrently.
Definition: MultiHttpClient.php:121
array
the array() calling protocol came about after MediaWiki 1.4rc1.
List of Api Query prop modules.
list
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
$options
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1530
$name
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:336
$matches
if(!defined( 'MEDIAWIKI')) if(!isset( $wgVersion)) $matches
Definition: NoLocalSettings.php:33
$value
$value
Definition: styleTest.css.php:45
MultiHttpClient\run
run(array $req, array $opts=array())
Execute an HTTP(S) request.
Definition: MultiHttpClient.php:91
MultiHttpClient\$multiHandle
resource $multiHandle
Definition: MultiHttpClient.php:43
MultiHttpClient\getCurlMulti
getCurlMulti()
Definition: MultiHttpClient.php:379
MultiHttpClient\__construct
__construct(array $options)
Definition: MultiHttpClient.php:57
MultiHttpClient\$maxConnsPerHost
integer $maxConnsPerHost
Definition: MultiHttpClient.php:48
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
$batch
$batch
Definition: linkcache.txt:23
$query
return true to allow those checks to and false if checking is done use this to change the tables headers temp or archived zone change it to an object instance and return false override the list derivative used the name of the old file when set the default code will be skipped add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1105
wfArrayToCgi
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes two arrays as input, and returns a CGI-style string, e.g.
Definition: GlobalFunctions.php:414