9use InvalidArgumentException;
17use Psr\Log\LoggerAwareInterface;
18use Psr\Log\LoggerInterface;
19use Psr\Log\NullLogger;
106 string $caller = __METHOD__,
107 protected readonly ?
Profiler $profiler =
null,
110 if ( !array_key_exists(
'timeout', $options )
111 || !array_key_exists(
'connectTimeout', $options ) ) {
112 throw new InvalidArgumentException(
"timeout and connectionTimeout options are required" );
114 $this->url = $this->urlUtils->expand(
$url,
PROTO_HTTP ) ??
false;
115 $this->parsedUrl = $this->urlUtils->parse( (
string)$this->url ) ??
false;
117 $this->logger = $options[
'logger'] ??
new NullLogger();
118 $this->timeout = $options[
'timeout'];
119 $this->connectTimeout = $options[
'connectTimeout'];
121 if ( !$this->parsedUrl || !self::isValidURI( $this->url ) ) {
122 $this->status = StatusValue::newFatal(
'http-invalid-url',
$url );
124 $this->status = StatusValue::newGood( 100 );
127 if ( isset( $options[
'userAgent'] ) ) {
130 if ( isset( $options[
'username'] ) && isset( $options[
'password'] ) ) {
133 'Basic ' . base64_encode( $options[
'username'] .
':' . $options[
'password'] )
136 if ( isset( $options[
'originalRequest'] ) ) {
140 $members = [
"postData",
"proxy",
"noProxy",
"sslVerifyHost",
"caInfo",
141 "method",
"followRedirects",
"maxRedirects",
"sslVerifyCert",
"callback" ];
143 foreach ( $members as $o ) {
144 if ( isset( $options[$o] ) ) {
147 if ( $o ==
'method' ) {
149 $options[$o] = strtoupper( $options[$o] );
151 $this->$o = $options[$o];
155 if ( $this->noProxy ) {
160 $this->profileName = $caller;
173 return function_exists(
'curl_init' ) ||
wfIniGetBool(
'allow_url_fopen' );
182 return $this->content;
192 $this->postData = $args;
202 foreach ( $telemetry->getRequestHeaders() as $header => $value ) {
203 $this->setHeader( $header, $value );
213 $httpProxy = MediaWikiServices::getInstance()->getMainConfig()->get(
214 MainConfigNames::HTTPProxy );
215 $localHTTPProxy = MediaWikiServices::getInstance()->getMainConfig()->get(
216 MainConfigNames::LocalHTTPProxy );
218 if ( $this->noProxy ) {
224 if ( $this->proxy ) {
230 if ( self::isLocalURL( $this->url ) ) {
231 if ( $localHTTPProxy !==
false ) {
232 $this->setReverseProxy( $localHTTPProxy );
235 $this->proxy = (string)$httpProxy;
250 $parsedProxy = $this->urlUtils->parse( $proxy );
251 if ( $parsedProxy ===
null ) {
252 throw new InvalidArgumentException(
"Invalid reverseProxy configured: $proxy" );
255 $this->setHeader(
'Host', $this->parsedUrl[
'host'] );
257 $this->parsedUrl[
'scheme'] = $parsedProxy[
'scheme'];
258 $this->parsedUrl[
'host'] = $parsedProxy[
'host'];
259 if ( isset( $parsedProxy[
'port'] ) ) {
260 $this->parsedUrl[
'port'] = $parsedProxy[
'port'];
262 unset( $this->parsedUrl[
'port'] );
264 $this->url = UrlUtils::assemble( $this->parsedUrl );
266 $this->noProxy =
true;
275 private static function isLocalURL(
$url ) {
276 $localVirtualHosts = MediaWikiServices::getInstance()->getMainConfig()->get(
277 MainConfigNames::LocalVirtualHosts );
281 if ( preg_match(
'!^https?://([\w.-]+)[/:].*$!',
$url,
$matches ) ) {
284 $domainParts = explode(
'.', $host );
286 $domainParts = array_reverse( $domainParts );
289 $countParts = count( $domainParts );
290 for ( $i = 0; $i < $countParts; $i++ ) {
291 $domainPart = $domainParts[$i];
293 $domain = $domainPart;
295 $domain = $domainPart .
'.' . $domain;
298 if ( in_array( $domain, $localVirtualHosts ) ) {
311 $this->setHeader(
'User-Agent', $UA );
321 $this->reqHeaders[$name] = $value;
343 $this->doSetCallback( $callback );
354 if ( $callback ===
null ) {
355 $callback = $this->read( ... );
356 } elseif ( !is_callable( $callback ) ) {
357 $this->status->fatal(
'http-internal-error' );
358 throw new InvalidArgumentException( __METHOD__ .
': invalid callback' );
360 $this->callback = $callback;
372 public function read( $fh, $content ) {
373 $this->content .= $content;
374 return strlen( $content );
384 throw new LogicException(
'children must override this' );
390 if ( strtoupper( $this->method ) ==
"HEAD" ) {
391 $this->headersOnly =
true;
396 if ( !$this->callback ) {
397 $this->doSetCallback(
null );
400 if ( !isset( $this->reqHeaders[
'User-Agent'] ) ) {
401 $http = MediaWikiServices::getInstance()->getHttpRequestFactory();
402 $this->setUserAgent( $http->getUserAgent() );
415 if ( !$this->status->isOK() ) {
416 $this->respStatus =
'0 Error';
419 foreach ( $this->headerList as $header ) {
420 if ( preg_match(
"#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
421 $this->respVersion = $match[1];
422 $this->respStatus = $match[2];
423 } elseif ( preg_match(
"#^[ \t]#", $header ) ) {
424 $last = count( $this->respHeaders[$lastname] ) - 1;
425 $this->respHeaders[$lastname][$last] .=
"\r\n$header";
426 } elseif ( preg_match(
"#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
427 $this->respHeaders[strtolower( $match[1] )][] = $match[2];
428 $lastname = strtolower( $match[1] );
432 $this->parseCookies();
443 if ( !$this->respHeaders ) {
444 $this->parseHeader();
447 if ( (
int)$this->respStatus > 0 && (
int)$this->respStatus < 400 ) {
448 $this->status->setResult(
true, (
int)$this->respStatus );
450 [ $code, $message ] = explode(
" ", $this->respStatus, 2 );
451 $this->status->setResult(
false, (
int)$this->respStatus );
452 $this->status->fatal(
"http-bad-status", $code, $message );
464 if ( !$this->respHeaders ) {
465 $this->parseHeader();
468 return (
int)$this->respStatus;
477 if ( !$this->respHeaders ) {
478 $this->parseHeader();
481 $status = (int)$this->respStatus;
483 if ( $status >= 300 && $status <= 303 ) {
500 if ( !$this->respHeaders ) {
501 $this->parseHeader();
504 return $this->respHeaders;
514 if ( !$this->respHeaders ) {
515 $this->parseHeader();
518 if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
519 $v = $this->respHeaders[strtolower( $header )];
520 return $v[count( $v ) - 1];
532 $this->cookieJar = $jar;
541 if ( !$this->respHeaders ) {
542 $this->parseHeader();
545 return $this->cookieJar;
557 public function setCookie( $name, $value, array $attr = [] ) {
560 if ( $this->parsedUrl && !isset( $attr[
'domain'] ) ) {
561 $attr[
'domain'] = $this->parsedUrl[
'host'];
564 $this->cookieJar->
setCookie( $name, $value, $attr );
573 if ( isset( $this->respHeaders[
'set-cookie'] ) ) {
574 $url = parse_url( $this->getFinalUrl() );
575 if ( !isset(
$url[
'host'] ) ) {
576 $this->status->fatal(
'http-invalid-url', $this->getFinalUrl() );
578 foreach ( $this->respHeaders[
'set-cookie'] as $cookie ) {
579 $this->cookieJar->parseCookieResponseHeader( $cookie,
$url[
'host'] );
602 $headers = $this->getResponseHeaders();
605 if ( isset( $headers[
'location'] ) ) {
606 $locations = $headers[
'location'];
608 $foundRelativeURI =
false;
609 $countLocations = count( $locations );
611 for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
612 $url = parse_url( $locations[$i] );
614 if ( isset(
$url[
'scheme'] ) && isset(
$url[
'host'] ) ) {
615 $domain =
$url[
'scheme'] .
'://' .
$url[
'host'];
618 $foundRelativeURI =
true;
622 if ( !$foundRelativeURI ) {
623 return $locations[$countLocations - 1];
626 return $domain . $locations[$countLocations - 1];
628 $url = parse_url( $this->url );
629 if ( isset(
$url[
'scheme'] ) && isset(
$url[
'host'] ) ) {
630 return $url[
'scheme'] .
'://' .
$url[
'host'] .
631 $locations[$countLocations - 1];
660 if ( $originalRequest instanceof
WebRequest ) {
662 'ip' => $originalRequest->getIP(),
663 'userAgent' => $originalRequest->getHeader(
'User-Agent' ),
666 !is_array( $originalRequest )
667 || array_diff( [
'ip',
'userAgent' ], array_keys( $originalRequest ) )
669 throw new InvalidArgumentException( __METHOD__ .
': $originalRequest must be a '
670 .
"WebRequest or an array with 'ip' and 'userAgent' keys" );
673 $this->reqHeaders[
'X-Forwarded-For'] = $originalRequest[
'ip'];
674 $this->reqHeaders[
'X-Original-User-Agent'] = $originalRequest[
'userAgent'];
694 return (
bool)preg_match(
695 '/^https?:\/\/[^\/\s]\S*$/D',
702class_alias( MWHttpRequest::class,
'MWHttpRequest' );
wfIniGetBool( $setting)
Safety wrapper around ini_get() for boolean settings.
A class containing constants representing the names of configuration variables.
Generic operation result class Has warning/error list, boolean status and arbitrary value.