25use Psr\Log\LoggerAwareInterface;
26use Psr\Log\LoggerInterface;
27use Psr\Log\NullLogger;
103 $url, array $options, $caller = __METHOD__,
Profiler $profiler =
null
105 if ( !array_key_exists(
'timeout', $options )
106 || !array_key_exists(
'connectTimeout', $options ) ) {
107 throw new InvalidArgumentException(
"timeout and connectionTimeout options are required" );
112 $this->logger = $options[
'logger'] ??
new NullLogger();
113 $this->timeout = $options[
'timeout'];
114 $this->connectTimeout = $options[
'connectTimeout'];
116 if ( !$this->parsedUrl || !self::isValidURI( $this->url ) ) {
117 $this->status = StatusValue::newFatal(
'http-invalid-url',
$url );
119 $this->status = StatusValue::newGood( 100 );
122 if ( isset( $options[
'userAgent'] ) ) {
125 if ( isset( $options[
'username'] ) && isset( $options[
'password'] ) ) {
128 'Basic ' . base64_encode( $options[
'username'] .
':' . $options[
'password'] )
131 if ( isset( $options[
'originalRequest'] ) ) {
135 $members = [
"postData",
"proxy",
"noProxy",
"sslVerifyHost",
"caInfo",
136 "method",
"followRedirects",
"maxRedirects",
"sslVerifyCert",
"callback" ];
138 foreach ( $members as $o ) {
139 if ( isset( $options[$o] ) ) {
142 if ( $o ==
'method' ) {
144 $options[$o] = strtoupper( $options[$o] );
146 $this->$o = $options[$o];
150 if ( $this->noProxy ) {
156 $this->profileName = $caller;
163 $this->logger = $logger;
172 return function_exists(
'curl_init' ) ||
wfIniGetBool(
'allow_url_fopen' );
181 return $this->content;
191 $this->postData = $args;
201 foreach ( $telemetry->getRequestHeaders() as
$header => $value ) {
212 $httpProxy = MediaWikiServices::getInstance()->getMainConfig()->get(
213 MainConfigNames::HTTPProxy );
214 $localHTTPProxy = MediaWikiServices::getInstance()->getMainConfig()->get(
215 MainConfigNames::LocalHTTPProxy );
217 if ( $this->noProxy ) {
223 if ( $this->proxy ) {
229 if ( self::isLocalURL( $this->url ) ) {
230 if ( $localHTTPProxy !==
false ) {
231 $this->setReverseProxy( $localHTTPProxy );
234 $this->proxy = (string)$httpProxy;
250 if ( $parsedProxy ===
false ) {
251 throw new InvalidArgumentException(
"Invalid reverseProxy configured: $proxy" );
254 $this->setHeader(
'Host', $this->parsedUrl[
'host'] );
256 $this->parsedUrl[
'scheme'] = $parsedProxy[
'scheme'];
257 $this->parsedUrl[
'host'] = $parsedProxy[
'host'];
258 if ( isset( $parsedProxy[
'port'] ) ) {
259 $this->parsedUrl[
'port'] = $parsedProxy[
'port'];
261 unset( $this->parsedUrl[
'port'] );
265 $this->noProxy =
true;
274 private static function isLocalURL( $url ) {
278 $localVirtualHosts = MediaWikiServices::getInstance()->getMainConfig()->get(
279 MainConfigNames::LocalVirtualHosts );
283 if ( preg_match(
'!^https?://([\w.-]+)[/:].*$!', $url,
$matches ) ) {
286 $domainParts = explode(
'.', $host );
288 $domainParts = array_reverse( $domainParts );
291 $countParts = count( $domainParts );
292 for ( $i = 0; $i < $countParts; $i++ ) {
293 $domainPart = $domainParts[$i];
295 $domain = $domainPart;
297 $domain = $domainPart .
'.' . $domain;
300 if ( in_array( $domain, $localVirtualHosts ) ) {
313 $this->setHeader(
'User-Agent', $UA );
323 $this->reqHeaders[$name] = $value;
333 if ( $this->cookieJar ) {
334 $this->reqHeaders[
'Cookie'] =
335 $this->cookieJar->serializeToHttpRequest(
336 $this->parsedUrl[
'path'],
337 $this->parsedUrl[
'host']
341 foreach ( $this->reqHeaders as $name => $value ) {
342 $list[] =
"$name: $value";
367 $this->doSetCallback( $callback );
378 if ( $callback ===
null ) {
379 $callback = [ $this,
'read' ];
380 } elseif ( !is_callable( $callback ) ) {
381 $this->status->fatal(
'http-internal-error' );
382 throw new InvalidArgumentException( __METHOD__ .
': invalid callback' );
384 $this->callback = $callback;
396 public function read( $fh, $content ) {
397 $this->content .= $content;
398 return strlen( $content );
408 throw new LogicException(
'children must override this' );
414 if ( strtoupper( $this->method ) ==
"HEAD" ) {
415 $this->headersOnly =
true;
420 if ( !$this->callback ) {
421 $this->doSetCallback(
null );
424 if ( !isset( $this->reqHeaders[
'User-Agent'] ) ) {
425 $http = MediaWikiServices::getInstance()->getHttpRequestFactory();
426 $this->setUserAgent( $http->getUserAgent() );
439 if ( !$this->status->isOK() ) {
440 $this->respStatus =
'0 Error';
443 foreach ( $this->headerList as
$header ) {
444 if ( preg_match(
"#^HTTP/([0-9.]+) (.*)#",
$header, $match ) ) {
445 $this->respVersion = $match[1];
446 $this->respStatus = $match[2];
447 } elseif ( preg_match(
"#^[ \t]#",
$header ) ) {
448 $last = count( $this->respHeaders[$lastname] ) - 1;
449 $this->respHeaders[$lastname][$last] .=
"\r\n$header";
450 } elseif ( preg_match(
"#^([^:]*):[\t ]*(.*)#",
$header, $match ) ) {
451 $this->respHeaders[strtolower( $match[1] )][] = $match[2];
452 $lastname = strtolower( $match[1] );
456 $this->parseCookies();
467 if ( !$this->respHeaders ) {
468 $this->parseHeader();
471 if ( (
int)$this->respStatus > 0 && (
int)$this->respStatus < 400 ) {
472 $this->status->setResult(
true, (
int)$this->respStatus );
474 [ $code, $message ] = explode(
" ", $this->respStatus, 2 );
475 $this->status->setResult(
false, (
int)$this->respStatus );
476 $this->status->fatal(
"http-bad-status", $code, $message );
488 if ( !$this->respHeaders ) {
489 $this->parseHeader();
492 return (
int)$this->respStatus;
501 if ( !$this->respHeaders ) {
502 $this->parseHeader();
505 $status = (int)$this->respStatus;
507 if ( $status >= 300 && $status <= 303 ) {
524 if ( !$this->respHeaders ) {
525 $this->parseHeader();
528 return $this->respHeaders;
538 if ( !$this->respHeaders ) {
539 $this->parseHeader();
542 if ( isset( $this->respHeaders[strtolower(
$header )] ) ) {
543 $v = $this->respHeaders[strtolower(
$header )];
544 return $v[count( $v ) - 1];
558 $this->cookieJar = $jar;
567 if ( !$this->respHeaders ) {
568 $this->parseHeader();
571 return $this->cookieJar;
583 public function setCookie( $name, $value, array $attr = [] ) {
584 if ( !$this->cookieJar ) {
588 if ( $this->parsedUrl && !isset( $attr[
'domain'] ) ) {
589 $attr[
'domain'] = $this->parsedUrl[
'host'];
592 $this->cookieJar->
setCookie( $name, $value, $attr );
599 if ( !$this->cookieJar ) {
603 if ( isset( $this->respHeaders[
'set-cookie'] ) ) {
604 $url = parse_url( $this->getFinalUrl() );
605 if ( !isset( $url[
'host'] ) ) {
606 $this->status->fatal(
'http-invalid-url', $url );
608 foreach ( $this->respHeaders[
'set-cookie'] as $cookie ) {
609 $this->cookieJar->parseCookieResponseHeader( $cookie, $url[
'host'] );
632 $headers = $this->getResponseHeaders();
635 if ( isset( $headers[
'location'] ) ) {
636 $locations = $headers[
'location'];
638 $foundRelativeURI =
false;
639 $countLocations = count( $locations );
641 for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
642 $url = parse_url( $locations[$i] );
644 if ( isset( $url[
'scheme'] ) && isset( $url[
'host'] ) ) {
645 $domain = $url[
'scheme'] .
'://' . $url[
'host'];
648 $foundRelativeURI =
true;
652 if ( !$foundRelativeURI ) {
653 return $locations[$countLocations - 1];
656 return $domain . $locations[$countLocations - 1];
658 $url = parse_url( $this->url );
659 if ( isset( $url[
'scheme'] ) && isset( $url[
'host'] ) ) {
660 return $url[
'scheme'] .
'://' . $url[
'host'] .
661 $locations[$countLocations - 1];
690 if ( $originalRequest instanceof
WebRequest ) {
692 'ip' => $originalRequest->getIP(),
693 'userAgent' => $originalRequest->getHeader(
'User-Agent' ),
696 !is_array( $originalRequest )
697 || array_diff( [
'ip',
'userAgent' ], array_keys( $originalRequest ) )
699 throw new InvalidArgumentException( __METHOD__ .
': $originalRequest must be a '
700 .
"WebRequest or an array with 'ip' and 'userAgent' keys" );
703 $this->reqHeaders[
'X-Forwarded-For'] = $originalRequest[
'ip'];
704 $this->reqHeaders[
'X-Original-User-Agent'] = $originalRequest[
'userAgent'];
724 return (
bool)preg_match(
725 '/^https?:\/\/[^\/\s]\S*$/D',
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
wfIniGetBool( $setting)
Safety wrapper around ini_get() for boolean settings.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL using $wgServer (or one of its alternatives).
wfAssembleUrl( $urlParts)
This function will reassemble a URL parsed with wfParseURL.
Cookie jar to use with MWHttpRequest.
setCookie( $name, $value, $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.
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.
__construct( $url, array $options, $caller=__METHOD__, Profiler $profiler=null)
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.
getHeaderList()
Get an array of the headers.
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.