[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Various HTTP related functions. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup HTTP 22 */ 23 24 /** 25 * @defgroup HTTP HTTP 26 */ 27 28 /** 29 * Various HTTP related functions 30 * @ingroup HTTP 31 */ 32 class Http { 33 static public $httpEngine = false; 34 35 /** 36 * Perform an HTTP request 37 * 38 * @param string $method HTTP method. Usually GET/POST 39 * @param string $url Full URL to act on. If protocol-relative, will be expanded to an http:// URL 40 * @param array $options Options to pass to MWHttpRequest object. 41 * Possible keys for the array: 42 * - timeout Timeout length in seconds 43 * - connectTimeout Timeout for connection, in seconds (curl only) 44 * - postData An array of key-value pairs or a url-encoded form data 45 * - proxy The proxy to use. 46 * Otherwise it will use $wgHTTPProxy (if set) 47 * Otherwise it will use the environment variable "http_proxy" (if set) 48 * - noProxy Don't use any proxy at all. Takes precedence over proxy value(s). 49 * - sslVerifyHost Verify hostname against certificate 50 * - sslVerifyCert Verify SSL certificate 51 * - caInfo Provide CA information 52 * - maxRedirects Maximum number of redirects to follow (defaults to 5) 53 * - followRedirects Whether to follow redirects (defaults to false). 54 * Note: this should only be used when the target URL is trusted, 55 * to avoid attacks on intranet services accessible by HTTP. 56 * - userAgent A user agent, if you want to override the default 57 * MediaWiki/$wgVersion 58 * @return string|bool (bool)false on failure or a string on success 59 */ 60 public static function request( $method, $url, $options = array() ) { 61 wfDebug( "HTTP: $method: $url\n" ); 62 wfProfileIn( __METHOD__ . "-$method" ); 63 64 $options['method'] = strtoupper( $method ); 65 66 if ( !isset( $options['timeout'] ) ) { 67 $options['timeout'] = 'default'; 68 } 69 if ( !isset( $options['connectTimeout'] ) ) { 70 $options['connectTimeout'] = 'default'; 71 } 72 73 $req = MWHttpRequest::factory( $url, $options ); 74 $status = $req->execute(); 75 76 $content = false; 77 if ( $status->isOK() ) { 78 $content = $req->getContent(); 79 } 80 wfProfileOut( __METHOD__ . "-$method" ); 81 return $content; 82 } 83 84 /** 85 * Simple wrapper for Http::request( 'GET' ) 86 * @see Http::request() 87 * 88 * @param string $url 89 * @param string $timeout 90 * @param array $options 91 * @return string 92 */ 93 public static function get( $url, $timeout = 'default', $options = array() ) { 94 $options['timeout'] = $timeout; 95 return Http::request( 'GET', $url, $options ); 96 } 97 98 /** 99 * Simple wrapper for Http::request( 'POST' ) 100 * @see Http::request() 101 * 102 * @param string $url 103 * @param array $options 104 * @return string 105 */ 106 public static function post( $url, $options = array() ) { 107 return Http::request( 'POST', $url, $options ); 108 } 109 110 /** 111 * Check if the URL can be served by localhost 112 * 113 * @param string $url Full url to check 114 * @return bool 115 */ 116 public static function isLocalURL( $url ) { 117 global $wgCommandLineMode, $wgConf; 118 119 if ( $wgCommandLineMode ) { 120 return false; 121 } 122 123 // Extract host part 124 $matches = array(); 125 if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) { 126 $host = $matches[1]; 127 // Split up dotwise 128 $domainParts = explode( '.', $host ); 129 // Check if this domain or any superdomain is listed in $wgConf as a local virtual host 130 $domainParts = array_reverse( $domainParts ); 131 132 $domain = ''; 133 $countParts = count( $domainParts ); 134 for ( $i = 0; $i < $countParts; $i++ ) { 135 $domainPart = $domainParts[$i]; 136 if ( $i == 0 ) { 137 $domain = $domainPart; 138 } else { 139 $domain = $domainPart . '.' . $domain; 140 } 141 142 if ( $wgConf->isLocalVHost( $domain ) ) { 143 return true; 144 } 145 } 146 } 147 148 return false; 149 } 150 151 /** 152 * A standard user-agent we can use for external requests. 153 * @return string 154 */ 155 public static function userAgent() { 156 global $wgVersion; 157 return "MediaWiki/$wgVersion"; 158 } 159 160 /** 161 * Checks that the given URI is a valid one. Hardcoding the 162 * protocols, because we only want protocols that both cURL 163 * and php support. 164 * 165 * file:// should not be allowed here for security purpose (r67684) 166 * 167 * @todo FIXME this is wildly inaccurate and fails to actually check most stuff 168 * 169 * @param string $uri URI to check for validity 170 * @return bool 171 */ 172 public static function isValidURI( $uri ) { 173 return preg_match( 174 '/^https?:\/\/[^\/\s]\S*$/D', 175 $uri 176 ); 177 } 178 } 179 180 /** 181 * This wrapper class will call out to curl (if available) or fallback 182 * to regular PHP if necessary for handling internal HTTP requests. 183 * 184 * Renamed from HttpRequest to MWHttpRequest to avoid conflict with 185 * PHP's HTTP extension. 186 */ 187 class MWHttpRequest { 188 const SUPPORTS_FILE_POSTS = false; 189 190 protected $content; 191 protected $timeout = 'default'; 192 protected $headersOnly = null; 193 protected $postData = null; 194 protected $proxy = null; 195 protected $noProxy = false; 196 protected $sslVerifyHost = true; 197 protected $sslVerifyCert = true; 198 protected $caInfo = null; 199 protected $method = "GET"; 200 protected $reqHeaders = array(); 201 protected $url; 202 protected $parsedUrl; 203 protected $callback; 204 protected $maxRedirects = 5; 205 protected $followRedirects = false; 206 207 /** 208 * @var CookieJar 209 */ 210 protected $cookieJar; 211 212 protected $headerList = array(); 213 protected $respVersion = "0.9"; 214 protected $respStatus = "200 Ok"; 215 protected $respHeaders = array(); 216 217 public $status; 218 219 /** 220 * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL 221 * @param array $options (optional) extra params to pass (see Http::request()) 222 */ 223 protected function __construct( $url, $options = array() ) { 224 global $wgHTTPTimeout, $wgHTTPConnectTimeout; 225 226 $this->url = wfExpandUrl( $url, PROTO_HTTP ); 227 $this->parsedUrl = wfParseUrl( $this->url ); 228 229 if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) { 230 $this->status = Status::newFatal( 'http-invalid-url' ); 231 } else { 232 $this->status = Status::newGood( 100 ); // continue 233 } 234 235 if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) { 236 $this->timeout = $options['timeout']; 237 } else { 238 $this->timeout = $wgHTTPTimeout; 239 } 240 if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) { 241 $this->connectTimeout = $options['connectTimeout']; 242 } else { 243 $this->connectTimeout = $wgHTTPConnectTimeout; 244 } 245 if ( isset( $options['userAgent'] ) ) { 246 $this->setUserAgent( $options['userAgent'] ); 247 } 248 249 $members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo", 250 "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ); 251 252 foreach ( $members as $o ) { 253 if ( isset( $options[$o] ) ) { 254 // ensure that MWHttpRequest::method is always 255 // uppercased. Bug 36137 256 if ( $o == 'method' ) { 257 $options[$o] = strtoupper( $options[$o] ); 258 } 259 $this->$o = $options[$o]; 260 } 261 } 262 263 if ( $this->noProxy ) { 264 $this->proxy = ''; // noProxy takes precedence 265 } 266 } 267 268 /** 269 * Simple function to test if we can make any sort of requests at all, using 270 * cURL or fopen() 271 * @return bool 272 */ 273 public static function canMakeRequests() { 274 return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' ); 275 } 276 277 /** 278 * Generate a new request object 279 * @param string $url Url to use 280 * @param array $options (optional) extra params to pass (see Http::request()) 281 * @throws MWException 282 * @return CurlHttpRequest|PhpHttpRequest 283 * @see MWHttpRequest::__construct 284 */ 285 public static function factory( $url, $options = null ) { 286 if ( !Http::$httpEngine ) { 287 Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php'; 288 } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { 289 throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . 290 ' Http::$httpEngine is set to "curl"' ); 291 } 292 293 switch ( Http::$httpEngine ) { 294 case 'curl': 295 return new CurlHttpRequest( $url, $options ); 296 case 'php': 297 if ( !wfIniGetBool( 'allow_url_fopen' ) ) { 298 throw new MWException( __METHOD__ . ': allow_url_fopen ' . 299 'needs to be enabled for pure PHP http requests to ' . 300 'work. If possible, curl should be used instead. See ' . 301 'http://php.net/curl.' 302 ); 303 } 304 return new PhpHttpRequest( $url, $options ); 305 default: 306 throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' ); 307 } 308 } 309 310 /** 311 * Get the body, or content, of the response to the request 312 * 313 * @return string 314 */ 315 public function getContent() { 316 return $this->content; 317 } 318 319 /** 320 * Set the parameters of the request 321 * 322 * @param array $args 323 * @todo overload the args param 324 */ 325 public function setData( $args ) { 326 $this->postData = $args; 327 } 328 329 /** 330 * Take care of setting up the proxy (do nothing if "noProxy" is set) 331 * 332 * @return void 333 */ 334 public function proxySetup() { 335 global $wgHTTPProxy; 336 337 // If there is an explicit proxy set and proxies are not disabled, then use it 338 if ( $this->proxy && !$this->noProxy ) { 339 return; 340 } 341 342 // Otherwise, fallback to $wgHTTPProxy/http_proxy (when set) if this is not a machine 343 // local URL and proxies are not disabled 344 if ( Http::isLocalURL( $this->url ) || $this->noProxy ) { 345 $this->proxy = ''; 346 } elseif ( $wgHTTPProxy ) { 347 $this->proxy = $wgHTTPProxy; 348 } elseif ( getenv( "http_proxy" ) ) { 349 $this->proxy = getenv( "http_proxy" ); 350 } 351 } 352 353 /** 354 * Set the user agent 355 * @param string $UA 356 */ 357 public function setUserAgent( $UA ) { 358 $this->setHeader( 'User-Agent', $UA ); 359 } 360 361 /** 362 * Set an arbitrary header 363 * @param string $name 364 * @param string $value 365 */ 366 public function setHeader( $name, $value ) { 367 // I feel like I should normalize the case here... 368 $this->reqHeaders[$name] = $value; 369 } 370 371 /** 372 * Get an array of the headers 373 * @return array 374 */ 375 public function getHeaderList() { 376 $list = array(); 377 378 if ( $this->cookieJar ) { 379 $this->reqHeaders['Cookie'] = 380 $this->cookieJar->serializeToHttpRequest( 381 $this->parsedUrl['path'], 382 $this->parsedUrl['host'] 383 ); 384 } 385 386 foreach ( $this->reqHeaders as $name => $value ) { 387 $list[] = "$name: $value"; 388 } 389 390 return $list; 391 } 392 393 /** 394 * Set a read callback to accept data read from the HTTP request. 395 * By default, data is appended to an internal buffer which can be 396 * retrieved through $req->getContent(). 397 * 398 * To handle data as it comes in -- especially for large files that 399 * would not fit in memory -- you can instead set your own callback, 400 * in the form function($resource, $buffer) where the first parameter 401 * is the low-level resource being read (implementation specific), 402 * and the second parameter is the data buffer. 403 * 404 * You MUST return the number of bytes handled in the buffer; if fewer 405 * bytes are reported handled than were passed to you, the HTTP fetch 406 * will be aborted. 407 * 408 * @param callable $callback 409 * @throws MWException 410 */ 411 public function setCallback( $callback ) { 412 if ( !is_callable( $callback ) ) { 413 throw new MWException( 'Invalid MwHttpRequest callback' ); 414 } 415 $this->callback = $callback; 416 } 417 418 /** 419 * A generic callback to read the body of the response from a remote 420 * server. 421 * 422 * @param resource $fh 423 * @param string $content 424 * @return int 425 */ 426 public function read( $fh, $content ) { 427 $this->content .= $content; 428 return strlen( $content ); 429 } 430 431 /** 432 * Take care of whatever is necessary to perform the URI request. 433 * 434 * @return Status 435 */ 436 public function execute() { 437 wfProfileIn( __METHOD__ ); 438 439 $this->content = ""; 440 441 if ( strtoupper( $this->method ) == "HEAD" ) { 442 $this->headersOnly = true; 443 } 444 445 $this->proxySetup(); // set up any proxy as needed 446 447 if ( !$this->callback ) { 448 $this->setCallback( array( $this, 'read' ) ); 449 } 450 451 if ( !isset( $this->reqHeaders['User-Agent'] ) ) { 452 $this->setUserAgent( Http::userAgent() ); 453 } 454 455 wfProfileOut( __METHOD__ ); 456 } 457 458 /** 459 * Parses the headers, including the HTTP status code and any 460 * Set-Cookie headers. This function expects the headers to be 461 * found in an array in the member variable headerList. 462 */ 463 protected function parseHeader() { 464 wfProfileIn( __METHOD__ ); 465 466 $lastname = ""; 467 468 foreach ( $this->headerList as $header ) { 469 if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) { 470 $this->respVersion = $match[1]; 471 $this->respStatus = $match[2]; 472 } elseif ( preg_match( "#^[ \t]#", $header ) ) { 473 $last = count( $this->respHeaders[$lastname] ) - 1; 474 $this->respHeaders[$lastname][$last] .= "\r\n$header"; 475 } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) { 476 $this->respHeaders[strtolower( $match[1] )][] = $match[2]; 477 $lastname = strtolower( $match[1] ); 478 } 479 } 480 481 $this->parseCookies(); 482 483 wfProfileOut( __METHOD__ ); 484 } 485 486 /** 487 * Sets HTTPRequest status member to a fatal value with the error 488 * message if the returned integer value of the status code was 489 * not successful (< 300) or a redirect (>=300 and < 400). (see 490 * RFC2616, section 10, 491 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for a 492 * list of status codes.) 493 */ 494 protected function setStatus() { 495 if ( !$this->respHeaders ) { 496 $this->parseHeader(); 497 } 498 499 if ( (int)$this->respStatus > 399 ) { 500 list( $code, $message ) = explode( " ", $this->respStatus, 2 ); 501 $this->status->fatal( "http-bad-status", $code, $message ); 502 } 503 } 504 505 /** 506 * Get the integer value of the HTTP status code (e.g. 200 for "200 Ok") 507 * (see RFC2616, section 10, http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html 508 * for a list of status codes.) 509 * 510 * @return int 511 */ 512 public function getStatus() { 513 if ( !$this->respHeaders ) { 514 $this->parseHeader(); 515 } 516 517 return (int)$this->respStatus; 518 } 519 520 /** 521 * Returns true if the last status code was a redirect. 522 * 523 * @return bool 524 */ 525 public function isRedirect() { 526 if ( !$this->respHeaders ) { 527 $this->parseHeader(); 528 } 529 530 $status = (int)$this->respStatus; 531 532 if ( $status >= 300 && $status <= 303 ) { 533 return true; 534 } 535 536 return false; 537 } 538 539 /** 540 * Returns an associative array of response headers after the 541 * request has been executed. Because some headers 542 * (e.g. Set-Cookie) can appear more than once the, each value of 543 * the associative array is an array of the values given. 544 * 545 * @return array 546 */ 547 public function getResponseHeaders() { 548 if ( !$this->respHeaders ) { 549 $this->parseHeader(); 550 } 551 552 return $this->respHeaders; 553 } 554 555 /** 556 * Returns the value of the given response header. 557 * 558 * @param string $header 559 * @return string 560 */ 561 public function getResponseHeader( $header ) { 562 if ( !$this->respHeaders ) { 563 $this->parseHeader(); 564 } 565 566 if ( isset( $this->respHeaders[strtolower( $header )] ) ) { 567 $v = $this->respHeaders[strtolower( $header )]; 568 return $v[count( $v ) - 1]; 569 } 570 571 return null; 572 } 573 574 /** 575 * Tells the MWHttpRequest object to use this pre-loaded CookieJar. 576 * 577 * @param CookieJar $jar 578 */ 579 public function setCookieJar( $jar ) { 580 $this->cookieJar = $jar; 581 } 582 583 /** 584 * Returns the cookie jar in use. 585 * 586 * @return CookieJar 587 */ 588 public function getCookieJar() { 589 if ( !$this->respHeaders ) { 590 $this->parseHeader(); 591 } 592 593 return $this->cookieJar; 594 } 595 596 /** 597 * Sets a cookie. Used before a request to set up any individual 598 * cookies. Used internally after a request to parse the 599 * Set-Cookie headers. 600 * @see Cookie::set 601 * @param string $name 602 * @param mixed $value 603 * @param array $attr 604 */ 605 public function setCookie( $name, $value = null, $attr = null ) { 606 if ( !$this->cookieJar ) { 607 $this->cookieJar = new CookieJar; 608 } 609 610 $this->cookieJar->setCookie( $name, $value, $attr ); 611 } 612 613 /** 614 * Parse the cookies in the response headers and store them in the cookie jar. 615 */ 616 protected function parseCookies() { 617 wfProfileIn( __METHOD__ ); 618 619 if ( !$this->cookieJar ) { 620 $this->cookieJar = new CookieJar; 621 } 622 623 if ( isset( $this->respHeaders['set-cookie'] ) ) { 624 $url = parse_url( $this->getFinalUrl() ); 625 foreach ( $this->respHeaders['set-cookie'] as $cookie ) { 626 $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] ); 627 } 628 } 629 630 wfProfileOut( __METHOD__ ); 631 } 632 633 /** 634 * Returns the final URL after all redirections. 635 * 636 * Relative values of the "Location" header are incorrect as 637 * stated in RFC, however they do happen and modern browsers 638 * support them. This function loops backwards through all 639 * locations in order to build the proper absolute URI - Marooned 640 * at wikia-inc.com 641 * 642 * Note that the multiple Location: headers are an artifact of 643 * CURL -- they shouldn't actually get returned this way. Rewrite 644 * this when bug 29232 is taken care of (high-level redirect 645 * handling rewrite). 646 * 647 * @return string 648 */ 649 public function getFinalUrl() { 650 $headers = $this->getResponseHeaders(); 651 652 //return full url (fix for incorrect but handled relative location) 653 if ( isset( $headers['location'] ) ) { 654 $locations = $headers['location']; 655 $domain = ''; 656 $foundRelativeURI = false; 657 $countLocations = count( $locations ); 658 659 for ( $i = $countLocations - 1; $i >= 0; $i-- ) { 660 $url = parse_url( $locations[$i] ); 661 662 if ( isset( $url['host'] ) ) { 663 $domain = $url['scheme'] . '://' . $url['host']; 664 break; //found correct URI (with host) 665 } else { 666 $foundRelativeURI = true; 667 } 668 } 669 670 if ( $foundRelativeURI ) { 671 if ( $domain ) { 672 return $domain . $locations[$countLocations - 1]; 673 } else { 674 $url = parse_url( $this->url ); 675 if ( isset( $url['host'] ) ) { 676 return $url['scheme'] . '://' . $url['host'] . 677 $locations[$countLocations - 1]; 678 } 679 } 680 } else { 681 return $locations[$countLocations - 1]; 682 } 683 } 684 685 return $this->url; 686 } 687 688 /** 689 * Returns true if the backend can follow redirects. Overridden by the 690 * child classes. 691 * @return bool 692 */ 693 public function canFollowRedirects() { 694 return true; 695 } 696 } 697 698 /** 699 * MWHttpRequest implemented using internal curl compiled into PHP 700 */ 701 class CurlHttpRequest extends MWHttpRequest { 702 const SUPPORTS_FILE_POSTS = true; 703 704 protected $curlOptions = array(); 705 protected $headerText = ""; 706 707 /** 708 * @param resource $fh 709 * @param string $content 710 * @return int 711 */ 712 protected function readHeader( $fh, $content ) { 713 $this->headerText .= $content; 714 return strlen( $content ); 715 } 716 717 public function execute() { 718 wfProfileIn( __METHOD__ ); 719 720 parent::execute(); 721 722 if ( !$this->status->isOK() ) { 723 wfProfileOut( __METHOD__ ); 724 return $this->status; 725 } 726 727 $this->curlOptions[CURLOPT_PROXY] = $this->proxy; 728 $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout; 729 730 // Only supported in curl >= 7.16.2 731 if ( defined( 'CURLOPT_CONNECTTIMEOUT_MS' ) ) { 732 $this->curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = $this->connectTimeout * 1000; 733 } 734 735 $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; 736 $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback; 737 $this->curlOptions[CURLOPT_HEADERFUNCTION] = array( $this, "readHeader" ); 738 $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects; 739 $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression 740 741 $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent']; 742 743 $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0; 744 $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert; 745 746 if ( $this->caInfo ) { 747 $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo; 748 } 749 750 if ( $this->headersOnly ) { 751 $this->curlOptions[CURLOPT_NOBODY] = true; 752 $this->curlOptions[CURLOPT_HEADER] = true; 753 } elseif ( $this->method == 'POST' ) { 754 $this->curlOptions[CURLOPT_POST] = true; 755 $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData; 756 // Suppress 'Expect: 100-continue' header, as some servers 757 // will reject it with a 417 and Curl won't auto retry 758 // with HTTP 1.0 fallback 759 $this->reqHeaders['Expect'] = ''; 760 } else { 761 $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method; 762 } 763 764 $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList(); 765 766 $curlHandle = curl_init( $this->url ); 767 768 if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) { 769 wfProfileOut( __METHOD__ ); 770 throw new MWException( "Error setting curl options." ); 771 } 772 773 if ( $this->followRedirects && $this->canFollowRedirects() ) { 774 wfSuppressWarnings(); 775 if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) { 776 wfDebug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " . 777 "Probably safe_mode or open_basedir is set.\n" ); 778 // Continue the processing. If it were in curl_setopt_array, 779 // processing would have halted on its entry 780 } 781 wfRestoreWarnings(); 782 } 783 784 $curlRes = curl_exec( $curlHandle ); 785 if ( curl_errno( $curlHandle ) == CURLE_OPERATION_TIMEOUTED ) { 786 $this->status->fatal( 'http-timed-out', $this->url ); 787 } elseif ( $curlRes === false ) { 788 $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) ); 789 } else { 790 $this->headerList = explode( "\r\n", $this->headerText ); 791 } 792 793 curl_close( $curlHandle ); 794 795 $this->parseHeader(); 796 $this->setStatus(); 797 798 wfProfileOut( __METHOD__ ); 799 800 return $this->status; 801 } 802 803 /** 804 * @return bool 805 */ 806 public function canFollowRedirects() { 807 if ( strval( ini_get( 'open_basedir' ) ) !== '' || wfIniGetBool( 'safe_mode' ) ) { 808 wfDebug( "Cannot follow redirects in safe mode\n" ); 809 return false; 810 } 811 812 $curlVersionInfo = curl_version(); 813 if ( $curlVersionInfo['version_number'] < 0x071304 ) { 814 wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" ); 815 return false; 816 } 817 818 return true; 819 } 820 } 821 822 class PhpHttpRequest extends MWHttpRequest { 823 824 /** 825 * @param string $url 826 * @return string 827 */ 828 protected function urlToTcp( $url ) { 829 $parsedUrl = parse_url( $url ); 830 831 return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port']; 832 } 833 834 public function execute() { 835 wfProfileIn( __METHOD__ ); 836 837 parent::execute(); 838 839 if ( is_array( $this->postData ) ) { 840 $this->postData = wfArrayToCgi( $this->postData ); 841 } 842 843 if ( $this->parsedUrl['scheme'] != 'http' 844 && $this->parsedUrl['scheme'] != 'https' ) { 845 $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] ); 846 } 847 848 $this->reqHeaders['Accept'] = "*/*"; 849 $this->reqHeaders['Connection'] = 'Close'; 850 if ( $this->method == 'POST' ) { 851 // Required for HTTP 1.0 POSTs 852 $this->reqHeaders['Content-Length'] = strlen( $this->postData ); 853 if ( !isset( $this->reqHeaders['Content-Type'] ) ) { 854 $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded"; 855 } 856 } 857 858 // Set up PHP stream context 859 $options = array( 860 'http' => array( 861 'method' => $this->method, 862 'header' => implode( "\r\n", $this->getHeaderList() ), 863 'protocol_version' => '1.1', 864 'max_redirects' => $this->followRedirects ? $this->maxRedirects : 0, 865 'ignore_errors' => true, 866 'timeout' => $this->timeout, 867 // Curl options in case curlwrappers are installed 868 'curl_verify_ssl_host' => $this->sslVerifyHost ? 2 : 0, 869 'curl_verify_ssl_peer' => $this->sslVerifyCert, 870 ), 871 'ssl' => array( 872 'verify_peer' => $this->sslVerifyCert, 873 'SNI_enabled' => true, 874 ), 875 ); 876 877 if ( $this->proxy ) { 878 $options['http']['proxy'] = $this->urlToTCP( $this->proxy ); 879 $options['http']['request_fulluri'] = true; 880 } 881 882 if ( $this->postData ) { 883 $options['http']['content'] = $this->postData; 884 } 885 886 if ( $this->sslVerifyHost ) { 887 $options['ssl']['CN_match'] = $this->parsedUrl['host']; 888 } 889 890 if ( is_dir( $this->caInfo ) ) { 891 $options['ssl']['capath'] = $this->caInfo; 892 } elseif ( is_file( $this->caInfo ) ) { 893 $options['ssl']['cafile'] = $this->caInfo; 894 } elseif ( $this->caInfo ) { 895 throw new MWException( "Invalid CA info passed: {$this->caInfo}" ); 896 } 897 898 $context = stream_context_create( $options ); 899 900 $this->headerList = array(); 901 $reqCount = 0; 902 $url = $this->url; 903 904 $result = array(); 905 906 do { 907 $reqCount++; 908 wfSuppressWarnings(); 909 $fh = fopen( $url, "r", false, $context ); 910 wfRestoreWarnings(); 911 912 if ( !$fh ) { 913 break; 914 } 915 916 $result = stream_get_meta_data( $fh ); 917 $this->headerList = $result['wrapper_data']; 918 $this->parseHeader(); 919 920 if ( !$this->followRedirects ) { 921 break; 922 } 923 924 # Handle manual redirection 925 if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) { 926 break; 927 } 928 # Check security of URL 929 $url = $this->getResponseHeader( "Location" ); 930 931 if ( !Http::isValidURI( $url ) ) { 932 wfDebug( __METHOD__ . ": insecure redirection\n" ); 933 break; 934 } 935 } while ( true ); 936 937 $this->setStatus(); 938 939 if ( $fh === false ) { 940 $this->status->fatal( 'http-request-error' ); 941 wfProfileOut( __METHOD__ ); 942 return $this->status; 943 } 944 945 if ( $result['timed_out'] ) { 946 $this->status->fatal( 'http-timed-out', $this->url ); 947 wfProfileOut( __METHOD__ ); 948 return $this->status; 949 } 950 951 // If everything went OK, or we received some error code 952 // get the response body content. 953 if ( $this->status->isOK() || (int)$this->respStatus >= 300 ) { 954 while ( !feof( $fh ) ) { 955 $buf = fread( $fh, 8192 ); 956 957 if ( $buf === false ) { 958 $this->status->fatal( 'http-read-error' ); 959 break; 960 } 961 962 if ( strlen( $buf ) ) { 963 call_user_func( $this->callback, $fh, $buf ); 964 } 965 } 966 } 967 fclose( $fh ); 968 969 wfProfileOut( __METHOD__ ); 970 971 return $this->status; 972 } 973 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |