12use Psr\Log\LoggerAwareInterface;
13use Psr\Log\LoggerInterface;
14use Psr\Log\NullLogger;
109 $url, array $options, $caller = __METHOD__, ?
Profiler $profiler =
null
111 $this->urlUtils = MediaWikiServices::getInstance()->getUrlUtils();
112 if ( !array_key_exists(
'timeout', $options )
113 || !array_key_exists(
'connectTimeout', $options ) ) {
114 throw new InvalidArgumentException(
"timeout and connectionTimeout options are required" );
116 $this->url = $this->urlUtils->expand(
$url,
PROTO_HTTP ) ??
false;
117 $this->parsedUrl = $this->urlUtils->parse( (
string)$this->url ) ??
false;
119 $this->logger = $options[
'logger'] ??
new NullLogger();
120 $this->timeout = $options[
'timeout'];
121 $this->connectTimeout = $options[
'connectTimeout'];
123 if ( !$this->parsedUrl || !self::isValidURI( $this->url ) ) {
124 $this->status = StatusValue::newFatal(
'http-invalid-url',
$url );
126 $this->status = StatusValue::newGood( 100 );
129 if ( isset( $options[
'userAgent'] ) ) {
132 if ( isset( $options[
'username'] ) && isset( $options[
'password'] ) ) {
135 'Basic ' . base64_encode( $options[
'username'] .
':' . $options[
'password'] )
138 if ( isset( $options[
'originalRequest'] ) ) {
142 $members = [
"postData",
"proxy",
"noProxy",
"sslVerifyHost",
"caInfo",
143 "method",
"followRedirects",
"maxRedirects",
"sslVerifyCert",
"callback" ];
145 foreach ( $members as $o ) {
146 if ( isset( $options[$o] ) ) {
149 if ( $o ==
'method' ) {
151 $options[$o] = strtoupper( $options[$o] );
153 $this->$o = $options[$o];
157 if ( $this->noProxy ) {
163 $this->profileName = $caller;
166 public function setLogger( LoggerInterface $logger ): void {
167 $this->logger = $logger;
176 return function_exists(
'curl_init' ) ||
wfIniGetBool(
'allow_url_fopen' );
185 return $this->content;
195 $this->postData = $args;
205 foreach ( $telemetry->getRequestHeaders() as $header => $value ) {
206 $this->setHeader( $header, $value );
216 $httpProxy = MediaWikiServices::getInstance()->getMainConfig()->get(
217 MainConfigNames::HTTPProxy );
218 $localHTTPProxy = MediaWikiServices::getInstance()->getMainConfig()->get(
219 MainConfigNames::LocalHTTPProxy );
221 if ( $this->noProxy ) {
227 if ( $this->proxy ) {
233 if ( self::isLocalURL( $this->url ) ) {
234 if ( $localHTTPProxy !==
false ) {
235 $this->setReverseProxy( $localHTTPProxy );
238 $this->proxy = (string)$httpProxy;
253 $parsedProxy = $this->urlUtils->parse( $proxy );
254 if ( $parsedProxy ===
null ) {
255 throw new InvalidArgumentException(
"Invalid reverseProxy configured: $proxy" );
258 $this->setHeader(
'Host', $this->parsedUrl[
'host'] );
260 $this->parsedUrl[
'scheme'] = $parsedProxy[
'scheme'];
261 $this->parsedUrl[
'host'] = $parsedProxy[
'host'];
262 if ( isset( $parsedProxy[
'port'] ) ) {
263 $this->parsedUrl[
'port'] = $parsedProxy[
'port'];
265 unset( $this->parsedUrl[
'port'] );
267 $this->url = UrlUtils::assemble( $this->parsedUrl );
269 $this->noProxy =
true;
278 private static function isLocalURL(
$url ) {
279 $localVirtualHosts = MediaWikiServices::getInstance()->getMainConfig()->get(
280 MainConfigNames::LocalVirtualHosts );
284 if ( preg_match(
'!^https?://([\w.-]+)[/:].*$!',
$url,
$matches ) ) {
287 $domainParts = explode(
'.', $host );
289 $domainParts = array_reverse( $domainParts );
292 $countParts = count( $domainParts );
293 for ( $i = 0; $i < $countParts; $i++ ) {
294 $domainPart = $domainParts[$i];
296 $domain = $domainPart;
298 $domain = $domainPart .
'.' . $domain;
301 if ( in_array( $domain, $localVirtualHosts ) ) {
314 $this->setHeader(
'User-Agent', $UA );
324 $this->reqHeaders[$name] = $value;
346 $this->doSetCallback( $callback );
357 if ( $callback ===
null ) {
358 $callback = $this->read( ... );
359 } elseif ( !is_callable( $callback ) ) {
360 $this->status->fatal(
'http-internal-error' );
361 throw new InvalidArgumentException( __METHOD__ .
': invalid callback' );
363 $this->callback = $callback;
375 public function read( $fh, $content ) {
376 $this->content .= $content;
377 return strlen( $content );
387 throw new LogicException(
'children must override this' );
393 if ( strtoupper( $this->method ) ==
"HEAD" ) {
394 $this->headersOnly =
true;
399 if ( !$this->callback ) {
400 $this->doSetCallback(
null );
403 if ( !isset( $this->reqHeaders[
'User-Agent'] ) ) {
404 $http = MediaWikiServices::getInstance()->getHttpRequestFactory();
405 $this->setUserAgent( $http->getUserAgent() );
418 if ( !$this->status->isOK() ) {
419 $this->respStatus =
'0 Error';
422 foreach ( $this->headerList as $header ) {
423 if ( preg_match(
"#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
424 $this->respVersion = $match[1];
425 $this->respStatus = $match[2];
426 } elseif ( preg_match(
"#^[ \t]#", $header ) ) {
427 $last = count( $this->respHeaders[$lastname] ) - 1;
428 $this->respHeaders[$lastname][$last] .=
"\r\n$header";
429 } elseif ( preg_match(
"#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
430 $this->respHeaders[strtolower( $match[1] )][] = $match[2];
431 $lastname = strtolower( $match[1] );
435 $this->parseCookies();
446 if ( !$this->respHeaders ) {
447 $this->parseHeader();
450 if ( (
int)$this->respStatus > 0 && (
int)$this->respStatus < 400 ) {
451 $this->status->setResult(
true, (
int)$this->respStatus );
453 [ $code, $message ] = explode(
" ", $this->respStatus, 2 );
454 $this->status->setResult(
false, (
int)$this->respStatus );
455 $this->status->fatal(
"http-bad-status", $code, $message );
467 if ( !$this->respHeaders ) {
468 $this->parseHeader();
471 return (
int)$this->respStatus;
480 if ( !$this->respHeaders ) {
481 $this->parseHeader();
484 $status = (int)$this->respStatus;
486 if ( $status >= 300 && $status <= 303 ) {
503 if ( !$this->respHeaders ) {
504 $this->parseHeader();
507 return $this->respHeaders;
517 if ( !$this->respHeaders ) {
518 $this->parseHeader();
521 if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
522 $v = $this->respHeaders[strtolower( $header )];
523 return $v[count( $v ) - 1];
535 $this->cookieJar = $jar;
544 if ( !$this->respHeaders ) {
545 $this->parseHeader();
548 return $this->cookieJar;
560 public function setCookie( $name, $value, array $attr = [] ) {
563 if ( $this->parsedUrl && !isset( $attr[
'domain'] ) ) {
564 $attr[
'domain'] = $this->parsedUrl[
'host'];
567 $this->cookieJar->
setCookie( $name, $value, $attr );
576 if ( isset( $this->respHeaders[
'set-cookie'] ) ) {
577 $url = parse_url( $this->getFinalUrl() );
578 if ( !isset(
$url[
'host'] ) ) {
579 $this->status->fatal(
'http-invalid-url', $this->getFinalUrl() );
581 foreach ( $this->respHeaders[
'set-cookie'] as $cookie ) {
582 $this->cookieJar->parseCookieResponseHeader( $cookie,
$url[
'host'] );
605 $headers = $this->getResponseHeaders();
608 if ( isset( $headers[
'location'] ) ) {
609 $locations = $headers[
'location'];
611 $foundRelativeURI =
false;
612 $countLocations = count( $locations );
614 for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
615 $url = parse_url( $locations[$i] );
617 if ( isset(
$url[
'scheme'] ) && isset(
$url[
'host'] ) ) {
618 $domain =
$url[
'scheme'] .
'://' .
$url[
'host'];
621 $foundRelativeURI =
true;
625 if ( !$foundRelativeURI ) {
626 return $locations[$countLocations - 1];
629 return $domain . $locations[$countLocations - 1];
631 $url = parse_url( $this->url );
632 if ( isset(
$url[
'scheme'] ) && isset(
$url[
'host'] ) ) {
633 return $url[
'scheme'] .
'://' .
$url[
'host'] .
634 $locations[$countLocations - 1];
663 if ( $originalRequest instanceof
WebRequest ) {
665 'ip' => $originalRequest->getIP(),
666 'userAgent' => $originalRequest->getHeader(
'User-Agent' ),
669 !is_array( $originalRequest )
670 || array_diff( [
'ip',
'userAgent' ], array_keys( $originalRequest ) )
672 throw new InvalidArgumentException( __METHOD__ .
': $originalRequest must be a '
673 .
"WebRequest or an array with 'ip' and 'userAgent' keys" );
676 $this->reqHeaders[
'X-Forwarded-For'] = $originalRequest[
'ip'];
677 $this->reqHeaders[
'X-Original-User-Agent'] = $originalRequest[
'userAgent'];
697 return (
bool)preg_match(
698 '/^https?:\/\/[^\/\s]\S*$/D',
wfIniGetBool( $setting)
Safety wrapper around ini_get() for boolean settings.
Cookie jar to use with MWHttpRequest.
setCookie(string $name, string $value, array $attr)
Set a cookie in the cookie jar.
This wrapper class will call out to curl (if available) or fallback to regular PHP if necessary for h...
getContent()
Get the body, or content, of the response to the request.
setLogger(LoggerInterface $logger)
setCookie( $name, $value, array $attr=[])
Sets a cookie.
getResponseHeaders()
Returns an associative array of response headers after the request has been executed.
doSetCallback( $callback)
Worker function for setting callbacks.
__construct( $url, array $options, $caller=__METHOD__, ?Profiler $profiler=null)
setHeader( $name, $value)
Set an arbitrary header.
getCookieJar()
Returns the cookie jar in use.
setReverseProxy(string $proxy)
Enable use of a reverse proxy in which the hostname is passed as a "Host" header, and the request is ...
setOriginalRequest( $originalRequest)
Set information about the original request.
isRedirect()
Returns true if the last status code was a redirect.
read( $fh, $content)
A generic callback to read the body of the response from a remote server.
getFinalUrl()
Returns the final URL after all redirections.
setStatus()
Sets HTTPRequest status member to a fatal value with the error message if the returned integer value ...
parseHeader()
Parses the headers, including the HTTP status code and any Set-Cookie headers.
canFollowRedirects()
Returns true if the backend can follow redirects.
setCallback( $callback)
Set a read callback to accept data read from the HTTP request.
addTelemetry(TelemetryHeadersInterface $telemetry)
Add Telemetry information to the request.
static canMakeRequests()
Simple function to test if we can make any sort of requests at all, using cURL or fopen()
static isValidURI( $uri)
Check that the given URI is a valid one.
getStatus()
Get the integer value of the HTTP status code (e.g.
const SUPPORTS_FILE_POSTS
execute()
Take care of whatever is necessary to perform the URI request.
getResponseHeader( $header)
Returns the value of the given response header.
proxySetup()
Take care of setting up the proxy (do nothing if "noProxy" is set)
setData(array $args)
Set the parameters of the request.
parseCookies()
Parse the cookies in the response headers and store them in the cookie jar.
setCookieJar(CookieJar $jar)
Tells the MWHttpRequest object to use this pre-loaded CookieJar.
A class containing constants representing the names of configuration variables.
Profiler base class that defines the interface and some shared functionality.
Generic operation result class Has warning/error list, boolean status and arbitrary value.