MediaWiki  master
MultiHttpClient.php
Go to the documentation of this file.
1 <?php
24 use Psr\Log\LoggerAwareInterface;
25 use Psr\Log\LoggerInterface;
26 use Psr\Log\NullLogger;
27 
55 class MultiHttpClient implements LoggerAwareInterface {
57  private const SENSITIVE_HEADERS = '/(^|-|_)(authorization|auth|password|cookie)($|-|_)/';
59  protected $cmh;
61  protected $caBundlePath;
63  protected $connTimeout = 10;
65  protected $maxConnTimeout = INF;
67  protected $reqTimeout = 30;
69  protected $maxReqTimeout = INF;
71  protected $usePipelining = false;
73  protected $maxConnsPerHost = 50;
75  protected $proxy;
77  protected $localProxy = false;
79  protected $localVirtualHosts = [];
81  protected $userAgent = 'wikimedia/multi-http-client v1.0';
83  protected $logger;
84 
85  // In PHP 7 due to https://bugs.php.net/bug.php?id=76480 the request/connect
86  // timeouts are periodically polled instead of being accurately respected.
87  // The select timeout is set to the minimum timeout multiplied by this factor.
88  private const TIMEOUT_ACCURACY_FACTOR = 0.1;
89 
110  public function __construct( array $options ) {
111  if ( isset( $options['caBundlePath'] ) ) {
112  $this->caBundlePath = $options['caBundlePath'];
113  if ( !file_exists( $this->caBundlePath ) ) {
114  throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath );
115  }
116  }
117  static $opts = [
118  'connTimeout', 'maxConnTimeout', 'reqTimeout', 'maxReqTimeout',
119  'usePipelining', 'maxConnsPerHost', 'proxy', 'userAgent', 'logger',
120  'localProxy', 'localVirtualHosts',
121  ];
122  foreach ( $opts as $key ) {
123  if ( isset( $options[$key] ) ) {
124  $this->$key = $options[$key];
125  }
126  }
127  if ( $this->logger === null ) {
128  $this->logger = new NullLogger;
129  }
130  }
131 
155  public function run( array $req, array $opts = [] ) {
156  return $this->runMulti( [ $req ], $opts )[0]['response'];
157  }
158 
190  public function runMulti( array $reqs, array $opts = [] ) {
191  $this->normalizeRequests( $reqs );
192  $opts += [ 'connTimeout' => $this->connTimeout, 'reqTimeout' => $this->reqTimeout ];
193 
194  if ( $opts['connTimeout'] > $this->maxConnTimeout ) {
195  $opts['connTimeout'] = $this->maxConnTimeout;
196  }
197  if ( $opts['reqTimeout'] > $this->maxReqTimeout ) {
198  $opts['reqTimeout'] = $this->maxReqTimeout;
199  }
200 
201  if ( $this->isCurlEnabled() ) {
202  switch ( $opts['httpVersion'] ?? null ) {
203  case 'v1.0':
204  $opts['httpVersion'] = CURL_HTTP_VERSION_1_0;
205  break;
206  case 'v1.1':
207  $opts['httpVersion'] = CURL_HTTP_VERSION_1_1;
208  break;
209  case 'v2':
210  case 'v2.0':
211  $opts['httpVersion'] = CURL_HTTP_VERSION_2_0;
212  break;
213  default:
214  $opts['httpVersion'] = CURL_HTTP_VERSION_NONE;
215  }
216  return $this->runMultiCurl( $reqs, $opts );
217  } else {
218  # TODO: Add handling for httpVersion option
219  return $this->runMultiHttp( $reqs, $opts );
220  }
221  }
222 
228  protected function isCurlEnabled() {
229  // Explicitly test if curl_multi* is blocked, as some users' hosts provide
230  // them with a modified curl with the multi-threaded parts removed(!)
231  return extension_loaded( 'curl' ) && function_exists( 'curl_multi_init' );
232  }
233 
251  private function runMultiCurl( array $reqs, array $opts ) {
252  $chm = $this->getCurlMulti( $opts );
253 
254  $selectTimeout = $this->getSelectTimeout( $opts );
255 
256  // Add all of the required cURL handles...
257  $handles = [];
258  foreach ( $reqs as $index => &$req ) {
259  $handles[$index] = $this->getCurlHandle( $req, $opts );
260  curl_multi_add_handle( $chm, $handles[$index] );
261  }
262  unset( $req ); // don't assign over this by accident
263 
264  $infos = [];
265  // Execute the cURL handles concurrently...
266  $active = null; // handles still being processed
267  do {
268  // Do any available work...
269  do {
270  $mrc = curl_multi_exec( $chm, $active );
271  $info = curl_multi_info_read( $chm );
272  if ( $info !== false ) {
273  $infos[(int)$info['handle']] = $info;
274  }
275  } while ( $mrc == CURLM_CALL_MULTI_PERFORM );
276  // Wait (if possible) for available work...
277  if ( $active > 0 && $mrc == CURLM_OK && curl_multi_select( $chm, $selectTimeout ) == -1 ) {
278  // PHP bug 63411; https://curl.haxx.se/libcurl/c/curl_multi_fdset.html
279  usleep( 5000 ); // 5ms
280  }
281  } while ( $active > 0 && $mrc == CURLM_OK );
282 
283  // Remove all of the added cURL handles and check for errors...
284  foreach ( $reqs as $index => &$req ) {
285  $ch = $handles[$index];
286  curl_multi_remove_handle( $chm, $ch );
287 
288  if ( isset( $infos[(int)$ch] ) ) {
289  $info = $infos[(int)$ch];
290  $errno = $info['result'];
291  if ( $errno !== 0 ) {
292  $req['response']['error'] = "(curl error: $errno)";
293  if ( function_exists( 'curl_strerror' ) ) {
294  $req['response']['error'] .= " " . curl_strerror( $errno );
295  }
296  $this->logger->warning( "Error fetching URL \"{$req['url']}\": " .
297  $req['response']['error'] );
298  } else {
299  $this->logger->debug(
300  "HTTP complete: {method} {url} code={response_code} size={size} " .
301  "total={total_time} connect={connect_time}",
302  [
303  'method' => $req['method'],
304  'url' => $req['url'],
305  'response_code' => $req['response']['code'],
306  'size' => curl_getinfo( $ch, CURLINFO_SIZE_DOWNLOAD ),
307  'total_time' => $this->getCurlTime(
308  $ch, CURLINFO_TOTAL_TIME, 'CURLINFO_TOTAL_TIME_T'
309  ),
310  'connect_time' => $this->getCurlTime(
311  $ch, CURLINFO_CONNECT_TIME, 'CURLINFO_CONNECT_TIME_T'
312  ),
313  ]
314  );
315  }
316  } else {
317  $req['response']['error'] = "(curl error: no status set)";
318  }
319 
320  // For convenience with the list() operator
321  $req['response'][0] = $req['response']['code'];
322  $req['response'][1] = $req['response']['reason'];
323  $req['response'][2] = $req['response']['headers'];
324  $req['response'][3] = $req['response']['body'];
325  $req['response'][4] = $req['response']['error'];
326  curl_close( $ch );
327  // Close any string wrapper file handles
328  if ( isset( $req['_closeHandle'] ) ) {
329  fclose( $req['_closeHandle'] );
330  unset( $req['_closeHandle'] );
331  }
332  }
333  unset( $req ); // don't assign over this by accident
334 
335  return $reqs;
336  }
337 
349  protected function getCurlHandle( array &$req, array $opts ) {
350  $ch = curl_init();
351 
352  curl_setopt( $ch, CURLOPT_PROXY, $req['proxy'] ?? $this->proxy );
353  curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT_MS, intval( $opts['connTimeout'] * 1e3 ) );
354  curl_setopt( $ch, CURLOPT_TIMEOUT_MS, intval( $opts['reqTimeout'] * 1e3 ) );
355  curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
356  curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 );
357  curl_setopt( $ch, CURLOPT_HEADER, 0 );
358  if ( $this->caBundlePath !== null ) {
359  curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
360  curl_setopt( $ch, CURLOPT_CAINFO, $this->caBundlePath );
361  }
362  curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
363 
364  $url = $req['url'];
365  $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
366  if ( $query != '' ) {
367  $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
368  }
369  curl_setopt( $ch, CURLOPT_URL, $url );
370  curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req['method'] );
371  curl_setopt( $ch, CURLOPT_NOBODY, ( $req['method'] === 'HEAD' ) );
372  curl_setopt( $ch, CURLOPT_HTTP_VERSION, $opts['httpVersion'] ?? CURL_HTTP_VERSION_NONE );
373 
374  if ( $req['method'] === 'PUT' ) {
375  curl_setopt( $ch, CURLOPT_PUT, 1 );
376  if ( is_resource( $req['body'] ) ) {
377  curl_setopt( $ch, CURLOPT_INFILE, $req['body'] );
378  if ( isset( $req['headers']['content-length'] ) ) {
379  curl_setopt( $ch, CURLOPT_INFILESIZE, $req['headers']['content-length'] );
380  } elseif ( isset( $req['headers']['transfer-encoding'] ) &&
381  $req['headers']['transfer-encoding'] === 'chunks'
382  ) {
383  curl_setopt( $ch, CURLOPT_UPLOAD, true );
384  } else {
385  throw new Exception( "Missing 'Content-Length' or 'Transfer-Encoding' header." );
386  }
387  } elseif ( $req['body'] !== '' ) {
388  $fp = fopen( "php://temp", "wb+" );
389  fwrite( $fp, $req['body'], strlen( $req['body'] ) );
390  rewind( $fp );
391  curl_setopt( $ch, CURLOPT_INFILE, $fp );
392  curl_setopt( $ch, CURLOPT_INFILESIZE, strlen( $req['body'] ) );
393  $req['_closeHandle'] = $fp; // remember to close this later
394  } else {
395  curl_setopt( $ch, CURLOPT_INFILESIZE, 0 );
396  }
397  curl_setopt( $ch, CURLOPT_READFUNCTION,
398  static function ( $ch, $fd, $length ) {
399  return (string)fread( $fd, $length );
400  }
401  );
402  } elseif ( $req['method'] === 'POST' ) {
403  curl_setopt( $ch, CURLOPT_POST, 1 );
404  curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
405  } else {
406  if ( is_resource( $req['body'] ) || $req['body'] !== '' ) {
407  throw new Exception( "HTTP body specified for a non PUT/POST request." );
408  }
409  $req['headers']['content-length'] = 0;
410  }
411 
412  if ( !isset( $req['headers']['user-agent'] ) ) {
413  $req['headers']['user-agent'] = $this->userAgent;
414  }
415 
416  $headers = [];
417  foreach ( $req['headers'] as $name => $value ) {
418  if ( strpos( $name, ': ' ) ) {
419  throw new Exception( "Headers cannot have ':' in the name." );
420  }
421  $headers[] = $name . ': ' . trim( $value );
422  }
423  curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
424 
425  curl_setopt( $ch, CURLOPT_HEADERFUNCTION,
426  static function ( $ch, $header ) use ( &$req ) {
427  if ( !empty( $req['flags']['relayResponseHeaders'] ) && trim( $header ) !== '' ) {
428  header( $header );
429  }
430  $length = strlen( $header );
431  $matches = [];
432  if ( preg_match( "/^(HTTP\/(?:1\.[01]|2)) (\d{3}) (.*)/", $header, $matches ) ) {
433  $req['response']['code'] = (int)$matches[2];
434  $req['response']['reason'] = trim( $matches[3] );
435  // After a redirect we will receive this again, but we already stored headers
436  // that belonged to a redirect response. Start over.
437  $req['response']['headers'] = [];
438  return $length;
439  }
440  if ( strpos( $header, ":" ) === false ) {
441  return $length;
442  }
443  list( $name, $value ) = explode( ":", $header, 2 );
444  $name = strtolower( $name );
445  $value = trim( $value );
446  if ( isset( $req['response']['headers'][$name] ) ) {
447  $req['response']['headers'][$name] .= ', ' . $value;
448  } else {
449  $req['response']['headers'][$name] = $value;
450  }
451  return $length;
452  }
453  );
454 
455  // This works with both file and php://temp handles (unlike CURLOPT_FILE)
456  $hasOutputStream = isset( $req['stream'] );
457  curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
458  static function ( $ch, $data ) use ( &$req, $hasOutputStream ) {
459  if ( $hasOutputStream ) {
460  return fwrite( $req['stream'], $data );
461  } else {
462  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
463  $req['response']['body'] .= $data;
464 
465  return strlen( $data );
466  }
467  }
468  );
469 
470  return $ch;
471  }
472 
478  protected function getCurlMulti( array $opts ) {
479  if ( !$this->cmh ) {
480  $cmh = curl_multi_init();
481  // Limit the size of the idle connection cache such that consecutive parallel
482  // request batches to the same host can avoid having to keep making connections
483  curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
484  $this->cmh = $cmh;
485  }
486 
487  // CURLMOPT_MAX_HOST_CONNECTIONS is available since PHP 7.0.7 and cURL 7.30.0
488  if ( version_compare( curl_version()['version'], '7.30.0', '>=' ) ) {
489  // Limit the number of in-flight requests for any given host
490  $maxHostConns = $opts['maxConnsPerHost'] ?? $this->maxConnsPerHost;
491  curl_multi_setopt( $this->cmh, CURLMOPT_MAX_HOST_CONNECTIONS, (int)$maxHostConns );
492  }
493 
494  // Configure when to multiplex multiple requests onto single TCP handles
495  $pipelining = $opts['usePipelining'] ?? $this->usePipelining;
496  curl_multi_setopt( $this->cmh, CURLMOPT_PIPELINING, $pipelining ? 3 : 0 );
497 
498  return $this->cmh;
499  }
500 
510  private function getCurlTime( $ch, $oldOption, $newConstName ): string {
511  if ( defined( $newConstName ) ) {
512  return sprintf( "%.6f", curl_getinfo( $ch, constant( $newConstName ) ) / 1e6 );
513  } else {
514  return (string)curl_getinfo( $ch, $oldOption );
515  }
516  }
517 
533  private function runMultiHttp( array $reqs, array $opts = [] ) {
534  $httpOptions = [
535  'timeout' => $opts['reqTimeout'] ?? $this->reqTimeout,
536  'connectTimeout' => $opts['connTimeout'] ?? $this->connTimeout,
537  'logger' => $this->logger,
538  'caInfo' => $this->caBundlePath,
539  ];
540  foreach ( $reqs as &$req ) {
541  $reqOptions = $httpOptions + [
542  'method' => $req['method'],
543  'proxy' => $req['proxy'] ?? $this->proxy,
544  'userAgent' => $req['headers']['user-agent'] ?? $this->userAgent,
545  'postData' => $req['body'],
546  ];
547 
548  $url = $req['url'];
549  $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
550  if ( $query != '' ) {
551  $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
552  }
553 
554  $httpRequest = MediaWikiServices::getInstance()->getHttpRequestFactory()->create(
555  $url, $reqOptions, __METHOD__ );
556  $httpRequest->setLogger( $this->logger );
557  $sv = $httpRequest->execute()->getStatusValue();
558 
559  $respHeaders = array_map(
560  static function ( $v ) {
561  return implode( ', ', $v );
562  },
563  $httpRequest->getResponseHeaders() );
564 
565  $req['response'] = [
566  'code' => $httpRequest->getStatus(),
567  'reason' => '',
568  'headers' => $respHeaders,
569  'body' => $httpRequest->getContent(),
570  'error' => '',
571  ];
572 
573  if ( !$sv->isOK() ) {
574  $svErrors = $sv->getErrors();
575  if ( isset( $svErrors[0] ) ) {
576  $req['response']['error'] = $svErrors[0]['message'];
577 
578  // param values vary per failure type (ex. unknown host vs unknown page)
579  if ( isset( $svErrors[0]['params'][0] ) ) {
580  if ( is_numeric( $svErrors[0]['params'][0] ) ) {
581  if ( isset( $svErrors[0]['params'][1] ) ) {
582  // @phan-suppress-next-line PhanTypeInvalidDimOffset
583  $req['response']['reason'] = $svErrors[0]['params'][1];
584  }
585  } else {
586  $req['response']['reason'] = $svErrors[0]['params'][0];
587  }
588  }
589  }
590  }
591 
592  $req['response'][0] = $req['response']['code'];
593  $req['response'][1] = $req['response']['reason'];
594  $req['response'][2] = $req['response']['headers'];
595  $req['response'][3] = $req['response']['body'];
596  $req['response'][4] = $req['response']['error'];
597  }
598 
599  return $reqs;
600  }
601 
607  private function normalizeRequests( array &$reqs ) {
608  foreach ( $reqs as &$req ) {
609  $req['response'] = [
610  'code' => 0,
611  'reason' => '',
612  'headers' => [],
613  'body' => '',
614  'error' => ''
615  ];
616  if ( isset( $req[0] ) ) {
617  $req['method'] = $req[0]; // short-form
618  unset( $req[0] );
619  }
620  if ( isset( $req[1] ) ) {
621  $req['url'] = $req[1]; // short-form
622  unset( $req[1] );
623  }
624  if ( !isset( $req['method'] ) ) {
625  throw new Exception( "Request has no 'method' field set." );
626  } elseif ( !isset( $req['url'] ) ) {
627  throw new Exception( "Request has no 'url' field set." );
628  }
629  if ( $this->localProxy !== false && $this->isLocalURL( $req['url'] ) ) {
630  $this->useReverseProxy( $req, $this->localProxy );
631  }
632  $req['query'] = $req['query'] ?? [];
633  $headers = []; // normalized headers
634  if ( isset( $req['headers'] ) ) {
635  foreach ( $req['headers'] as $name => $value ) {
636  $headers[strtolower( $name )] = $value;
637  }
638  }
639  $req['headers'] = $headers;
640  if ( !isset( $req['body'] ) ) {
641  $req['body'] = '';
642  $req['headers']['content-length'] = 0;
643  }
644  // Redact some headers we know to have tokens before logging them
645  $logHeaders = $req['headers'];
646  foreach ( $logHeaders as $header => $value ) {
647  if ( preg_match( self::SENSITIVE_HEADERS, $header ) === 1 ) {
648  $logHeaders[$header] = '[redacted]';
649  }
650  }
651  $this->logger->debug( "HTTP start: {method} {url}",
652  [
653  'method' => $req['method'],
654  'url' => $req['url'],
655  'headers' => $logHeaders,
656  ]
657  );
658  $req['flags'] = $req['flags'] ?? [];
659  }
660  }
661 
662  private function useReverseProxy( array &$req, $proxy ) {
663  $parsedProxy = wfParseUrl( $proxy );
664  if ( $parsedProxy === false ) {
665  throw new Exception( "Invalid reverseProxy configured: $proxy" );
666  }
667  $parsedUrl = wfParseUrl( $req['url'] );
668  if ( $parsedUrl === false ) {
669  throw new Exception( "Invalid url specified: ${req['url']}" );
670  }
671  // Set the current host in the Host header
672  $req['headers']['Host'] = $parsedUrl['host'];
673  // Replace scheme, host and port in the request
674  $parsedUrl['scheme'] = $parsedProxy['scheme'];
675  $parsedUrl['host'] = $parsedProxy['host'];
676  if ( isset( $parsedProxy['port'] ) ) {
677  $parsedUrl['port'] = $parsedProxy['port'];
678  } else {
679  unset( $parsedUrl['port'] );
680  }
681  $req['url'] = wfAssembleUrl( $parsedUrl );
682  // Explicitly disable use of another proxy by setting to false,
683  // since null will fallback to $this->proxy
684  $req['proxy'] = false;
685  }
686 
694  private function isLocalURL( $url ) {
695  if ( !$this->localVirtualHosts ) {
696  // Shortcut
697  return false;
698  }
699 
700  // Extract host part
701  $matches = [];
702  if ( preg_match( '!^https?://([\w.-]+)[/:].*$!', $url, $matches ) ) {
703  $host = $matches[1];
704  // Split up dotwise
705  $domainParts = explode( '.', $host );
706  // Check if this domain or any superdomain is listed as a local virtual host
707  $domainParts = array_reverse( $domainParts );
708 
709  $domain = '';
710  $countParts = count( $domainParts );
711  for ( $i = 0; $i < $countParts; $i++ ) {
712  $domainPart = $domainParts[$i];
713  if ( $i == 0 ) {
714  $domain = $domainPart;
715  } else {
716  $domain = $domainPart . '.' . $domain;
717  }
718 
719  if ( in_array( $domain, $this->localVirtualHosts ) ) {
720  return true;
721  }
722  }
723  }
724 
725  return false;
726  }
727 
734  private function getSelectTimeout( $opts ) {
735  $connTimeout = $opts['connTimeout'] ?? $this->connTimeout;
736  $reqTimeout = $opts['reqTimeout'] ?? $this->reqTimeout;
737  $timeouts = array_filter( [ $connTimeout, $reqTimeout ] );
738  if ( count( $timeouts ) === 0 ) {
739  return 1;
740  }
741 
742  $selectTimeout = min( $timeouts ) * self::TIMEOUT_ACCURACY_FACTOR;
743  // Minimum 10us
744  if ( $selectTimeout < 10e-6 ) {
745  $selectTimeout = 10e-6;
746  }
747  return $selectTimeout;
748  }
749 
755  public function setLogger( LoggerInterface $logger ) {
756  $this->logger = $logger;
757  }
758 
759  public function __destruct() {
760  if ( $this->cmh ) {
761  curl_multi_close( $this->cmh );
762  }
763  }
764 }
MultiHttpClient
Class to handle multiple HTTP requests.
Definition: MultiHttpClient.php:55
MultiHttpClient\$usePipelining
bool $usePipelining
Definition: MultiHttpClient.php:71
MultiHttpClient\$maxConnsPerHost
int $maxConnsPerHost
Definition: MultiHttpClient.php:73
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:203
MultiHttpClient\$reqTimeout
float $reqTimeout
Definition: MultiHttpClient.php:67
MultiHttpClient\isLocalURL
isLocalURL( $url)
Check if the URL can be served by localhost.
Definition: MultiHttpClient.php:694
MultiHttpClient\run
run(array $req, array $opts=[])
Execute an HTTP(S) request.
Definition: MultiHttpClient.php:155
MultiHttpClient\__destruct
__destruct()
Definition: MultiHttpClient.php:759
wfParseUrl
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
Definition: GlobalFunctions.php:776
MultiHttpClient\$connTimeout
float $connTimeout
Definition: MultiHttpClient.php:63
MultiHttpClient\$maxReqTimeout
float $maxReqTimeout
Definition: MultiHttpClient.php:69
$matches
$matches
Definition: NoLocalSettings.php:24
MultiHttpClient\$caBundlePath
string null $caBundlePath
SSL certificates path.
Definition: MultiHttpClient.php:61
MultiHttpClient\getSelectTimeout
getSelectTimeout( $opts)
Get a suitable select timeout for the given options.
Definition: MultiHttpClient.php:734
MultiHttpClient\setLogger
setLogger(LoggerInterface $logger)
Register a logger.
Definition: MultiHttpClient.php:755
MultiHttpClient\$localProxy
string bool $localProxy
Definition: MultiHttpClient.php:77
MultiHttpClient\runMultiHttp
runMultiHttp(array $reqs, array $opts=[])
Execute a set of HTTP(S) requests sequentially.
Definition: MultiHttpClient.php:533
MultiHttpClient\getCurlHandle
getCurlHandle(array &$req, array $opts)
Definition: MultiHttpClient.php:349
$header
$header
Definition: updateCredits.php:37
MultiHttpClient\SENSITIVE_HEADERS
const SENSITIVE_HEADERS
Regex for headers likely to contain tokens, etc.
Definition: MultiHttpClient.php:57
MultiHttpClient\isCurlEnabled
isCurlEnabled()
Determines if the curl extension is available.
Definition: MultiHttpClient.php:228
MultiHttpClient\useReverseProxy
useReverseProxy(array &$req, $proxy)
Definition: MultiHttpClient.php:662
MultiHttpClient\$userAgent
string $userAgent
Definition: MultiHttpClient.php:81
MultiHttpClient\runMulti
runMulti(array $reqs, array $opts=[])
Execute a set of HTTP(S) requests.
Definition: MultiHttpClient.php:190
MultiHttpClient\$proxy
string null $proxy
proxy
Definition: MultiHttpClient.php:75
MultiHttpClient\runMultiCurl
runMultiCurl(array $reqs, array $opts)
Execute a set of HTTP(S) requests concurrently.
Definition: MultiHttpClient.php:251
MultiHttpClient\__construct
__construct(array $options)
Since 1.35, callers should use HttpRequestFactory::createMultiClient() to get a client object with ap...
Definition: MultiHttpClient.php:110
MultiHttpClient\TIMEOUT_ACCURACY_FACTOR
const TIMEOUT_ACCURACY_FACTOR
Definition: MultiHttpClient.php:88
MultiHttpClient\$localVirtualHosts
string[] $localVirtualHosts
Definition: MultiHttpClient.php:79
MultiHttpClient\getCurlMulti
getCurlMulti(array $opts)
Definition: MultiHttpClient.php:478
wfAssembleUrl
wfAssembleUrl( $urlParts)
This function will reassemble a URL parsed with wfParseURL.
Definition: GlobalFunctions.php:570
MultiHttpClient\$maxConnTimeout
float $maxConnTimeout
Definition: MultiHttpClient.php:65
MultiHttpClient\normalizeRequests
normalizeRequests(array &$reqs)
Normalize request information.
Definition: MultiHttpClient.php:607
MultiHttpClient\getCurlTime
getCurlTime( $ch, $oldOption, $newConstName)
Get a time in seconds, formatted with microsecond resolution, or fall back to second resolution on PH...
Definition: MultiHttpClient.php:510
MultiHttpClient\$cmh
resource $cmh
curl_multi_init() handle
Definition: MultiHttpClient.php:59
MultiHttpClient\$logger
LoggerInterface $logger
Definition: MultiHttpClient.php:83