MediaWiki  1.27.2
MultiHttpClient.php
Go to the documentation of this file.
1 <?php
45  protected $multiHandle = null; // curl_multi handle
47  protected $caBundlePath;
49  protected $connTimeout = 10;
51  protected $reqTimeout = 300;
53  protected $usePipelining = false;
55  protected $maxConnsPerHost = 50;
57  protected $proxy;
59  protected $userAgent = 'wikimedia/multi-http-client v1.0';
60 
71  public function __construct( array $options ) {
72  if ( isset( $options['caBundlePath'] ) ) {
73  $this->caBundlePath = $options['caBundlePath'];
74  if ( !file_exists( $this->caBundlePath ) ) {
75  throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath );
76  }
77  }
78  static $opts = [
79  'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost', 'proxy', 'userAgent'
80  ];
81  foreach ( $opts as $key ) {
82  if ( isset( $options[$key] ) ) {
83  $this->$key = $options[$key];
84  }
85  }
86  }
87 
107  final public function run( array $req, array $opts = [] ) {
108  return $this->runMulti( [ $req ], $opts )[0]['response'];
109  }
110 
137  public function runMulti( array $reqs, array $opts = [] ) {
138  $chm = $this->getCurlMulti();
139 
140  // Normalize $reqs and add all of the required cURL handles...
141  $handles = [];
142  foreach ( $reqs as $index => &$req ) {
143  $req['response'] = [
144  'code' => 0,
145  'reason' => '',
146  'headers' => [],
147  'body' => '',
148  'error' => ''
149  ];
150  if ( isset( $req[0] ) ) {
151  $req['method'] = $req[0]; // short-form
152  unset( $req[0] );
153  }
154  if ( isset( $req[1] ) ) {
155  $req['url'] = $req[1]; // short-form
156  unset( $req[1] );
157  }
158  if ( !isset( $req['method'] ) ) {
159  throw new Exception( "Request has no 'method' field set." );
160  } elseif ( !isset( $req['url'] ) ) {
161  throw new Exception( "Request has no 'url' field set." );
162  }
163  $req['query'] = isset( $req['query'] ) ? $req['query'] : [];
164  $headers = []; // normalized headers
165  if ( isset( $req['headers'] ) ) {
166  foreach ( $req['headers'] as $name => $value ) {
167  $headers[strtolower( $name )] = $value;
168  }
169  }
170  $req['headers'] = $headers;
171  if ( !isset( $req['body'] ) ) {
172  $req['body'] = '';
173  $req['headers']['content-length'] = 0;
174  }
175  $handles[$index] = $this->getCurlHandle( $req, $opts );
176  if ( count( $reqs ) > 1 ) {
177  // https://github.com/guzzle/guzzle/issues/349
178  curl_setopt( $handles[$index], CURLOPT_FORBID_REUSE, true );
179  }
180  }
181  unset( $req ); // don't assign over this by accident
182 
183  $indexes = array_keys( $reqs );
184  if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
185  if ( isset( $opts['usePipelining'] ) ) {
186  curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] );
187  }
188  if ( isset( $opts['maxConnsPerHost'] ) ) {
189  // Keep these sockets around as they may be needed later in the request
190  curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] );
191  }
192  }
193 
194  // @TODO: use a per-host rolling handle window (e.g. CURLMOPT_MAX_HOST_CONNECTIONS)
195  $batches = array_chunk( $indexes, $this->maxConnsPerHost );
196  $infos = [];
197 
198  foreach ( $batches as $batch ) {
199  // Attach all cURL handles for this batch
200  foreach ( $batch as $index ) {
201  curl_multi_add_handle( $chm, $handles[$index] );
202  }
203  // Execute the cURL handles concurrently...
204  $active = null; // handles still being processed
205  do {
206  // Do any available work...
207  do {
208  $mrc = curl_multi_exec( $chm, $active );
209  $info = curl_multi_info_read( $chm );
210  if ( $info !== false ) {
211  $infos[(int)$info['handle']] = $info;
212  }
213  } while ( $mrc == CURLM_CALL_MULTI_PERFORM );
214  // Wait (if possible) for available work...
215  if ( $active > 0 && $mrc == CURLM_OK ) {
216  if ( curl_multi_select( $chm, 10 ) == -1 ) {
217  // PHP bug 63411; http://curl.haxx.se/libcurl/c/curl_multi_fdset.html
218  usleep( 5000 ); // 5ms
219  }
220  }
221  } while ( $active > 0 && $mrc == CURLM_OK );
222  }
223 
224  // Remove all of the added cURL handles and check for errors...
225  foreach ( $reqs as $index => &$req ) {
226  $ch = $handles[$index];
227  curl_multi_remove_handle( $chm, $ch );
228 
229  if ( isset( $infos[(int)$ch] ) ) {
230  $info = $infos[(int)$ch];
231  $errno = $info['result'];
232  if ( $errno !== 0 ) {
233  $req['response']['error'] = "(curl error: $errno)";
234  if ( function_exists( 'curl_strerror' ) ) {
235  $req['response']['error'] .= " " . curl_strerror( $errno );
236  }
237  }
238  } else {
239  $req['response']['error'] = "(curl error: no status set)";
240  }
241 
242  // For convenience with the list() operator
243  $req['response'][0] = $req['response']['code'];
244  $req['response'][1] = $req['response']['reason'];
245  $req['response'][2] = $req['response']['headers'];
246  $req['response'][3] = $req['response']['body'];
247  $req['response'][4] = $req['response']['error'];
248  curl_close( $ch );
249  // Close any string wrapper file handles
250  if ( isset( $req['_closeHandle'] ) ) {
251  fclose( $req['_closeHandle'] );
252  unset( $req['_closeHandle'] );
253  }
254  }
255  unset( $req ); // don't assign over this by accident
256 
257  // Restore the default settings
258  if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
259  curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining );
260  curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
261  }
262 
263  return $reqs;
264  }
265 
274  protected function getCurlHandle( array &$req, array $opts = [] ) {
275  $ch = curl_init();
276 
277  curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT,
278  isset( $opts['connTimeout'] ) ? $opts['connTimeout'] : $this->connTimeout );
279  curl_setopt( $ch, CURLOPT_PROXY, isset( $req['proxy'] ) ? $req['proxy'] : $this->proxy );
280  curl_setopt( $ch, CURLOPT_TIMEOUT,
281  isset( $opts['reqTimeout'] ) ? $opts['reqTimeout'] : $this->reqTimeout );
282  curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
283  curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 );
284  curl_setopt( $ch, CURLOPT_HEADER, 0 );
285  if ( !is_null( $this->caBundlePath ) ) {
286  curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
287  curl_setopt( $ch, CURLOPT_CAINFO, $this->caBundlePath );
288  }
289  curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
290 
291  $url = $req['url'];
292  // PHP_QUERY_RFC3986 is PHP 5.4+ only
293  $query = str_replace(
294  [ '+', '%7E' ],
295  [ '%20', '~' ],
296  http_build_query( $req['query'], '', '&' )
297  );
298  if ( $query != '' ) {
299  $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
300  }
301  curl_setopt( $ch, CURLOPT_URL, $url );
302 
303  curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req['method'] );
304  if ( $req['method'] === 'HEAD' ) {
305  curl_setopt( $ch, CURLOPT_NOBODY, 1 );
306  }
307 
308  if ( $req['method'] === 'PUT' ) {
309  curl_setopt( $ch, CURLOPT_PUT, 1 );
310  if ( is_resource( $req['body'] ) ) {
311  curl_setopt( $ch, CURLOPT_INFILE, $req['body'] );
312  if ( isset( $req['headers']['content-length'] ) ) {
313  curl_setopt( $ch, CURLOPT_INFILESIZE, $req['headers']['content-length'] );
314  } elseif ( isset( $req['headers']['transfer-encoding'] ) &&
315  $req['headers']['transfer-encoding'] === 'chunks'
316  ) {
317  curl_setopt( $ch, CURLOPT_UPLOAD, true );
318  } else {
319  throw new Exception( "Missing 'Content-Length' or 'Transfer-Encoding' header." );
320  }
321  } elseif ( $req['body'] !== '' ) {
322  $fp = fopen( "php://temp", "wb+" );
323  fwrite( $fp, $req['body'], strlen( $req['body'] ) );
324  rewind( $fp );
325  curl_setopt( $ch, CURLOPT_INFILE, $fp );
326  curl_setopt( $ch, CURLOPT_INFILESIZE, strlen( $req['body'] ) );
327  $req['_closeHandle'] = $fp; // remember to close this later
328  } else {
329  curl_setopt( $ch, CURLOPT_INFILESIZE, 0 );
330  }
331  curl_setopt( $ch, CURLOPT_READFUNCTION,
332  function ( $ch, $fd, $length ) {
333  $data = fread( $fd, $length );
334  $len = strlen( $data );
335  return $data;
336  }
337  );
338  } elseif ( $req['method'] === 'POST' ) {
339  curl_setopt( $ch, CURLOPT_POST, 1 );
340  // Don't interpret POST parameters starting with '@' as file uploads, because this
341  // makes it impossible to POST plain values starting with '@' (and causes security
342  // issues potentially exposing the contents of local files).
343  // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6,
344  // but we support lower versions, and the option doesn't exist in HHVM 5.6.99.
345  if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) {
346  curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true );
347  } elseif ( is_array( $req['body'] ) ) {
348  // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS
349  // is an array, but not if it's a string. So convert $req['body'] to a string
350  // for safety.
351  $req['body'] = wfArrayToCgi( $req['body'] );
352  }
353  curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
354  } else {
355  if ( is_resource( $req['body'] ) || $req['body'] !== '' ) {
356  throw new Exception( "HTTP body specified for a non PUT/POST request." );
357  }
358  $req['headers']['content-length'] = 0;
359  }
360 
361  if ( !isset( $req['headers']['user-agent'] ) ) {
362  $req['headers']['user-agent'] = $this->userAgent;
363  }
364 
365  $headers = [];
366  foreach ( $req['headers'] as $name => $value ) {
367  if ( strpos( $name, ': ' ) ) {
368  throw new Exception( "Headers cannot have ':' in the name." );
369  }
370  $headers[] = $name . ': ' . trim( $value );
371  }
372  curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
373 
374  curl_setopt( $ch, CURLOPT_HEADERFUNCTION,
375  function ( $ch, $header ) use ( &$req ) {
376  $length = strlen( $header );
377  $matches = [];
378  if ( preg_match( "/^(HTTP\/1\.[01]) (\d{3}) (.*)/", $header, $matches ) ) {
379  $req['response']['code'] = (int)$matches[2];
380  $req['response']['reason'] = trim( $matches[3] );
381  return $length;
382  }
383  if ( strpos( $header, ":" ) === false ) {
384  return $length;
385  }
386  list( $name, $value ) = explode( ":", $header, 2 );
387  $req['response']['headers'][strtolower( $name )] = trim( $value );
388  return $length;
389  }
390  );
391 
392  if ( isset( $req['stream'] ) ) {
393  // Don't just use CURLOPT_FILE as that might give:
394  // curl_setopt(): cannot represent a stream of type Output as a STDIO FILE*
395  // The callback here handles both normal files and php://temp handles.
396  curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
397  function ( $ch, $data ) use ( &$req ) {
398  return fwrite( $req['stream'], $data );
399  }
400  );
401  } else {
402  curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
403  function ( $ch, $data ) use ( &$req ) {
404  $req['response']['body'] .= $data;
405  return strlen( $data );
406  }
407  );
408  }
409 
410  return $ch;
411  }
412 
416  protected function getCurlMulti() {
417  if ( !$this->multiHandle ) {
418  $cmh = curl_multi_init();
419  if ( function_exists( 'curl_multi_setopt' ) ) { // PHP 5.5
420  curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining );
421  curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
422  }
423  $this->multiHandle = $cmh;
424  }
425  return $this->multiHandle;
426  }
427 
428  function __destruct() {
429  if ( $this->multiHandle ) {
430  curl_multi_close( $this->multiHandle );
431  }
432  }
433 }
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
string null $caBundlePath
SSL certificates path.
the array() calling protocol came about after MediaWiki 1.4rc1.
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1418
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2321
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
$value
__construct(array $options)
$batch
Definition: linkcache.txt:23
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition: hooks.txt:1004
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
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
this hook is for auditing only $req
Definition: hooks.txt:965
string null $proxy
proxy
getCurlHandle(array &$req, array $opts=[])
wfArrayToCgi($array1, $array2=null, $prefix= '')
This function takes one or two arrays as input, and returns a CGI-style string, e.g.
integer $maxConnsPerHost
run(array $req, array $opts=[])
Execute an HTTP(S) request.
Class to handle concurrent HTTP requests.
runMulti(array $reqs, array $opts=[])
Execute a set of HTTP(S) requests concurrently.
$matches
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:310