MediaWiki REL1_34
MWHttpRequest.php
Go to the documentation of this file.
1<?php
21use Psr\Log\LoggerInterface;
22use Psr\Log\LoggerAwareInterface;
23use Psr\Log\NullLogger;
24
32abstract class MWHttpRequest implements LoggerAwareInterface {
33 const SUPPORTS_FILE_POSTS = false;
34
38 protected $timeout = 'default';
39
40 protected $content;
41 protected $headersOnly = null;
42 protected $postData = null;
43 protected $proxy = null;
44 protected $noProxy = false;
45 protected $sslVerifyHost = true;
46 protected $sslVerifyCert = true;
47 protected $caInfo = null;
48 protected $method = "GET";
50 protected $reqHeaders = [];
51 protected $url;
52 protected $parsedUrl;
54 protected $callback;
55 protected $maxRedirects = 5;
56 protected $followRedirects = false;
57 protected $connectTimeout;
58
62 protected $cookieJar;
63
64 protected $headerList = [];
65 protected $respVersion = "0.9";
66 protected $respStatus = "200 Ok";
68 protected $respHeaders = [];
69
71 protected $status;
72
76 protected $profiler;
77
81 protected $profileName;
82
86 protected $logger;
87
98 public function __construct(
99 $url, array $options = [], $caller = __METHOD__, Profiler $profiler = null
100 ) {
102
103 $this->url = wfExpandUrl( $url, PROTO_HTTP );
104 $this->parsedUrl = wfParseUrl( $this->url );
105
106 $this->logger = $options['logger'] ?? new NullLogger();
107
108 if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) {
109 $this->status = StatusValue::newFatal( 'http-invalid-url', $url );
110 } else {
111 $this->status = StatusValue::newGood( 100 ); // continue
112 }
113
114 if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) {
115 $this->timeout = $options['timeout'];
116 } else {
117 $this->timeout = $wgHTTPTimeout;
118 }
119 if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) {
120 $this->connectTimeout = $options['connectTimeout'];
121 } else {
122 $this->connectTimeout = $wgHTTPConnectTimeout;
123 }
124 if ( isset( $options['userAgent'] ) ) {
125 $this->setUserAgent( $options['userAgent'] );
126 }
127 if ( isset( $options['username'] ) && isset( $options['password'] ) ) {
128 $this->setHeader(
129 'Authorization',
130 'Basic ' . base64_encode( $options['username'] . ':' . $options['password'] )
131 );
132 }
133 if ( isset( $options['originalRequest'] ) ) {
134 $this->setOriginalRequest( $options['originalRequest'] );
135 }
136
137 $this->setHeader( 'X-Request-Id', WebRequest::getRequestId() );
138
139 $members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
140 "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ];
141
142 foreach ( $members as $o ) {
143 if ( isset( $options[$o] ) ) {
144 // ensure that MWHttpRequest::method is always
145 // uppercased. T38137
146 if ( $o == 'method' ) {
147 // @phan-suppress-next-line PhanTypeInvalidDimOffset
148 $options[$o] = strtoupper( $options[$o] );
149 }
150 $this->$o = $options[$o];
151 }
152 }
153
154 if ( $this->noProxy ) {
155 $this->proxy = ''; // noProxy takes precedence
156 }
157
158 // Profile based on what's calling us
159 $this->profiler = $profiler;
160 $this->profileName = $caller;
161 }
162
166 public function setLogger( LoggerInterface $logger ) {
167 $this->logger = $logger;
168 }
169
175 public static function canMakeRequests() {
176 return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
177 }
178
189 public static function factory( $url, array $options = null, $caller = __METHOD__ ) {
190 if ( $options === null ) {
191 $options = [];
192 }
193 return \MediaWiki\MediaWikiServices::getInstance()
194 ->getHttpRequestFactory()
195 ->create( $url, $options, $caller );
196 }
197
203 public function getContent() {
204 return $this->content;
205 }
206
213 public function setData( array $args ) {
214 $this->postData = $args;
215 }
216
222 protected function proxySetup() {
223 // If there is an explicit proxy set and proxies are not disabled, then use it
224 if ( $this->proxy && !$this->noProxy ) {
225 return;
226 }
227
228 // Otherwise, fallback to $wgHTTPProxy if this is not a machine
229 // local URL and proxies are not disabled
230 if ( self::isLocalURL( $this->url ) || $this->noProxy ) {
231 $this->proxy = '';
232 } else {
233 global $wgHTTPProxy;
234 $this->proxy = (string)$wgHTTPProxy;
235 }
236 }
237
244 private static function isLocalURL( $url ) {
246
247 if ( $wgCommandLineMode ) {
248 return false;
249 }
250
251 // Extract host part
252 $matches = [];
253 if ( preg_match( '!^https?://([\w.-]+)[/:].*$!', $url, $matches ) ) {
254 $host = $matches[1];
255 // Split up dotwise
256 $domainParts = explode( '.', $host );
257 // Check if this domain or any superdomain is listed as a local virtual host
258 $domainParts = array_reverse( $domainParts );
259
260 $domain = '';
261 $countParts = count( $domainParts );
262 for ( $i = 0; $i < $countParts; $i++ ) {
263 $domainPart = $domainParts[$i];
264 if ( $i == 0 ) {
265 $domain = $domainPart;
266 } else {
267 $domain = $domainPart . '.' . $domain;
268 }
269
270 if ( in_array( $domain, $wgLocalVirtualHosts ) ) {
271 return true;
272 }
273 }
274 }
275
276 return false;
277 }
278
283 public function setUserAgent( $UA ) {
284 $this->setHeader( 'User-Agent', $UA );
285 }
286
292 public function setHeader( $name, $value ) {
293 // I feel like I should normalize the case here...
294 $this->reqHeaders[$name] = $value;
295 }
296
301 protected function getHeaderList() {
302 $list = [];
303
304 if ( $this->cookieJar ) {
305 $this->reqHeaders['Cookie'] =
306 $this->cookieJar->serializeToHttpRequest(
307 $this->parsedUrl['path'],
308 $this->parsedUrl['host']
309 );
310 }
311
312 foreach ( $this->reqHeaders as $name => $value ) {
313 $list[] = "$name: $value";
314 }
315
316 return $list;
317 }
318
337 public function setCallback( $callback ) {
338 return $this->doSetCallback( $callback );
339 }
340
348 protected function doSetCallback( $callback ) {
349 if ( is_null( $callback ) ) {
350 $callback = [ $this, 'read' ];
351 } elseif ( !is_callable( $callback ) ) {
352 $this->status->fatal( 'http-internal-error' );
353 throw new InvalidArgumentException( __METHOD__ . ': invalid callback' );
354 }
355 $this->callback = $callback;
356 }
357
367 public function read( $fh, $content ) {
368 $this->content .= $content;
369 return strlen( $content );
370 }
371
378 public function execute() {
379 throw new LogicException( 'children must override this' );
380 }
381
382 protected function prepare() {
383 $this->content = "";
384
385 if ( strtoupper( $this->method ) == "HEAD" ) {
386 $this->headersOnly = true;
387 }
388
389 $this->proxySetup(); // set up any proxy as needed
390
391 if ( !$this->callback ) {
392 $this->doSetCallback( null );
393 }
394
395 if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
396 $this->setUserAgent( Http::userAgent() );
397 }
398 }
399
405 protected function parseHeader() {
406 $lastname = "";
407
408 // Failure without (valid) headers gets a response status of zero
409 if ( !$this->status->isOK() ) {
410 $this->respStatus = '0 Error';
411 }
412
413 foreach ( $this->headerList as $header ) {
414 if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
415 $this->respVersion = $match[1];
416 $this->respStatus = $match[2];
417 } elseif ( preg_match( "#^[ \t]#", $header ) ) {
418 $last = count( $this->respHeaders[$lastname] ) - 1;
419 $this->respHeaders[$lastname][$last] .= "\r\n$header";
420 } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
421 $this->respHeaders[strtolower( $match[1] )][] = $match[2];
422 $lastname = strtolower( $match[1] );
423 }
424 }
425
426 $this->parseCookies();
427 }
428
436 protected function setStatus() {
437 if ( !$this->respHeaders ) {
438 $this->parseHeader();
439 }
440
441 if ( ( (int)$this->respStatus > 0 && (int)$this->respStatus < 400 ) ) {
442 $this->status->setResult( true, (int)$this->respStatus );
443 } else {
444 list( $code, $message ) = explode( " ", $this->respStatus, 2 );
445 $this->status->setResult( false, (int)$this->respStatus );
446 $this->status->fatal( "http-bad-status", $code, $message );
447 }
448 }
449
457 public function getStatus() {
458 if ( !$this->respHeaders ) {
459 $this->parseHeader();
460 }
461
462 return (int)$this->respStatus;
463 }
464
470 public function isRedirect() {
471 if ( !$this->respHeaders ) {
472 $this->parseHeader();
473 }
474
475 $status = (int)$this->respStatus;
476
477 if ( $status >= 300 && $status <= 303 ) {
478 return true;
479 }
480
481 return false;
482 }
483
493 public function getResponseHeaders() {
494 if ( !$this->respHeaders ) {
495 $this->parseHeader();
496 }
497
498 return $this->respHeaders;
499 }
500
507 public function getResponseHeader( $header ) {
508 if ( !$this->respHeaders ) {
509 $this->parseHeader();
510 }
511
512 if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
513 $v = $this->respHeaders[strtolower( $header )];
514 return $v[count( $v ) - 1];
515 }
516
517 return null;
518 }
519
527 public function setCookieJar( CookieJar $jar ) {
528 $this->cookieJar = $jar;
529 }
530
536 public function getCookieJar() {
537 if ( !$this->respHeaders ) {
538 $this->parseHeader();
539 }
540
541 return $this->cookieJar;
542 }
543
553 public function setCookie( $name, $value, array $attr = [] ) {
554 if ( !$this->cookieJar ) {
555 $this->cookieJar = new CookieJar;
556 }
557
558 if ( $this->parsedUrl && !isset( $attr['domain'] ) ) {
559 $attr['domain'] = $this->parsedUrl['host'];
560 }
561
562 $this->cookieJar->setCookie( $name, $value, $attr );
563 }
564
568 protected function parseCookies() {
569 if ( !$this->cookieJar ) {
570 $this->cookieJar = new CookieJar;
571 }
572
573 if ( isset( $this->respHeaders['set-cookie'] ) ) {
574 $url = parse_url( $this->getFinalUrl() );
575 foreach ( $this->respHeaders['set-cookie'] as $cookie ) {
576 $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
577 }
578 }
579 }
580
597 public function getFinalUrl() {
598 $headers = $this->getResponseHeaders();
599
600 // return full url (fix for incorrect but handled relative location)
601 if ( isset( $headers['location'] ) ) {
602 $locations = $headers['location'];
603 $domain = '';
604 $foundRelativeURI = false;
605 $countLocations = count( $locations );
606
607 for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
608 $url = parse_url( $locations[$i] );
609
610 if ( isset( $url['host'] ) ) {
611 $domain = $url['scheme'] . '://' . $url['host'];
612 break; // found correct URI (with host)
613 } else {
614 $foundRelativeURI = true;
615 }
616 }
617
618 if ( !$foundRelativeURI ) {
619 return $locations[$countLocations - 1];
620 }
621 if ( $domain ) {
622 return $domain . $locations[$countLocations - 1];
623 }
624 $url = parse_url( $this->url );
625 if ( isset( $url['host'] ) ) {
626 return $url['scheme'] . '://' . $url['host'] .
627 $locations[$countLocations - 1];
628 }
629 }
630
631 return $this->url;
632 }
633
639 public function canFollowRedirects() {
640 return true;
641 }
642
655 public function setOriginalRequest( $originalRequest ) {
656 if ( $originalRequest instanceof WebRequest ) {
657 $originalRequest = [
658 'ip' => $originalRequest->getIP(),
659 'userAgent' => $originalRequest->getHeader( 'User-Agent' ),
660 ];
661 } elseif (
662 !is_array( $originalRequest )
663 || array_diff( [ 'ip', 'userAgent' ], array_keys( $originalRequest ) )
664 ) {
665 throw new InvalidArgumentException( __METHOD__ . ': $originalRequest must be a '
666 . "WebRequest or an array with 'ip' and 'userAgent' keys" );
667 }
668
669 $this->reqHeaders['X-Forwarded-For'] = $originalRequest['ip'];
670 $this->reqHeaders['X-Original-User-Agent'] = $originalRequest['userAgent'];
671 }
672
689 public static function isValidURI( $uri ) {
690 return (bool)preg_match(
691 '/^https?:\/\/[^\/\s]\S*$/D',
692 $uri
693 );
694 }
695}
int $wgHTTPTimeout
Timeout for HTTP requests done internally, in seconds.
$wgHTTPConnectTimeout
Timeout for connections done internally (in seconds) Only works for curl.
$wgHTTPProxy
Proxy to use for CURL requests.
$wgLocalVirtualHosts
Local virtual hosts.
global $wgCommandLineMode
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.
if( $line===false) $args
Definition cdb.php:64
Cookie jar to use with MWHttpRequest.
Definition CookieJar.php:25
setCookie( $name, $value, $attr)
Set a cookie in the cookie jar.
Definition CookieJar.php:36
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.
static isLocalURL( $url)
Check if the URL can be served by localhost.
doSetCallback( $callback)
Worker function for setting callbacks.
setHeader( $name, $value)
Set an arbitrary header.
getCookieJar()
Returns the cookie jar in use.
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 ...
setUserAgent( $UA)
Set the user agent.
parseHeader()
Parses the headers, including the HTTP status code and any Set-Cookie headers.
canFollowRedirects()
Returns true if the backend can follow redirects.
__construct( $url, array $options=[], $caller=__METHOD__, Profiler $profiler=null)
setCallback( $callback)
Set a read callback to accept data read from the HTTP request.
static canMakeRequests()
Simple function to test if we can make any sort of requests at all, using cURL or fopen()
static factory( $url, array $options=null, $caller=__METHOD__)
Generate a new request object.
static isValidURI( $uri)
Check that the given URI is a valid one.
getStatus()
Get the integer value of the HTTP status code (e.g.
StatusValue $status
const SUPPORTS_FILE_POSTS
CookieJar $cookieJar
string[][] $respHeaders
execute()
Take care of whatever is necessary to perform the URI request.
getResponseHeader( $header)
Returns the value of the given response header.
LoggerInterface $logger
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.
int string $timeout
getHeaderList()
Get an array of the headers.
callable $callback
Profiler $profiler
setCookieJar(CookieJar $jar)
Tells the MWHttpRequest object to use this pre-loaded CookieJar.
Profiler base class that defines the interface and some trivial functionality.
Definition Profiler.php:33
Generic operation result class Has warning/error list, boolean status and arbitrary value.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
const PROTO_HTTP
Definition Defines.php:208
$fh
$last
$content
Definition router.php:78
$header