MediaWiki REL1_35
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
99 public function __construct(
100 $url, array $options = [], $caller = __METHOD__, Profiler $profiler = null
101 ) {
102 $this->url = wfExpandUrl( $url, PROTO_HTTP );
103 $this->parsedUrl = wfParseUrl( $this->url );
104
105 $this->logger = $options['logger'] ?? new NullLogger();
106
107 if ( !$this->parsedUrl || !self::isValidURI( $this->url ) ) {
108 $this->status = StatusValue::newFatal( 'http-invalid-url', $url );
109 } else {
110 $this->status = StatusValue::newGood( 100 ); // continue
111 }
112
113 if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) {
114 $this->timeout = $options['timeout'];
115 } else {
116 // The timeout should always be set by HttpRequestFactory, so this
117 // should only happen if the class was directly constructed
118 wfDeprecated( __METHOD__ . ' without the timeout option', '1.35' );
119 global $wgHTTPTimeout;
120 $this->timeout = $wgHTTPTimeout;
121 }
122 if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) {
123 $this->connectTimeout = $options['connectTimeout'];
124 } else {
125 // The timeout should always be set by HttpRequestFactory, so this
126 // should only happen if the class was directly constructed
127 wfDeprecated( __METHOD__ . ' without the connectTimeout option', '1.35' );
129 $this->connectTimeout = $wgHTTPConnectTimeout;
130 }
131 if ( isset( $options['userAgent'] ) ) {
132 $this->setUserAgent( $options['userAgent'] );
133 }
134 if ( isset( $options['username'] ) && isset( $options['password'] ) ) {
135 $this->setHeader(
136 'Authorization',
137 'Basic ' . base64_encode( $options['username'] . ':' . $options['password'] )
138 );
139 }
140 if ( isset( $options['originalRequest'] ) ) {
141 $this->setOriginalRequest( $options['originalRequest'] );
142 }
143
144 $this->setHeader( 'X-Request-Id', WebRequest::getRequestId() );
145
146 $members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
147 "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ];
148
149 foreach ( $members as $o ) {
150 if ( isset( $options[$o] ) ) {
151 // ensure that MWHttpRequest::method is always
152 // uppercased. T38137
153 if ( $o == 'method' ) {
154 $options[$o] = strtoupper( $options[$o] );
155 }
156 $this->$o = $options[$o];
157 }
158 }
159
160 if ( $this->noProxy ) {
161 $this->proxy = ''; // noProxy takes precedence
162 }
163
164 // Profile based on what's calling us
165 $this->profiler = $profiler;
166 $this->profileName = $caller;
167 }
168
172 public function setLogger( LoggerInterface $logger ) {
173 $this->logger = $logger;
174 }
175
181 public static function canMakeRequests() {
182 return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
183 }
184
195 public static function factory( $url, array $options = null, $caller = __METHOD__ ) {
196 if ( $options === null ) {
197 $options = [];
198 }
199 return MediaWikiServices::getInstance()->getHttpRequestFactory()
200 ->create( $url, $options, $caller );
201 }
202
208 public function getContent() {
209 return $this->content;
210 }
211
218 public function setData( array $args ) {
219 $this->postData = $args;
220 }
221
227 protected function proxySetup() {
228 // If there is an explicit proxy set and proxies are not disabled, then use it
229 if ( $this->proxy && !$this->noProxy ) {
230 return;
231 }
232
233 // Otherwise, fallback to $wgHTTPProxy if this is not a machine
234 // local URL and proxies are not disabled
235 if ( self::isLocalURL( $this->url ) || $this->noProxy ) {
236 $this->proxy = '';
237 } else {
238 global $wgHTTPProxy;
239 $this->proxy = (string)$wgHTTPProxy;
240 }
241 }
242
249 private static function isLocalURL( $url ) {
251
252 if ( $wgCommandLineMode ) {
253 return false;
254 }
255
256 // Extract host part
257 $matches = [];
258 if ( preg_match( '!^https?://([\w.-]+)[/:].*$!', $url, $matches ) ) {
259 $host = $matches[1];
260 // Split up dotwise
261 $domainParts = explode( '.', $host );
262 // Check if this domain or any superdomain is listed as a local virtual host
263 $domainParts = array_reverse( $domainParts );
264
265 $domain = '';
266 $countParts = count( $domainParts );
267 for ( $i = 0; $i < $countParts; $i++ ) {
268 $domainPart = $domainParts[$i];
269 if ( $i == 0 ) {
270 $domain = $domainPart;
271 } else {
272 $domain = $domainPart . '.' . $domain;
273 }
274
275 if ( in_array( $domain, $wgLocalVirtualHosts ) ) {
276 return true;
277 }
278 }
279 }
280
281 return false;
282 }
283
288 public function setUserAgent( $UA ) {
289 $this->setHeader( 'User-Agent', $UA );
290 }
291
297 public function setHeader( $name, $value ) {
298 // I feel like I should normalize the case here...
299 $this->reqHeaders[$name] = $value;
300 }
301
306 protected function getHeaderList() {
307 $list = [];
308
309 if ( $this->cookieJar ) {
310 $this->reqHeaders['Cookie'] =
311 $this->cookieJar->serializeToHttpRequest(
312 $this->parsedUrl['path'],
313 $this->parsedUrl['host']
314 );
315 }
316
317 foreach ( $this->reqHeaders as $name => $value ) {
318 $list[] = "$name: $value";
319 }
320
321 return $list;
322 }
323
342 public function setCallback( $callback ) {
343 $this->doSetCallback( $callback );
344 }
345
353 protected function 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' );
359 }
360 $this->callback = $callback;
361 }
362
372 public function read( $fh, $content ) {
373 $this->content .= $content;
374 return strlen( $content );
375 }
376
383 public function execute() {
384 throw new LogicException( 'children must override this' );
385 }
386
387 protected function prepare() {
388 $this->content = "";
389
390 if ( strtoupper( $this->method ) == "HEAD" ) {
391 $this->headersOnly = true;
392 }
393
394 $this->proxySetup(); // set up any proxy as needed
395
396 if ( !$this->callback ) {
397 $this->doSetCallback( null );
398 }
399
400 if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
401 $http = MediaWikiServices::getInstance()->getHttpRequestFactory();
402 $this->setUserAgent( $http->getUserAgent() );
403 }
404 }
405
411 protected function parseHeader() {
412 $lastname = "";
413
414 // Failure without (valid) headers gets a response status of zero
415 if ( !$this->status->isOK() ) {
416 $this->respStatus = '0 Error';
417 }
418
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] );
429 }
430 }
431
432 $this->parseCookies();
433 }
434
442 protected function setStatus() {
443 if ( !$this->respHeaders ) {
444 $this->parseHeader();
445 }
446
447 if ( ( (int)$this->respStatus > 0 && (int)$this->respStatus < 400 ) ) {
448 $this->status->setResult( true, (int)$this->respStatus );
449 } else {
450 list( $code, $message ) = explode( " ", $this->respStatus, 2 );
451 $this->status->setResult( false, (int)$this->respStatus );
452 $this->status->fatal( "http-bad-status", $code, $message );
453 }
454 }
455
463 public function getStatus() {
464 if ( !$this->respHeaders ) {
465 $this->parseHeader();
466 }
467
468 return (int)$this->respStatus;
469 }
470
476 public function isRedirect() {
477 if ( !$this->respHeaders ) {
478 $this->parseHeader();
479 }
480
481 $status = (int)$this->respStatus;
482
483 if ( $status >= 300 && $status <= 303 ) {
484 return true;
485 }
486
487 return false;
488 }
489
499 public function getResponseHeaders() {
500 if ( !$this->respHeaders ) {
501 $this->parseHeader();
502 }
503
504 return $this->respHeaders;
505 }
506
513 public function getResponseHeader( $header ) {
514 if ( !$this->respHeaders ) {
515 $this->parseHeader();
516 }
517
518 if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
519 $v = $this->respHeaders[strtolower( $header )];
520 return $v[count( $v ) - 1];
521 }
522
523 return null;
524 }
525
533 public function setCookieJar( CookieJar $jar ) {
534 $this->cookieJar = $jar;
535 }
536
542 public function getCookieJar() {
543 if ( !$this->respHeaders ) {
544 $this->parseHeader();
545 }
546
547 return $this->cookieJar;
548 }
549
559 public function setCookie( $name, $value, array $attr = [] ) {
560 if ( !$this->cookieJar ) {
561 $this->cookieJar = new CookieJar;
562 }
563
564 if ( $this->parsedUrl && !isset( $attr['domain'] ) ) {
565 $attr['domain'] = $this->parsedUrl['host'];
566 }
567
568 $this->cookieJar->setCookie( $name, $value, $attr );
569 }
570
574 protected function parseCookies() {
575 if ( !$this->cookieJar ) {
576 $this->cookieJar = new CookieJar;
577 }
578
579 if ( isset( $this->respHeaders['set-cookie'] ) ) {
580 $url = parse_url( $this->getFinalUrl() );
581 foreach ( $this->respHeaders['set-cookie'] as $cookie ) {
582 $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
583 }
584 }
585 }
586
603 public function getFinalUrl() {
604 $headers = $this->getResponseHeaders();
605
606 // return full url (fix for incorrect but handled relative location)
607 if ( isset( $headers['location'] ) ) {
608 $locations = $headers['location'];
609 $domain = '';
610 $foundRelativeURI = false;
611 $countLocations = count( $locations );
612
613 for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
614 $url = parse_url( $locations[$i] );
615
616 if ( isset( $url['host'] ) ) {
617 $domain = $url['scheme'] . '://' . $url['host'];
618 break; // found correct URI (with host)
619 } else {
620 $foundRelativeURI = true;
621 }
622 }
623
624 if ( !$foundRelativeURI ) {
625 return $locations[$countLocations - 1];
626 }
627 if ( $domain ) {
628 return $domain . $locations[$countLocations - 1];
629 }
630 $url = parse_url( $this->url );
631 if ( isset( $url['host'] ) ) {
632 return $url['scheme'] . '://' . $url['host'] .
633 $locations[$countLocations - 1];
634 }
635 }
636
637 return $this->url;
638 }
639
645 public function canFollowRedirects() {
646 return true;
647 }
648
661 public function setOriginalRequest( $originalRequest ) {
662 if ( $originalRequest instanceof WebRequest ) {
663 $originalRequest = [
664 'ip' => $originalRequest->getIP(),
665 'userAgent' => $originalRequest->getHeader( 'User-Agent' ),
666 ];
667 } elseif (
668 !is_array( $originalRequest )
669 || array_diff( [ 'ip', 'userAgent' ], array_keys( $originalRequest ) )
670 ) {
671 throw new InvalidArgumentException( __METHOD__ . ': $originalRequest must be a '
672 . "WebRequest or an array with 'ip' and 'userAgent' keys" );
673 }
674
675 $this->reqHeaders['X-Forwarded-For'] = $originalRequest['ip'];
676 $this->reqHeaders['X-Original-User-Agent'] = $originalRequest['userAgent'];
677 }
678
695 public static function isValidURI( $uri ) {
696 return (bool)preg_match(
697 '/^https?:\/\/[^\/\s]\S*$/D',
698 $uri
699 );
700 }
701}
$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).
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 $function is deprecated.
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.
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: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:209
if( $line===false) $args
Definition mcc.php:124
$content
Definition router.php:76
$header