MediaWiki  master
MWHttpRequest.php
Go to the documentation of this file.
1 <?php
25 
33 abstract class MWHttpRequest implements LoggerAwareInterface {
34  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  ) {
103 
104  $this->url = wfExpandUrl( $url, PROTO_HTTP );
105  $this->parsedUrl = wfParseUrl( $this->url );
106 
107  $this->logger = $options['logger'] ?? new NullLogger();
108 
109  if ( !$this->parsedUrl || !self::isValidURI( $this->url ) ) {
110  $this->status = StatusValue::newFatal( 'http-invalid-url', $url );
111  } else {
112  $this->status = StatusValue::newGood( 100 ); // continue
113  }
114 
115  if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) {
116  $this->timeout = $options['timeout'];
117  } else {
118  $this->timeout = $wgHTTPTimeout;
119  }
120  if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) {
121  $this->connectTimeout = $options['connectTimeout'];
122  } else {
123  $this->connectTimeout = $wgHTTPConnectTimeout;
124  }
125  if ( isset( $options['userAgent'] ) ) {
126  $this->setUserAgent( $options['userAgent'] );
127  }
128  if ( isset( $options['username'] ) && isset( $options['password'] ) ) {
129  $this->setHeader(
130  'Authorization',
131  'Basic ' . base64_encode( $options['username'] . ':' . $options['password'] )
132  );
133  }
134  if ( isset( $options['originalRequest'] ) ) {
135  $this->setOriginalRequest( $options['originalRequest'] );
136  }
137 
138  $this->setHeader( 'X-Request-Id', WebRequest::getRequestId() );
139 
140  $members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
141  "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ];
142 
143  foreach ( $members as $o ) {
144  if ( isset( $options[$o] ) ) {
145  // ensure that MWHttpRequest::method is always
146  // uppercased. T38137
147  if ( $o == 'method' ) {
148  // @phan-suppress-next-line PhanTypeInvalidDimOffset
149  $options[$o] = strtoupper( $options[$o] );
150  }
151  $this->$o = $options[$o];
152  }
153  }
154 
155  if ( $this->noProxy ) {
156  $this->proxy = ''; // noProxy takes precedence
157  }
158 
159  // Profile based on what's calling us
160  $this->profiler = $profiler;
161  $this->profileName = $caller;
162  }
163 
167  public function setLogger( LoggerInterface $logger ) {
168  $this->logger = $logger;
169  }
170 
176  public static function canMakeRequests() {
177  return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
178  }
179 
190  public static function factory( $url, array $options = null, $caller = __METHOD__ ) {
191  if ( $options === null ) {
192  $options = [];
193  }
194  return MediaWikiServices::getInstance()->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  $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  $http = MediaWikiServices::getInstance()->getHttpRequestFactory();
397  $this->setUserAgent( $http->getUserAgent() );
398  }
399  }
400 
406  protected function parseHeader() {
407  $lastname = "";
408 
409  // Failure without (valid) headers gets a response status of zero
410  if ( !$this->status->isOK() ) {
411  $this->respStatus = '0 Error';
412  }
413 
414  foreach ( $this->headerList as $header ) {
415  if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
416  $this->respVersion = $match[1];
417  $this->respStatus = $match[2];
418  } elseif ( preg_match( "#^[ \t]#", $header ) ) {
419  $last = count( $this->respHeaders[$lastname] ) - 1;
420  $this->respHeaders[$lastname][$last] .= "\r\n$header";
421  } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
422  $this->respHeaders[strtolower( $match[1] )][] = $match[2];
423  $lastname = strtolower( $match[1] );
424  }
425  }
426 
427  $this->parseCookies();
428  }
429 
437  protected function setStatus() {
438  if ( !$this->respHeaders ) {
439  $this->parseHeader();
440  }
441 
442  if ( ( (int)$this->respStatus > 0 && (int)$this->respStatus < 400 ) ) {
443  $this->status->setResult( true, (int)$this->respStatus );
444  } else {
445  list( $code, $message ) = explode( " ", $this->respStatus, 2 );
446  $this->status->setResult( false, (int)$this->respStatus );
447  $this->status->fatal( "http-bad-status", $code, $message );
448  }
449  }
450 
458  public function getStatus() {
459  if ( !$this->respHeaders ) {
460  $this->parseHeader();
461  }
462 
463  return (int)$this->respStatus;
464  }
465 
471  public function isRedirect() {
472  if ( !$this->respHeaders ) {
473  $this->parseHeader();
474  }
475 
476  $status = (int)$this->respStatus;
477 
478  if ( $status >= 300 && $status <= 303 ) {
479  return true;
480  }
481 
482  return false;
483  }
484 
494  public function getResponseHeaders() {
495  if ( !$this->respHeaders ) {
496  $this->parseHeader();
497  }
498 
499  return $this->respHeaders;
500  }
501 
508  public function getResponseHeader( $header ) {
509  if ( !$this->respHeaders ) {
510  $this->parseHeader();
511  }
512 
513  if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
514  $v = $this->respHeaders[strtolower( $header )];
515  return $v[count( $v ) - 1];
516  }
517 
518  return null;
519  }
520 
528  public function setCookieJar( CookieJar $jar ) {
529  $this->cookieJar = $jar;
530  }
531 
537  public function getCookieJar() {
538  if ( !$this->respHeaders ) {
539  $this->parseHeader();
540  }
541 
542  return $this->cookieJar;
543  }
544 
554  public function setCookie( $name, $value, array $attr = [] ) {
555  if ( !$this->cookieJar ) {
556  $this->cookieJar = new CookieJar;
557  }
558 
559  if ( $this->parsedUrl && !isset( $attr['domain'] ) ) {
560  $attr['domain'] = $this->parsedUrl['host'];
561  }
562 
563  $this->cookieJar->setCookie( $name, $value, $attr );
564  }
565 
569  protected function parseCookies() {
570  if ( !$this->cookieJar ) {
571  $this->cookieJar = new CookieJar;
572  }
573 
574  if ( isset( $this->respHeaders['set-cookie'] ) ) {
575  $url = parse_url( $this->getFinalUrl() );
576  foreach ( $this->respHeaders['set-cookie'] as $cookie ) {
577  $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
578  }
579  }
580  }
581 
598  public function getFinalUrl() {
599  $headers = $this->getResponseHeaders();
600 
601  // return full url (fix for incorrect but handled relative location)
602  if ( isset( $headers['location'] ) ) {
603  $locations = $headers['location'];
604  $domain = '';
605  $foundRelativeURI = false;
606  $countLocations = count( $locations );
607 
608  for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
609  $url = parse_url( $locations[$i] );
610 
611  if ( isset( $url['host'] ) ) {
612  $domain = $url['scheme'] . '://' . $url['host'];
613  break; // found correct URI (with host)
614  } else {
615  $foundRelativeURI = true;
616  }
617  }
618 
619  if ( !$foundRelativeURI ) {
620  return $locations[$countLocations - 1];
621  }
622  if ( $domain ) {
623  return $domain . $locations[$countLocations - 1];
624  }
625  $url = parse_url( $this->url );
626  if ( isset( $url['host'] ) ) {
627  return $url['scheme'] . '://' . $url['host'] .
628  $locations[$countLocations - 1];
629  }
630  }
631 
632  return $this->url;
633  }
634 
640  public function canFollowRedirects() {
641  return true;
642  }
643 
656  public function setOriginalRequest( $originalRequest ) {
657  if ( $originalRequest instanceof WebRequest ) {
658  $originalRequest = [
659  'ip' => $originalRequest->getIP(),
660  'userAgent' => $originalRequest->getHeader( 'User-Agent' ),
661  ];
662  } elseif (
663  !is_array( $originalRequest )
664  || array_diff( [ 'ip', 'userAgent' ], array_keys( $originalRequest ) )
665  ) {
666  throw new InvalidArgumentException( __METHOD__ . ': $originalRequest must be a '
667  . "WebRequest or an array with 'ip' and 'userAgent' keys" );
668  }
669 
670  $this->reqHeaders['X-Forwarded-For'] = $originalRequest['ip'];
671  $this->reqHeaders['X-Original-User-Agent'] = $originalRequest['userAgent'];
672  }
673 
690  public static function isValidURI( $uri ) {
691  return (bool)preg_match(
692  '/^https?:\/\/[^\/\s]\S*$/D',
693  $uri
694  );
695  }
696 }
CookieJar $cookieJar
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
string [][] $respHeaders
static factory( $url, array $options=null, $caller=__METHOD__)
Generate a new request object.
read( $fh, $content)
A generic callback to read the body of the response from a remote server.
proxySetup()
Take care of setting up the proxy (do nothing if "noProxy" is set)
static getRequestId()
Get the unique request ID.
Definition: WebRequest.php:309
static isValidURI( $uri)
Check that the given URI is a valid one.
execute()
Take care of whatever is necessary to perform the URI request.
setUserAgent( $UA)
Set the user agent.
canFollowRedirects()
Returns true if the backend can follow redirects.
LoggerInterface $logger
callable $callback
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
__construct( $url, array $options=[], $caller=__METHOD__, Profiler $profiler=null)
isRedirect()
Returns true if the last status code was a redirect.
int $wgHTTPTimeout
Timeout for HTTP requests done internally, in seconds.
static canMakeRequests()
Simple function to test if we can make any sort of requests at all, using cURL or fopen() ...
$wgHTTPProxy
Proxy to use for CURL requests.
parseCookies()
Parse the cookies in the response headers and store them in the cookie jar.
setData(array $args)
Set the parameters of the request.
if( $line===false) $args
Definition: mcc.php:124
getCookieJar()
Returns the cookie jar in use.
static isLocalURL( $url)
Check if the URL can be served by localhost.
getHeaderList()
Get an array of the headers.
getFinalUrl()
Returns the final URL after all redirections.
Profiler $profiler
setLogger(LoggerInterface $logger)
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
const SUPPORTS_FILE_POSTS
StatusValue $status
getResponseHeader( $header)
Returns the value of the given response header.
setStatus()
Sets HTTPRequest status member to a fatal value with the error message if the returned integer value ...
$header
getResponseHeaders()
Returns an associative array of response headers after the request has been executed.
const PROTO_HTTP
Definition: Defines.php:199
getStatus()
Get the integer value of the HTTP status code (e.g.
string $profileName
setCookieJar(CookieJar $jar)
Tells the MWHttpRequest object to use this pre-loaded CookieJar.
$wgLocalVirtualHosts
Local virtual hosts.
parseHeader()
Parses the headers, including the HTTP status code and any Set-Cookie headers.
setCookie( $name, $value, array $attr=[])
Sets a cookie.
$wgHTTPConnectTimeout
Timeout for connections done internally (in seconds) Only works for curl.
int string $timeout
setHeader( $name, $value)
Set an arbitrary header.
global $wgCommandLineMode
doSetCallback( $callback)
Worker function for setting callbacks.
setCookie( $name, $value, $attr)
Set a cookie in the cookie jar.
Definition: CookieJar.php:36
Cookie jar to use with MWHttpRequest.
Definition: CookieJar.php:25
getContent()
Get the body, or content, of the response to the request.
setOriginalRequest( $originalRequest)
Set information about the original request.
wfIniGetBool( $setting)
Safety wrapper around ini_get() for boolean settings.
setCallback( $callback)
Set a read callback to accept data read from the HTTP request.
$matches