MediaWiki REL1_37
MWHttpRequest.php
Go to the documentation of this file.
1<?php
22use Psr\Log\LoggerAwareInterface;
23use Psr\Log\LoggerInterface;
24use Psr\Log\NullLogger;
25
33abstract class MWHttpRequest implements LoggerAwareInterface {
34 public const SUPPORTS_FILE_POSTS = false;
35
39 protected $timeout = 'default';
40
41 protected $content;
42 protected $headersOnly = null;
43 protected $postData = null;
44 protected $proxy = null;
45 protected $noProxy = false;
46 protected $sslVerifyHost = true;
47 protected $sslVerifyCert = true;
48 protected $caInfo = null;
49 protected $method = "GET";
51 protected $reqHeaders = [];
52 protected $url;
53 protected $parsedUrl;
55 protected $callback;
56 protected $maxRedirects = 5;
57 protected $followRedirects = false;
58 protected $connectTimeout;
59
63 protected $cookieJar;
64
65 protected $headerList = [];
66 protected $respVersion = "0.9";
67 protected $respStatus = "200 Ok";
69 protected $respHeaders = [];
70
72 protected $status;
73
77 protected $profiler;
78
82 protected $profileName;
83
87 protected $logger;
88
98 public function __construct(
99 $url, array $options = [], $caller = __METHOD__, Profiler $profiler = null
100 ) {
101 $this->url = wfExpandUrl( $url, PROTO_HTTP );
102 $this->parsedUrl = wfParseUrl( $this->url );
103
104 $this->logger = $options['logger'] ?? new NullLogger();
105
106 if ( !$this->parsedUrl || !self::isValidURI( $this->url ) ) {
107 $this->status = StatusValue::newFatal( 'http-invalid-url', $url );
108 } else {
109 $this->status = StatusValue::newGood( 100 ); // continue
110 }
111
112 if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) {
113 $this->timeout = $options['timeout'];
114 } else {
115 // The timeout should always be set by HttpRequestFactory, so this
116 // should only happen if the class was directly constructed
117 wfDeprecated( __METHOD__ . ' without the timeout option', '1.35' );
118 global $wgHTTPTimeout;
119 $this->timeout = $wgHTTPTimeout;
120 }
121 if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) {
122 $this->connectTimeout = $options['connectTimeout'];
123 } else {
124 // The timeout should always be set by HttpRequestFactory, so this
125 // should only happen if the class was directly constructed
126 wfDeprecated( __METHOD__ . ' without the connectTimeout option', '1.35' );
128 $this->connectTimeout = $wgHTTPConnectTimeout;
129 }
130 if ( isset( $options['userAgent'] ) ) {
131 $this->setUserAgent( $options['userAgent'] );
132 }
133 if ( isset( $options['username'] ) && isset( $options['password'] ) ) {
134 $this->setHeader(
135 'Authorization',
136 'Basic ' . base64_encode( $options['username'] . ':' . $options['password'] )
137 );
138 }
139 if ( isset( $options['originalRequest'] ) ) {
140 $this->setOriginalRequest( $options['originalRequest'] );
141 }
142
143 $this->setHeader( 'X-Request-Id', WebRequest::getRequestId() );
144
145 $members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
146 "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ];
147
148 foreach ( $members as $o ) {
149 if ( isset( $options[$o] ) ) {
150 // ensure that MWHttpRequest::method is always
151 // uppercased. T38137
152 if ( $o == 'method' ) {
153 $options[$o] = strtoupper( $options[$o] );
154 }
155 $this->$o = $options[$o];
156 }
157 }
158
159 if ( $this->noProxy ) {
160 $this->proxy = ''; // noProxy takes precedence
161 }
162
163 // Profile based on what's calling us
164 $this->profiler = $profiler;
165 $this->profileName = $caller;
166 }
167
171 public function setLogger( LoggerInterface $logger ) {
172 $this->logger = $logger;
173 }
174
180 public static function canMakeRequests() {
181 return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
182 }
183
194 public static function factory( $url, array $options = null, $caller = __METHOD__ ) {
195 if ( $options === null ) {
196 $options = [];
197 }
198 return MediaWikiServices::getInstance()->getHttpRequestFactory()
199 ->create( $url, $options, $caller );
200 }
201
207 public function getContent() {
208 return $this->content;
209 }
210
217 public function setData( array $args ) {
218 $this->postData = $args;
219 }
220
226 protected function proxySetup() {
227 // If there is an explicit proxy set and proxies are not disabled, then use it
228 if ( $this->proxy && !$this->noProxy ) {
229 return;
230 }
231
232 // Otherwise, fallback to $wgHTTPProxy if this is not a machine
233 // local URL and proxies are not disabled
234 if ( self::isLocalURL( $this->url ) || $this->noProxy ) {
235 $this->proxy = '';
236 } else {
237 global $wgHTTPProxy;
238 $this->proxy = (string)$wgHTTPProxy;
239 }
240 }
241
248 private static function isLocalURL( $url ) {
250
251 if ( $wgCommandLineMode ) {
252 return false;
253 }
254
255 // Extract host part
256 $matches = [];
257 if ( preg_match( '!^https?://([\w.-]+)[/:].*$!', $url, $matches ) ) {
258 $host = $matches[1];
259 // Split up dotwise
260 $domainParts = explode( '.', $host );
261 // Check if this domain or any superdomain is listed as a local virtual host
262 $domainParts = array_reverse( $domainParts );
263
264 $domain = '';
265 $countParts = count( $domainParts );
266 for ( $i = 0; $i < $countParts; $i++ ) {
267 $domainPart = $domainParts[$i];
268 if ( $i == 0 ) {
269 $domain = $domainPart;
270 } else {
271 $domain = $domainPart . '.' . $domain;
272 }
273
274 if ( in_array( $domain, $wgLocalVirtualHosts ) ) {
275 return true;
276 }
277 }
278 }
279
280 return false;
281 }
282
286 public function setUserAgent( $UA ) {
287 $this->setHeader( 'User-Agent', $UA );
288 }
289
295 public function setHeader( $name, $value ) {
296 // I feel like I should normalize the case here...
297 $this->reqHeaders[$name] = $value;
298 }
299
304 protected function getHeaderList() {
305 $list = [];
306
307 if ( $this->cookieJar ) {
308 $this->reqHeaders['Cookie'] =
309 $this->cookieJar->serializeToHttpRequest(
310 $this->parsedUrl['path'],
311 $this->parsedUrl['host']
312 );
313 }
314
315 foreach ( $this->reqHeaders as $name => $value ) {
316 $list[] = "$name: $value";
317 }
318
319 return $list;
320 }
321
340 public function setCallback( $callback ) {
341 $this->doSetCallback( $callback );
342 }
343
351 protected function doSetCallback( $callback ) {
352 if ( $callback === null ) {
353 $callback = [ $this, 'read' ];
354 } elseif ( !is_callable( $callback ) ) {
355 $this->status->fatal( 'http-internal-error' );
356 throw new InvalidArgumentException( __METHOD__ . ': invalid callback' );
357 }
358 $this->callback = $callback;
359 }
360
370 public function read( $fh, $content ) {
371 $this->content .= $content;
372 return strlen( $content );
373 }
374
381 public function execute() {
382 throw new LogicException( 'children must override this' );
383 }
384
385 protected function prepare() {
386 $this->content = "";
387
388 if ( strtoupper( $this->method ) == "HEAD" ) {
389 $this->headersOnly = true;
390 }
391
392 $this->proxySetup(); // set up any proxy as needed
393
394 if ( !$this->callback ) {
395 $this->doSetCallback( null );
396 }
397
398 if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
399 $http = MediaWikiServices::getInstance()->getHttpRequestFactory();
400 $this->setUserAgent( $http->getUserAgent() );
401 }
402 }
403
409 protected function parseHeader() {
410 $lastname = "";
411
412 // Failure without (valid) headers gets a response status of zero
413 if ( !$this->status->isOK() ) {
414 $this->respStatus = '0 Error';
415 }
416
417 foreach ( $this->headerList as $header ) {
418 if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
419 $this->respVersion = $match[1];
420 $this->respStatus = $match[2];
421 } elseif ( preg_match( "#^[ \t]#", $header ) ) {
422 $last = count( $this->respHeaders[$lastname] ) - 1;
423 $this->respHeaders[$lastname][$last] .= "\r\n$header";
424 } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
425 $this->respHeaders[strtolower( $match[1] )][] = $match[2];
426 $lastname = strtolower( $match[1] );
427 }
428 }
429
430 $this->parseCookies();
431 }
432
440 protected function setStatus() {
441 if ( !$this->respHeaders ) {
442 $this->parseHeader();
443 }
444
445 if ( (int)$this->respStatus > 0 && (int)$this->respStatus < 400 ) {
446 $this->status->setResult( true, (int)$this->respStatus );
447 } else {
448 list( $code, $message ) = explode( " ", $this->respStatus, 2 );
449 $this->status->setResult( false, (int)$this->respStatus );
450 $this->status->fatal( "http-bad-status", $code, $message );
451 }
452 }
453
461 public function getStatus() {
462 if ( !$this->respHeaders ) {
463 $this->parseHeader();
464 }
465
466 return (int)$this->respStatus;
467 }
468
474 public function isRedirect() {
475 if ( !$this->respHeaders ) {
476 $this->parseHeader();
477 }
478
479 $status = (int)$this->respStatus;
480
481 if ( $status >= 300 && $status <= 303 ) {
482 return true;
483 }
484
485 return false;
486 }
487
497 public function getResponseHeaders() {
498 if ( !$this->respHeaders ) {
499 $this->parseHeader();
500 }
501
502 return $this->respHeaders;
503 }
504
511 public function getResponseHeader( $header ) {
512 if ( !$this->respHeaders ) {
513 $this->parseHeader();
514 }
515
516 if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
517 $v = $this->respHeaders[strtolower( $header )];
518 return $v[count( $v ) - 1];
519 }
520
521 return null;
522 }
523
531 public function setCookieJar( CookieJar $jar ) {
532 $this->cookieJar = $jar;
533 }
534
540 public function getCookieJar() {
541 if ( !$this->respHeaders ) {
542 $this->parseHeader();
543 }
544
545 return $this->cookieJar;
546 }
547
557 public function setCookie( $name, $value, array $attr = [] ) {
558 if ( !$this->cookieJar ) {
559 $this->cookieJar = new CookieJar;
560 }
561
562 if ( $this->parsedUrl && !isset( $attr['domain'] ) ) {
563 $attr['domain'] = $this->parsedUrl['host'];
564 }
565
566 $this->cookieJar->setCookie( $name, $value, $attr );
567 }
568
572 protected function parseCookies() {
573 if ( !$this->cookieJar ) {
574 $this->cookieJar = new CookieJar;
575 }
576
577 if ( isset( $this->respHeaders['set-cookie'] ) ) {
578 $url = parse_url( $this->getFinalUrl() );
579 foreach ( $this->respHeaders['set-cookie'] as $cookie ) {
580 $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
581 }
582 }
583 }
584
601 public function getFinalUrl() {
602 $headers = $this->getResponseHeaders();
603
604 // return full url (fix for incorrect but handled relative location)
605 if ( isset( $headers['location'] ) ) {
606 $locations = $headers['location'];
607 $domain = '';
608 $foundRelativeURI = false;
609 $countLocations = count( $locations );
610
611 for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
612 $url = parse_url( $locations[$i] );
613
614 if ( isset( $url['host'] ) ) {
615 $domain = $url['scheme'] . '://' . $url['host'];
616 break; // found correct URI (with host)
617 } else {
618 $foundRelativeURI = true;
619 }
620 }
621
622 if ( !$foundRelativeURI ) {
623 return $locations[$countLocations - 1];
624 }
625 if ( $domain ) {
626 return $domain . $locations[$countLocations - 1];
627 }
628 $url = parse_url( $this->url );
629 if ( isset( $url['host'] ) ) {
630 return $url['scheme'] . '://' . $url['host'] .
631 $locations[$countLocations - 1];
632 }
633 }
634
635 return $this->url;
636 }
637
643 public function canFollowRedirects() {
644 return true;
645 }
646
659 public function setOriginalRequest( $originalRequest ) {
660 if ( $originalRequest instanceof WebRequest ) {
661 $originalRequest = [
662 'ip' => $originalRequest->getIP(),
663 'userAgent' => $originalRequest->getHeader( 'User-Agent' ),
664 ];
665 } elseif (
666 !is_array( $originalRequest )
667 || array_diff( [ 'ip', 'userAgent' ], array_keys( $originalRequest ) )
668 ) {
669 throw new InvalidArgumentException( __METHOD__ . ': $originalRequest must be a '
670 . "WebRequest or an array with 'ip' and 'userAgent' keys" );
671 }
672
673 $this->reqHeaders['X-Forwarded-For'] = $originalRequest['ip'];
674 $this->reqHeaders['X-Original-User-Agent'] = $originalRequest['userAgent'];
675 }
676
693 public static function isValidURI( $uri ) {
694 return (bool)preg_match(
695 '/^https?:\/\/[^\/\s]\S*$/D',
696 $uri
697 );
698 }
699}
$wgHTTPProxy
Proxy to use for CURL requests.
float int $wgHTTPTimeout
Timeout for HTTP requests done internally, in seconds.
$wgLocalVirtualHosts
Local virtual hosts.
float int $wgHTTPConnectTimeout
Timeout for connections done internally (in seconds).
const PROTO_HTTP
Definition Defines.php:192
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.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
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 ...
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.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Profiler base class that defines the interface and some shared functionality.
Definition Profiler.php:36
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...
if( $line===false) $args
Definition mcc.php:124
$content
Definition router.php:76
$header