Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
43.13% |
91 / 211 |
|
32.26% |
10 / 31 |
CRAP | |
0.00% |
0 / 1 |
MWHttpRequest | |
43.13% |
91 / 211 |
|
32.26% |
10 / 31 |
1901.88 | |
0.00% |
0 / 1 |
__construct | |
60.53% |
23 / 38 |
|
0.00% |
0 / 1 |
28.84 | |||
setLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canMakeRequests | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
getContent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addTelemetry | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
proxySetup | |
61.54% |
8 / 13 |
|
0.00% |
0 / 1 |
6.42 | |||
setReverseProxy | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
3.05 | |||
isLocalURL | |
10.53% |
2 / 19 |
|
0.00% |
0 / 1 |
31.79 | |||
setUserAgent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHeaderList | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
setCallback | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doSetCallback | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
3.33 | |||
read | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
prepare | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
4.02 | |||
parseHeader | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
setStatus | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
getStatus | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
isRedirect | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
getResponseHeaders | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
getResponseHeader | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
setCookieJar | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCookieJar | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setCookie | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
parseCookies | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
5.05 | |||
getFinalUrl | |
14.29% |
3 / 21 |
|
0.00% |
0 / 1 |
60.01 | |||
canFollowRedirects | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setOriginalRequest | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
isValidURI | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | use MediaWiki\MainConfigNames; |
22 | use MediaWiki\MediaWikiServices; |
23 | use MediaWiki\Request\WebRequest; |
24 | use MediaWiki\Status\Status; |
25 | use Psr\Log\LoggerAwareInterface; |
26 | use Psr\Log\LoggerInterface; |
27 | use Psr\Log\NullLogger; |
28 | use Wikimedia\Http\TelemetryHeadersInterface; |
29 | |
30 | /** |
31 | * This wrapper class will call out to curl (if available) or fallback |
32 | * to regular PHP if necessary for handling internal HTTP requests. |
33 | * |
34 | * Renamed from HttpRequest to MWHttpRequest to avoid conflict with |
35 | * PHP's HTTP extension. |
36 | */ |
37 | abstract class MWHttpRequest implements LoggerAwareInterface { |
38 | public const SUPPORTS_FILE_POSTS = false; |
39 | |
40 | /** |
41 | * @var int|string |
42 | */ |
43 | protected $timeout = 'default'; |
44 | |
45 | protected $content; |
46 | protected $headersOnly = null; |
47 | protected $postData = null; |
48 | protected $proxy = null; |
49 | protected $noProxy = false; |
50 | protected $sslVerifyHost = true; |
51 | protected $sslVerifyCert = true; |
52 | protected $caInfo = null; |
53 | protected $method = "GET"; |
54 | /** @var array */ |
55 | protected $reqHeaders = []; |
56 | protected $url; |
57 | protected $parsedUrl; |
58 | /** @var callable */ |
59 | protected $callback; |
60 | protected $maxRedirects = 5; |
61 | protected $followRedirects = false; |
62 | protected $connectTimeout; |
63 | |
64 | /** |
65 | * @var CookieJar |
66 | */ |
67 | protected $cookieJar; |
68 | |
69 | protected $headerList = []; |
70 | protected $respVersion = "0.9"; |
71 | protected $respStatus = "200 Ok"; |
72 | /** @var string[][] */ |
73 | protected $respHeaders = []; |
74 | |
75 | /** @var StatusValue */ |
76 | protected $status; |
77 | |
78 | /** |
79 | * @var Profiler |
80 | */ |
81 | protected $profiler; |
82 | |
83 | /** |
84 | * @var string |
85 | */ |
86 | protected $profileName; |
87 | |
88 | /** |
89 | * @var LoggerInterface |
90 | */ |
91 | protected $logger; |
92 | |
93 | /** |
94 | * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL |
95 | * @param array $options (optional) extra params to pass (see HttpRequestFactory::create()) |
96 | * @phpcs:ignore Generic.Files.LineLength |
97 | * @phan-param array{timeout?:int|string,connectTimeout?:int|string,postData?:array,proxy?:string,noProxy?:bool,sslVerifyHost?:bool,sslVerifyCert?:bool,caInfo?:string,maxRedirects?:int,followRedirects?:bool,userAgent?:string,logger?:LoggerInterface,username?:string,password?:string,originalRequest?:WebRequest|array{ip:string,userAgent:string},method?:string} $options |
98 | * @param string $caller The method making this request, for profiling |
99 | * @param Profiler|null $profiler An instance of the profiler for profiling, or null |
100 | * @throws Exception |
101 | */ |
102 | public function __construct( |
103 | $url, array $options = [], $caller = __METHOD__, Profiler $profiler = null |
104 | ) { |
105 | $this->url = wfExpandUrl( $url, PROTO_HTTP ); |
106 | $this->parsedUrl = wfParseUrl( $this->url ); |
107 | |
108 | $this->logger = $options['logger'] ?? new NullLogger(); |
109 | |
110 | if ( !$this->parsedUrl || !self::isValidURI( $this->url ) ) { |
111 | $this->status = StatusValue::newFatal( 'http-invalid-url', $url ); |
112 | } else { |
113 | $this->status = StatusValue::newGood( 100 ); // continue |
114 | } |
115 | |
116 | if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) { |
117 | $this->timeout = $options['timeout']; |
118 | } else { |
119 | // The timeout should always be set by HttpRequestFactory, so this |
120 | // should only happen if the class was directly constructed |
121 | wfDeprecated( __METHOD__ . ' without the timeout option', '1.35' ); |
122 | $httpTimeout = MediaWikiServices::getInstance()->getMainConfig()->get( |
123 | MainConfigNames::HTTPTimeout ); |
124 | $this->timeout = $httpTimeout; |
125 | } |
126 | if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) { |
127 | $this->connectTimeout = $options['connectTimeout']; |
128 | } else { |
129 | // The timeout should always be set by HttpRequestFactory, so this |
130 | // should only happen if the class was directly constructed |
131 | wfDeprecated( __METHOD__ . ' without the connectTimeout option', '1.35' ); |
132 | $httpConnectTimeout = MediaWikiServices::getInstance()->getMainConfig()->get( |
133 | MainConfigNames::HTTPConnectTimeout ); |
134 | $this->connectTimeout = $httpConnectTimeout; |
135 | } |
136 | if ( isset( $options['userAgent'] ) ) { |
137 | $this->setUserAgent( $options['userAgent'] ); |
138 | } |
139 | if ( isset( $options['username'] ) && isset( $options['password'] ) ) { |
140 | $this->setHeader( |
141 | 'Authorization', |
142 | 'Basic ' . base64_encode( $options['username'] . ':' . $options['password'] ) |
143 | ); |
144 | } |
145 | if ( isset( $options['originalRequest'] ) ) { |
146 | $this->setOriginalRequest( $options['originalRequest'] ); |
147 | } |
148 | |
149 | $members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo", |
150 | "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ]; |
151 | |
152 | foreach ( $members as $o ) { |
153 | if ( isset( $options[$o] ) ) { |
154 | // ensure that MWHttpRequest::method is always |
155 | // uppercased. T38137 |
156 | if ( $o == 'method' ) { |
157 | // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive |
158 | $options[$o] = strtoupper( $options[$o] ); |
159 | } |
160 | $this->$o = $options[$o]; |
161 | } |
162 | } |
163 | |
164 | if ( $this->noProxy ) { |
165 | $this->proxy = ''; // noProxy takes precedence |
166 | } |
167 | |
168 | // Profile based on what's calling us |
169 | $this->profiler = $profiler; |
170 | $this->profileName = $caller; |
171 | } |
172 | |
173 | /** |
174 | * @param LoggerInterface $logger |
175 | */ |
176 | public function setLogger( LoggerInterface $logger ) { |
177 | $this->logger = $logger; |
178 | } |
179 | |
180 | /** |
181 | * Simple function to test if we can make any sort of requests at all, using |
182 | * cURL or fopen() |
183 | * @return bool |
184 | */ |
185 | public static function canMakeRequests() { |
186 | return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' ); |
187 | } |
188 | |
189 | /** |
190 | * Get the body, or content, of the response to the request |
191 | * |
192 | * @return string |
193 | */ |
194 | public function getContent() { |
195 | return $this->content; |
196 | } |
197 | |
198 | /** |
199 | * Set the parameters of the request |
200 | * |
201 | * @param array $args |
202 | * @todo overload the args param |
203 | */ |
204 | public function setData( array $args ) { |
205 | $this->postData = $args; |
206 | } |
207 | |
208 | /** |
209 | * Add Telemetry information to the request |
210 | * |
211 | * @param TelemetryHeadersInterface $telemetry |
212 | * @return void |
213 | */ |
214 | public function addTelemetry( TelemetryHeadersInterface $telemetry ): void { |
215 | foreach ( $telemetry->getRequestHeaders() as $header => $value ) { |
216 | $this->setHeader( $header, $value ); |
217 | } |
218 | } |
219 | |
220 | /** |
221 | * Take care of setting up the proxy (do nothing if "noProxy" is set) |
222 | * |
223 | * @return void |
224 | */ |
225 | protected function proxySetup() { |
226 | $httpProxy = MediaWikiServices::getInstance()->getMainConfig()->get( |
227 | MainConfigNames::HTTPProxy ); |
228 | $localHTTPProxy = MediaWikiServices::getInstance()->getMainConfig()->get( |
229 | MainConfigNames::LocalHTTPProxy ); |
230 | // If proxies are disabled, clear any other proxy |
231 | if ( $this->noProxy ) { |
232 | $this->proxy = ''; |
233 | return; |
234 | } |
235 | |
236 | // If there is an explicit proxy already set, use it |
237 | if ( $this->proxy ) { |
238 | return; |
239 | } |
240 | |
241 | // Otherwise, fallback to $wgLocalHTTPProxy for local URLs |
242 | // or $wgHTTPProxy for everything else |
243 | if ( self::isLocalURL( $this->url ) ) { |
244 | if ( $localHTTPProxy !== false ) { |
245 | $this->setReverseProxy( $localHTTPProxy ); |
246 | } |
247 | } else { |
248 | $this->proxy = (string)$httpProxy; |
249 | } |
250 | } |
251 | |
252 | /** |
253 | * Enable use of a reverse proxy in which the hostname is |
254 | * passed as a "Host" header, and the request is sent to the |
255 | * proxy's host:port instead. |
256 | * |
257 | * Note that any custom port in the request URL will be lost |
258 | * and cookies and redirects may not work properly. |
259 | * |
260 | * @param string $proxy URL of proxy |
261 | */ |
262 | protected function setReverseProxy( string $proxy ) { |
263 | $parsedProxy = wfParseUrl( $proxy ); |
264 | if ( $parsedProxy === false ) { |
265 | throw new InvalidArgumentException( "Invalid reverseProxy configured: $proxy" ); |
266 | } |
267 | // Set the current host in the Host header |
268 | $this->setHeader( 'Host', $this->parsedUrl['host'] ); |
269 | // Replace scheme, host and port in the request |
270 | $this->parsedUrl['scheme'] = $parsedProxy['scheme']; |
271 | $this->parsedUrl['host'] = $parsedProxy['host']; |
272 | if ( isset( $parsedProxy['port'] ) ) { |
273 | $this->parsedUrl['port'] = $parsedProxy['port']; |
274 | } else { |
275 | unset( $this->parsedUrl['port'] ); |
276 | } |
277 | $this->url = wfAssembleUrl( $this->parsedUrl ); |
278 | // Mark that we're already using a proxy |
279 | $this->noProxy = true; |
280 | } |
281 | |
282 | /** |
283 | * Check if the URL can be served by localhost |
284 | * |
285 | * @param string $url Full url to check |
286 | * @return bool |
287 | */ |
288 | private static function isLocalURL( $url ) { |
289 | if ( MW_ENTRY_POINT === 'cli' ) { |
290 | return false; |
291 | } |
292 | $localVirtualHosts = MediaWikiServices::getInstance()->getMainConfig()->get( |
293 | MainConfigNames::LocalVirtualHosts ); |
294 | |
295 | // Extract host part |
296 | $matches = []; |
297 | if ( preg_match( '!^https?://([\w.-]+)[/:].*$!', $url, $matches ) ) { |
298 | $host = $matches[1]; |
299 | // Split up dotwise |
300 | $domainParts = explode( '.', $host ); |
301 | // Check if this domain or any superdomain is listed as a local virtual host |
302 | $domainParts = array_reverse( $domainParts ); |
303 | |
304 | $domain = ''; |
305 | $countParts = count( $domainParts ); |
306 | for ( $i = 0; $i < $countParts; $i++ ) { |
307 | $domainPart = $domainParts[$i]; |
308 | if ( $i == 0 ) { |
309 | $domain = $domainPart; |
310 | } else { |
311 | $domain = $domainPart . '.' . $domain; |
312 | } |
313 | |
314 | if ( in_array( $domain, $localVirtualHosts ) ) { |
315 | return true; |
316 | } |
317 | } |
318 | } |
319 | |
320 | return false; |
321 | } |
322 | |
323 | /** |
324 | * @param string $UA |
325 | */ |
326 | public function setUserAgent( $UA ) { |
327 | $this->setHeader( 'User-Agent', $UA ); |
328 | } |
329 | |
330 | /** |
331 | * Set an arbitrary header |
332 | * @param string $name |
333 | * @param string $value |
334 | */ |
335 | public function setHeader( $name, $value ) { |
336 | // I feel like I should normalize the case here... |
337 | $this->reqHeaders[$name] = $value; |
338 | } |
339 | |
340 | /** |
341 | * Get an array of the headers |
342 | * @return array |
343 | */ |
344 | protected function getHeaderList() { |
345 | $list = []; |
346 | |
347 | if ( $this->cookieJar ) { |
348 | $this->reqHeaders['Cookie'] = |
349 | $this->cookieJar->serializeToHttpRequest( |
350 | $this->parsedUrl['path'], |
351 | $this->parsedUrl['host'] |
352 | ); |
353 | } |
354 | |
355 | foreach ( $this->reqHeaders as $name => $value ) { |
356 | $list[] = "$name: $value"; |
357 | } |
358 | |
359 | return $list; |
360 | } |
361 | |
362 | /** |
363 | * Set a read callback to accept data read from the HTTP request. |
364 | * By default, data is appended to an internal buffer which can be |
365 | * retrieved through $req->getContent(). |
366 | * |
367 | * To handle data as it comes in -- especially for large files that |
368 | * would not fit in memory -- you can instead set your own callback, |
369 | * in the form function($resource, $buffer) where the first parameter |
370 | * is the low-level resource being read (implementation specific), |
371 | * and the second parameter is the data buffer. |
372 | * |
373 | * You MUST return the number of bytes handled in the buffer; if fewer |
374 | * bytes are reported handled than were passed to you, the HTTP fetch |
375 | * will be aborted. |
376 | * |
377 | * @param callable|null $callback |
378 | * @throws InvalidArgumentException |
379 | */ |
380 | public function setCallback( $callback ) { |
381 | $this->doSetCallback( $callback ); |
382 | } |
383 | |
384 | /** |
385 | * Worker function for setting callbacks. Calls can originate both internally and externally |
386 | * via setCallback). Defaults to the internal read callback if $callback is null. |
387 | * |
388 | * @param callable|null $callback |
389 | * @throws InvalidArgumentException |
390 | */ |
391 | protected function doSetCallback( $callback ) { |
392 | if ( $callback === null ) { |
393 | $callback = [ $this, 'read' ]; |
394 | } elseif ( !is_callable( $callback ) ) { |
395 | $this->status->fatal( 'http-internal-error' ); |
396 | throw new InvalidArgumentException( __METHOD__ . ': invalid callback' ); |
397 | } |
398 | $this->callback = $callback; |
399 | } |
400 | |
401 | /** |
402 | * A generic callback to read the body of the response from a remote |
403 | * server. |
404 | * |
405 | * @param resource $fh |
406 | * @param string $content |
407 | * @return int |
408 | * @internal |
409 | */ |
410 | public function read( $fh, $content ) { |
411 | $this->content .= $content; |
412 | return strlen( $content ); |
413 | } |
414 | |
415 | /** |
416 | * Take care of whatever is necessary to perform the URI request. |
417 | * |
418 | * @return Status |
419 | * @note currently returns Status for B/C |
420 | */ |
421 | public function execute() { |
422 | throw new LogicException( 'children must override this' ); |
423 | } |
424 | |
425 | protected function prepare() { |
426 | $this->content = ""; |
427 | |
428 | if ( strtoupper( $this->method ) == "HEAD" ) { |
429 | $this->headersOnly = true; |
430 | } |
431 | |
432 | $this->proxySetup(); // set up any proxy as needed |
433 | |
434 | if ( !$this->callback ) { |
435 | $this->doSetCallback( null ); |
436 | } |
437 | |
438 | if ( !isset( $this->reqHeaders['User-Agent'] ) ) { |
439 | $http = MediaWikiServices::getInstance()->getHttpRequestFactory(); |
440 | $this->setUserAgent( $http->getUserAgent() ); |
441 | } |
442 | } |
443 | |
444 | /** |
445 | * Parses the headers, including the HTTP status code and any |
446 | * Set-Cookie headers. This function expects the headers to be |
447 | * found in an array in the member variable headerList. |
448 | */ |
449 | protected function parseHeader() { |
450 | $lastname = ""; |
451 | |
452 | // Failure without (valid) headers gets a response status of zero |
453 | if ( !$this->status->isOK() ) { |
454 | $this->respStatus = '0 Error'; |
455 | } |
456 | |
457 | foreach ( $this->headerList as $header ) { |
458 | if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) { |
459 | $this->respVersion = $match[1]; |
460 | $this->respStatus = $match[2]; |
461 | } elseif ( preg_match( "#^[ \t]#", $header ) ) { |
462 | $last = count( $this->respHeaders[$lastname] ) - 1; |
463 | $this->respHeaders[$lastname][$last] .= "\r\n$header"; |
464 | } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) { |
465 | $this->respHeaders[strtolower( $match[1] )][] = $match[2]; |
466 | $lastname = strtolower( $match[1] ); |
467 | } |
468 | } |
469 | |
470 | $this->parseCookies(); |
471 | } |
472 | |
473 | /** |
474 | * Sets HTTPRequest status member to a fatal value with the error |
475 | * message if the returned integer value of the status code was |
476 | * not successful (1-299) or a redirect (300-399). |
477 | * See RFC2616, section 10, http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html |
478 | * for a list of status codes. |
479 | */ |
480 | protected function setStatus() { |
481 | if ( !$this->respHeaders ) { |
482 | $this->parseHeader(); |
483 | } |
484 | |
485 | if ( (int)$this->respStatus > 0 && (int)$this->respStatus < 400 ) { |
486 | $this->status->setResult( true, (int)$this->respStatus ); |
487 | } else { |
488 | [ $code, $message ] = explode( " ", $this->respStatus, 2 ); |
489 | $this->status->setResult( false, (int)$this->respStatus ); |
490 | $this->status->fatal( "http-bad-status", $code, $message ); |
491 | } |
492 | } |
493 | |
494 | /** |
495 | * Get the integer value of the HTTP status code (e.g. 200 for "200 Ok") |
496 | * (see RFC2616, section 10, http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html |
497 | * for a list of status codes.) |
498 | * |
499 | * @return int |
500 | */ |
501 | public function getStatus() { |
502 | if ( !$this->respHeaders ) { |
503 | $this->parseHeader(); |
504 | } |
505 | |
506 | return (int)$this->respStatus; |
507 | } |
508 | |
509 | /** |
510 | * Returns true if the last status code was a redirect. |
511 | * |
512 | * @return bool |
513 | */ |
514 | public function isRedirect() { |
515 | if ( !$this->respHeaders ) { |
516 | $this->parseHeader(); |
517 | } |
518 | |
519 | $status = (int)$this->respStatus; |
520 | |
521 | if ( $status >= 300 && $status <= 303 ) { |
522 | return true; |
523 | } |
524 | |
525 | return false; |
526 | } |
527 | |
528 | /** |
529 | * Returns an associative array of response headers after the |
530 | * request has been executed. Because some headers |
531 | * (e.g. Set-Cookie) can appear more than once the, each value of |
532 | * the associative array is an array of the values given. |
533 | * Header names are always in lowercase. |
534 | * |
535 | * @return array |
536 | */ |
537 | public function getResponseHeaders() { |
538 | if ( !$this->respHeaders ) { |
539 | $this->parseHeader(); |
540 | } |
541 | |
542 | return $this->respHeaders; |
543 | } |
544 | |
545 | /** |
546 | * Returns the value of the given response header. |
547 | * |
548 | * @param string $header case-insensitive |
549 | * @return string|null |
550 | */ |
551 | public function getResponseHeader( $header ) { |
552 | if ( !$this->respHeaders ) { |
553 | $this->parseHeader(); |
554 | } |
555 | |
556 | if ( isset( $this->respHeaders[strtolower( $header )] ) ) { |
557 | $v = $this->respHeaders[strtolower( $header )]; |
558 | return $v[count( $v ) - 1]; |
559 | } |
560 | |
561 | return null; |
562 | } |
563 | |
564 | /** |
565 | * Tells the MWHttpRequest object to use this pre-loaded CookieJar. |
566 | * |
567 | * To read response cookies from the jar, getCookieJar must be called first. |
568 | * |
569 | * @param CookieJar $jar |
570 | */ |
571 | public function setCookieJar( CookieJar $jar ) { |
572 | $this->cookieJar = $jar; |
573 | } |
574 | |
575 | /** |
576 | * Returns the cookie jar in use. |
577 | * |
578 | * @return CookieJar |
579 | */ |
580 | public function getCookieJar() { |
581 | if ( !$this->respHeaders ) { |
582 | $this->parseHeader(); |
583 | } |
584 | |
585 | return $this->cookieJar; |
586 | } |
587 | |
588 | /** |
589 | * Sets a cookie. Used before a request to set up any individual |
590 | * cookies. Used internally after a request to parse the |
591 | * Set-Cookie headers. |
592 | * @see Cookie::set |
593 | * @param string $name |
594 | * @param string $value |
595 | * @param array $attr |
596 | */ |
597 | public function setCookie( $name, $value, array $attr = [] ) { |
598 | if ( !$this->cookieJar ) { |
599 | $this->cookieJar = new CookieJar; |
600 | } |
601 | |
602 | if ( $this->parsedUrl && !isset( $attr['domain'] ) ) { |
603 | $attr['domain'] = $this->parsedUrl['host']; |
604 | } |
605 | |
606 | $this->cookieJar->setCookie( $name, $value, $attr ); |
607 | } |
608 | |
609 | /** |
610 | * Parse the cookies in the response headers and store them in the cookie jar. |
611 | */ |
612 | protected function parseCookies() { |
613 | if ( !$this->cookieJar ) { |
614 | $this->cookieJar = new CookieJar; |
615 | } |
616 | |
617 | if ( isset( $this->respHeaders['set-cookie'] ) ) { |
618 | $url = parse_url( $this->getFinalUrl() ); |
619 | if ( !isset( $url['host'] ) ) { |
620 | $this->status->fatal( 'http-invalid-url', $url ); |
621 | } else { |
622 | foreach ( $this->respHeaders['set-cookie'] as $cookie ) { |
623 | $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] ); |
624 | } |
625 | } |
626 | } |
627 | } |
628 | |
629 | /** |
630 | * Returns the final URL after all redirections. |
631 | * |
632 | * Relative values of the "Location" header are incorrect as |
633 | * stated in RFC, however they do happen and modern browsers |
634 | * support them. This function loops backwards through all |
635 | * locations in order to build the proper absolute URI - Marooned |
636 | * at wikia-inc.com |
637 | * |
638 | * Note that the multiple Location: headers are an artifact of |
639 | * CURL -- they shouldn't actually get returned this way. Rewrite |
640 | * this when T31232 is taken care of (high-level redirect |
641 | * handling rewrite). |
642 | * |
643 | * @return string |
644 | */ |
645 | public function getFinalUrl() { |
646 | $headers = $this->getResponseHeaders(); |
647 | |
648 | // return full url (fix for incorrect but handled relative location) |
649 | if ( isset( $headers['location'] ) ) { |
650 | $locations = $headers['location']; |
651 | $domain = ''; |
652 | $foundRelativeURI = false; |
653 | $countLocations = count( $locations ); |
654 | |
655 | for ( $i = $countLocations - 1; $i >= 0; $i-- ) { |
656 | $url = parse_url( $locations[$i] ); |
657 | |
658 | if ( isset( $url['scheme'] ) && isset( $url['host'] ) ) { |
659 | $domain = $url['scheme'] . '://' . $url['host']; |
660 | break; // found correct URI (with host) |
661 | } else { |
662 | $foundRelativeURI = true; |
663 | } |
664 | } |
665 | |
666 | if ( !$foundRelativeURI ) { |
667 | return $locations[$countLocations - 1]; |
668 | } |
669 | if ( $domain ) { |
670 | return $domain . $locations[$countLocations - 1]; |
671 | } |
672 | $url = parse_url( $this->url ); |
673 | if ( isset( $url['scheme'] ) && isset( $url['host'] ) ) { |
674 | return $url['scheme'] . '://' . $url['host'] . |
675 | $locations[$countLocations - 1]; |
676 | } |
677 | } |
678 | |
679 | return $this->url; |
680 | } |
681 | |
682 | /** |
683 | * Returns true if the backend can follow redirects. Overridden by the |
684 | * child classes. |
685 | * @return bool |
686 | */ |
687 | public function canFollowRedirects() { |
688 | return true; |
689 | } |
690 | |
691 | /** |
692 | * Set information about the original request. This can be useful for |
693 | * endpoints/API modules which act as a proxy for some service, and |
694 | * throttling etc. needs to happen in that service. |
695 | * Calling this will result in the X-Forwarded-For and X-Original-User-Agent |
696 | * headers being set. |
697 | * @param WebRequest|array $originalRequest When in array form, it's |
698 | * expected to have the keys 'ip' and 'userAgent'. |
699 | * @note IP/user agent is personally identifiable information, and should |
700 | * only be set when the privacy policy of the request target is |
701 | * compatible with that of the MediaWiki installation. |
702 | */ |
703 | public function setOriginalRequest( $originalRequest ) { |
704 | if ( $originalRequest instanceof WebRequest ) { |
705 | $originalRequest = [ |
706 | 'ip' => $originalRequest->getIP(), |
707 | 'userAgent' => $originalRequest->getHeader( 'User-Agent' ), |
708 | ]; |
709 | } elseif ( |
710 | !is_array( $originalRequest ) |
711 | || array_diff( [ 'ip', 'userAgent' ], array_keys( $originalRequest ) ) |
712 | ) { |
713 | throw new InvalidArgumentException( __METHOD__ . ': $originalRequest must be a ' |
714 | . "WebRequest or an array with 'ip' and 'userAgent' keys" ); |
715 | } |
716 | |
717 | $this->reqHeaders['X-Forwarded-For'] = $originalRequest['ip']; |
718 | $this->reqHeaders['X-Original-User-Agent'] = $originalRequest['userAgent']; |
719 | } |
720 | |
721 | /** |
722 | * Check that the given URI is a valid one. |
723 | * |
724 | * This hardcodes a small set of protocols only, because we want to |
725 | * deterministically reject protocols not supported by all HTTP-transport |
726 | * methods. |
727 | * |
728 | * "file://" specifically must not be allowed, for security reasons |
729 | * (see <https://www.mediawiki.org/wiki/Special:Code/MediaWiki/r67684>). |
730 | * |
731 | * @todo FIXME this is wildly inaccurate and fails to actually check most stuff |
732 | * |
733 | * @since 1.34 |
734 | * @param string $uri URI to check for validity |
735 | * @return bool |
736 | */ |
737 | public static function isValidURI( $uri ) { |
738 | return (bool)preg_match( |
739 | '/^https?:\/\/[^\/\s]\S*$/D', |
740 | $uri |
741 | ); |
742 | } |
743 | } |