MediaWiki  master
MWHttpRequest.php
Go to the documentation of this file.
1 <?php
22 use Psr\Log\LoggerAwareInterface;
23 use Psr\Log\LoggerInterface;
24 use Psr\Log\NullLogger;
25 
33 abstract 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  $httpTimeout = MediaWikiServices::getInstance()->getMainConfig()->get( 'HTTPTimeout' );
119  $this->timeout = $httpTimeout;
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' );
127  $httpConnectTimeout = MediaWikiServices::getInstance()->getMainConfig()->get( 'HTTPConnectTimeout' );
128  $this->connectTimeout = $httpConnectTimeout;
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  $httpProxy = MediaWikiServices::getInstance()->getMainConfig()->get( 'HTTPProxy' );
228  $localHTTPProxy = MediaWikiServices::getInstance()->getMainConfig()->get( 'LocalHTTPProxy' );
229  // If proxies are disabled, clear any other proxy
230  if ( $this->noProxy ) {
231  $this->proxy = '';
232  return;
233  }
234 
235  // If there is an explicit proxy already set, use it
236  if ( $this->proxy ) {
237  return;
238  }
239 
240  // Otherwise, fallback to $wgLocalHTTPProxy for local URLs
241  // or $wgHTTPProxy for everything else
242  if ( self::isLocalURL( $this->url ) ) {
243  if ( $localHTTPProxy !== false ) {
244  $this->setReverseProxy( $localHTTPProxy );
245  }
246  } else {
247  $this->proxy = (string)$httpProxy;
248  }
249  }
250 
261  protected function setReverseProxy( string $proxy ) {
262  $parsedProxy = wfParseUrl( $proxy );
263  if ( $parsedProxy === false ) {
264  throw new Exception( "Invalid reverseProxy configured: $proxy" );
265  }
266  // Set the current host in the Host header
267  $this->setHeader( 'Host', $this->parsedUrl['host'] );
268  // Replace scheme, host and port in the request
269  $this->parsedUrl['scheme'] = $parsedProxy['scheme'];
270  $this->parsedUrl['host'] = $parsedProxy['host'];
271  if ( isset( $parsedProxy['port'] ) ) {
272  $this->parsedUrl['port'] = $parsedProxy['port'];
273  } else {
274  unset( $this->parsedUrl['port'] );
275  }
276  $this->url = wfAssembleUrl( $this->parsedUrl );
277  // Mark that we're already using a proxy
278  $this->noProxy = true;
279  }
280 
287  private static function isLocalURL( $url ) {
288  $commandLineMode = MediaWikiServices::getInstance()->getMainConfig()->get( 'CommandLineMode' );
289  $localVirtualHosts = MediaWikiServices::getInstance()->getMainConfig()->get( 'LocalVirtualHosts' );
290  if ( $commandLineMode ) {
291  return false;
292  }
293 
294  // Extract host part
295  $matches = [];
296  if ( preg_match( '!^https?://([\w.-]+)[/:].*$!', $url, $matches ) ) {
297  $host = $matches[1];
298  // Split up dotwise
299  $domainParts = explode( '.', $host );
300  // Check if this domain or any superdomain is listed as a local virtual host
301  $domainParts = array_reverse( $domainParts );
302 
303  $domain = '';
304  $countParts = count( $domainParts );
305  for ( $i = 0; $i < $countParts; $i++ ) {
306  $domainPart = $domainParts[$i];
307  if ( $i == 0 ) {
308  $domain = $domainPart;
309  } else {
310  $domain = $domainPart . '.' . $domain;
311  }
312 
313  if ( in_array( $domain, $localVirtualHosts ) ) {
314  return true;
315  }
316  }
317  }
318 
319  return false;
320  }
321 
325  public function setUserAgent( $UA ) {
326  $this->setHeader( 'User-Agent', $UA );
327  }
328 
334  public function setHeader( $name, $value ) {
335  // I feel like I should normalize the case here...
336  $this->reqHeaders[$name] = $value;
337  }
338 
343  protected function getHeaderList() {
344  $list = [];
345 
346  if ( $this->cookieJar ) {
347  $this->reqHeaders['Cookie'] =
348  $this->cookieJar->serializeToHttpRequest(
349  $this->parsedUrl['path'],
350  $this->parsedUrl['host']
351  );
352  }
353 
354  foreach ( $this->reqHeaders as $name => $value ) {
355  $list[] = "$name: $value";
356  }
357 
358  return $list;
359  }
360 
379  public function setCallback( $callback ) {
380  $this->doSetCallback( $callback );
381  }
382 
390  protected function doSetCallback( $callback ) {
391  if ( $callback === null ) {
392  $callback = [ $this, 'read' ];
393  } elseif ( !is_callable( $callback ) ) {
394  $this->status->fatal( 'http-internal-error' );
395  throw new InvalidArgumentException( __METHOD__ . ': invalid callback' );
396  }
397  $this->callback = $callback;
398  }
399 
409  public function read( $fh, $content ) {
410  $this->content .= $content;
411  return strlen( $content );
412  }
413 
420  public function execute() {
421  throw new LogicException( 'children must override this' );
422  }
423 
424  protected function prepare() {
425  $this->content = "";
426 
427  if ( strtoupper( $this->method ) == "HEAD" ) {
428  $this->headersOnly = true;
429  }
430 
431  $this->proxySetup(); // set up any proxy as needed
432 
433  if ( !$this->callback ) {
434  $this->doSetCallback( null );
435  }
436 
437  if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
438  $http = MediaWikiServices::getInstance()->getHttpRequestFactory();
439  $this->setUserAgent( $http->getUserAgent() );
440  }
441  }
442 
448  protected function parseHeader() {
449  $lastname = "";
450 
451  // Failure without (valid) headers gets a response status of zero
452  if ( !$this->status->isOK() ) {
453  $this->respStatus = '0 Error';
454  }
455 
456  foreach ( $this->headerList as $header ) {
457  if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
458  $this->respVersion = $match[1];
459  $this->respStatus = $match[2];
460  } elseif ( preg_match( "#^[ \t]#", $header ) ) {
461  $last = count( $this->respHeaders[$lastname] ) - 1;
462  $this->respHeaders[$lastname][$last] .= "\r\n$header";
463  } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
464  $this->respHeaders[strtolower( $match[1] )][] = $match[2];
465  $lastname = strtolower( $match[1] );
466  }
467  }
468 
469  $this->parseCookies();
470  }
471 
479  protected function setStatus() {
480  if ( !$this->respHeaders ) {
481  $this->parseHeader();
482  }
483 
484  if ( (int)$this->respStatus > 0 && (int)$this->respStatus < 400 ) {
485  $this->status->setResult( true, (int)$this->respStatus );
486  } else {
487  list( $code, $message ) = explode( " ", $this->respStatus, 2 );
488  $this->status->setResult( false, (int)$this->respStatus );
489  $this->status->fatal( "http-bad-status", $code, $message );
490  }
491  }
492 
500  public function getStatus() {
501  if ( !$this->respHeaders ) {
502  $this->parseHeader();
503  }
504 
505  return (int)$this->respStatus;
506  }
507 
513  public function isRedirect() {
514  if ( !$this->respHeaders ) {
515  $this->parseHeader();
516  }
517 
518  $status = (int)$this->respStatus;
519 
520  if ( $status >= 300 && $status <= 303 ) {
521  return true;
522  }
523 
524  return false;
525  }
526 
536  public function getResponseHeaders() {
537  if ( !$this->respHeaders ) {
538  $this->parseHeader();
539  }
540 
541  return $this->respHeaders;
542  }
543 
550  public function getResponseHeader( $header ) {
551  if ( !$this->respHeaders ) {
552  $this->parseHeader();
553  }
554 
555  if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
556  $v = $this->respHeaders[strtolower( $header )];
557  return $v[count( $v ) - 1];
558  }
559 
560  return null;
561  }
562 
570  public function setCookieJar( CookieJar $jar ) {
571  $this->cookieJar = $jar;
572  }
573 
579  public function getCookieJar() {
580  if ( !$this->respHeaders ) {
581  $this->parseHeader();
582  }
583 
584  return $this->cookieJar;
585  }
586 
596  public function setCookie( $name, $value, array $attr = [] ) {
597  if ( !$this->cookieJar ) {
598  $this->cookieJar = new CookieJar;
599  }
600 
601  if ( $this->parsedUrl && !isset( $attr['domain'] ) ) {
602  $attr['domain'] = $this->parsedUrl['host'];
603  }
604 
605  $this->cookieJar->setCookie( $name, $value, $attr );
606  }
607 
611  protected function parseCookies() {
612  if ( !$this->cookieJar ) {
613  $this->cookieJar = new CookieJar;
614  }
615 
616  if ( isset( $this->respHeaders['set-cookie'] ) ) {
617  $url = parse_url( $this->getFinalUrl() );
618  foreach ( $this->respHeaders['set-cookie'] as $cookie ) {
619  $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
620  }
621  }
622  }
623 
640  public function getFinalUrl() {
641  $headers = $this->getResponseHeaders();
642 
643  // return full url (fix for incorrect but handled relative location)
644  if ( isset( $headers['location'] ) ) {
645  $locations = $headers['location'];
646  $domain = '';
647  $foundRelativeURI = false;
648  $countLocations = count( $locations );
649 
650  for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
651  $url = parse_url( $locations[$i] );
652 
653  if ( isset( $url['host'] ) ) {
654  $domain = $url['scheme'] . '://' . $url['host'];
655  break; // found correct URI (with host)
656  } else {
657  $foundRelativeURI = true;
658  }
659  }
660 
661  if ( !$foundRelativeURI ) {
662  return $locations[$countLocations - 1];
663  }
664  if ( $domain ) {
665  return $domain . $locations[$countLocations - 1];
666  }
667  $url = parse_url( $this->url );
668  if ( isset( $url['host'] ) ) {
669  return $url['scheme'] . '://' . $url['host'] .
670  $locations[$countLocations - 1];
671  }
672  }
673 
674  return $this->url;
675  }
676 
682  public function canFollowRedirects() {
683  return true;
684  }
685 
698  public function setOriginalRequest( $originalRequest ) {
699  if ( $originalRequest instanceof WebRequest ) {
700  $originalRequest = [
701  'ip' => $originalRequest->getIP(),
702  'userAgent' => $originalRequest->getHeader( 'User-Agent' ),
703  ];
704  } elseif (
705  !is_array( $originalRequest )
706  || array_diff( [ 'ip', 'userAgent' ], array_keys( $originalRequest ) )
707  ) {
708  throw new InvalidArgumentException( __METHOD__ . ': $originalRequest must be a '
709  . "WebRequest or an array with 'ip' and 'userAgent' keys" );
710  }
711 
712  $this->reqHeaders['X-Forwarded-For'] = $originalRequest['ip'];
713  $this->reqHeaders['X-Original-User-Agent'] = $originalRequest['userAgent'];
714  }
715 
732  public static function isValidURI( $uri ) {
733  return (bool)preg_match(
734  '/^https?:\/\/[^\/\s]\S*$/D',
735  $uri
736  );
737  }
738 }
MWHttpRequest\$headerList
$headerList
Definition: MWHttpRequest.php:65
MWHttpRequest\$headersOnly
$headersOnly
Definition: MWHttpRequest.php:42
StatusValue
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:43
MWHttpRequest\$logger
LoggerInterface $logger
Definition: MWHttpRequest.php:87
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
MWHttpRequest\$callback
callable $callback
Definition: MWHttpRequest.php:55
MWHttpRequest\setStatus
setStatus()
Sets HTTPRequest status member to a fatal value with the error message if the returned integer value ...
Definition: MWHttpRequest.php:479
MWHttpRequest\$respVersion
$respVersion
Definition: MWHttpRequest.php:66
MWHttpRequest\__construct
__construct( $url, array $options=[], $caller=__METHOD__, Profiler $profiler=null)
Definition: MWHttpRequest.php:98
MWHttpRequest\doSetCallback
doSetCallback( $callback)
Worker function for setting callbacks.
Definition: MWHttpRequest.php:390
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:204
MWHttpRequest\proxySetup
proxySetup()
Take care of setting up the proxy (do nothing if "noProxy" is set)
Definition: MWHttpRequest.php:226
MWHttpRequest\$maxRedirects
$maxRedirects
Definition: MWHttpRequest.php:56
MWHttpRequest\$sslVerifyCert
$sslVerifyCert
Definition: MWHttpRequest.php:47
MWHttpRequest\$followRedirects
$followRedirects
Definition: MWHttpRequest.php:57
MWHttpRequest\$content
$content
Definition: MWHttpRequest.php:41
MWHttpRequest\$status
StatusValue $status
Definition: MWHttpRequest.php:72
MWHttpRequest\$profiler
Profiler $profiler
Definition: MWHttpRequest.php:77
MWHttpRequest\getStatus
getStatus()
Get the integer value of the HTTP status code (e.g.
Definition: MWHttpRequest.php:500
MWHttpRequest\$noProxy
$noProxy
Definition: MWHttpRequest.php:45
MWHttpRequest\setCookieJar
setCookieJar(CookieJar $jar)
Tells the MWHttpRequest object to use this pre-loaded CookieJar.
Definition: MWHttpRequest.php:570
MWHttpRequest\parseHeader
parseHeader()
Parses the headers, including the HTTP status code and any Set-Cookie headers.
Definition: MWHttpRequest.php:448
MWHttpRequest\$connectTimeout
$connectTimeout
Definition: MWHttpRequest.php:58
wfParseUrl
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
Definition: GlobalFunctions.php:776
MWHttpRequest\$postData
$postData
Definition: MWHttpRequest.php:43
MWHttpRequest\getContent
getContent()
Get the body, or content, of the response to the request.
Definition: MWHttpRequest.php:207
MWHttpRequest\SUPPORTS_FILE_POSTS
const SUPPORTS_FILE_POSTS
Definition: MWHttpRequest.php:34
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:997
MWHttpRequest\parseCookies
parseCookies()
Parse the cookies in the response headers and store them in the cookie jar.
Definition: MWHttpRequest.php:611
MWHttpRequest\$respStatus
$respStatus
Definition: MWHttpRequest.php:67
CookieJar\setCookie
setCookie( $name, $value, $attr)
Set a cookie in the cookie jar.
Definition: CookieJar.php:36
MWHttpRequest\setData
setData(array $args)
Set the parameters of the request.
Definition: MWHttpRequest.php:217
$matches
$matches
Definition: NoLocalSettings.php:24
MWHttpRequest\isRedirect
isRedirect()
Returns true if the last status code was a redirect.
Definition: MWHttpRequest.php:513
Profiler
Profiler base class that defines the interface and some shared functionality.
Definition: Profiler.php:36
MWHttpRequest\getCookieJar
getCookieJar()
Returns the cookie jar in use.
Definition: MWHttpRequest.php:579
MWHttpRequest\$timeout
int string $timeout
Definition: MWHttpRequest.php:39
$args
if( $line===false) $args
Definition: mcc.php:124
MWHttpRequest\isLocalURL
static isLocalURL( $url)
Check if the URL can be served by localhost.
Definition: MWHttpRequest.php:287
MWHttpRequest\$cookieJar
CookieJar $cookieJar
Definition: MWHttpRequest.php:63
MWHttpRequest\read
read( $fh, $content)
A generic callback to read the body of the response from a remote server.
Definition: MWHttpRequest.php:409
$header
$header
Definition: updateCredits.php:37
MWHttpRequest\$method
$method
Definition: MWHttpRequest.php:49
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
MWHttpRequest
This wrapper class will call out to curl (if available) or fallback to regular PHP if necessary for h...
Definition: MWHttpRequest.php:33
MWHttpRequest\setCallback
setCallback( $callback)
Set a read callback to accept data read from the HTTP request.
Definition: MWHttpRequest.php:379
MWHttpRequest\setHeader
setHeader( $name, $value)
Set an arbitrary header.
Definition: MWHttpRequest.php:334
MWHttpRequest\getHeaderList
getHeaderList()
Get an array of the headers.
Definition: MWHttpRequest.php:343
MWHttpRequest\$parsedUrl
$parsedUrl
Definition: MWHttpRequest.php:53
MWHttpRequest\$profileName
string $profileName
Definition: MWHttpRequest.php:82
MWHttpRequest\getResponseHeaders
getResponseHeaders()
Returns an associative array of response headers after the request has been executed.
Definition: MWHttpRequest.php:536
wfIniGetBool
wfIniGetBool( $setting)
Safety wrapper around ini_get() for boolean settings.
Definition: GlobalFunctions.php:1828
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:43
MWHttpRequest\$sslVerifyHost
$sslVerifyHost
Definition: MWHttpRequest.php:46
MWHttpRequest\setCookie
setCookie( $name, $value, array $attr=[])
Sets a cookie.
Definition: MWHttpRequest.php:596
MWHttpRequest\$respHeaders
string[][] $respHeaders
Definition: MWHttpRequest.php:69
MWHttpRequest\$reqHeaders
array $reqHeaders
Definition: MWHttpRequest.php:51
CookieJar
Cookie jar to use with MWHttpRequest.
Definition: CookieJar.php:25
WebRequest\getRequestId
static getRequestId()
Get the current request ID.
Definition: WebRequest.php:333
MWHttpRequest\$caInfo
$caInfo
Definition: MWHttpRequest.php:48
MWHttpRequest\canMakeRequests
static canMakeRequests()
Simple function to test if we can make any sort of requests at all, using cURL or fopen()
Definition: MWHttpRequest.php:180
MWHttpRequest\isValidURI
static isValidURI( $uri)
Check that the given URI is a valid one.
Definition: MWHttpRequest.php:732
MWHttpRequest\getResponseHeader
getResponseHeader( $header)
Returns the value of the given response header.
Definition: MWHttpRequest.php:550
MWHttpRequest\setUserAgent
setUserAgent( $UA)
Definition: MWHttpRequest.php:325
MWHttpRequest\execute
execute()
Take care of whatever is necessary to perform the URI request.
Definition: MWHttpRequest.php:420
wfAssembleUrl
wfAssembleUrl( $urlParts)
This function will reassemble a URL parsed with wfParseURL.
Definition: GlobalFunctions.php:570
MWHttpRequest\canFollowRedirects
canFollowRedirects()
Returns true if the backend can follow redirects.
Definition: MWHttpRequest.php:682
MWHttpRequest\$proxy
$proxy
Definition: MWHttpRequest.php:44
MWHttpRequest\setReverseProxy
setReverseProxy(string $proxy)
Enable use of a reverse proxy in which the hostname is passed as a "Host" header, and the request is ...
Definition: MWHttpRequest.php:261
MWHttpRequest\setLogger
setLogger(LoggerInterface $logger)
Definition: MWHttpRequest.php:171
MWHttpRequest\getFinalUrl
getFinalUrl()
Returns the final URL after all redirections.
Definition: MWHttpRequest.php:640
MWHttpRequest\setOriginalRequest
setOriginalRequest( $originalRequest)
Set information about the original request.
Definition: MWHttpRequest.php:698
PROTO_HTTP
const PROTO_HTTP
Definition: Defines.php:192
MWHttpRequest\$url
$url
Definition: MWHttpRequest.php:52
MWHttpRequest\factory
static factory( $url, array $options=null, $caller=__METHOD__)
Generate a new request object.
Definition: MWHttpRequest.php:194
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:474
MWHttpRequest\prepare
prepare()
Definition: MWHttpRequest.php:424