MediaWiki
REL1_21
|
00001 <?php 00032 class Http { 00033 static $httpEngine = false; 00034 00059 public static function request( $method, $url, $options = array() ) { 00060 wfDebug( "HTTP: $method: $url\n" ); 00061 $options['method'] = strtoupper( $method ); 00062 00063 if ( !isset( $options['timeout'] ) ) { 00064 $options['timeout'] = 'default'; 00065 } 00066 00067 $req = MWHttpRequest::factory( $url, $options ); 00068 $status = $req->execute(); 00069 00070 if ( $status->isOK() ) { 00071 return $req->getContent(); 00072 } else { 00073 return false; 00074 } 00075 } 00076 00086 public static function get( $url, $timeout = 'default', $options = array() ) { 00087 $options['timeout'] = $timeout; 00088 return Http::request( 'GET', $url, $options ); 00089 } 00090 00099 public static function post( $url, $options = array() ) { 00100 return Http::request( 'POST', $url, $options ); 00101 } 00102 00109 public static function isLocalURL( $url ) { 00110 global $wgCommandLineMode, $wgConf; 00111 00112 if ( $wgCommandLineMode ) { 00113 return false; 00114 } 00115 00116 // Extract host part 00117 $matches = array(); 00118 if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) { 00119 $host = $matches[1]; 00120 // Split up dotwise 00121 $domainParts = explode( '.', $host ); 00122 // Check if this domain or any superdomain is listed in $wgConf as a local virtual host 00123 $domainParts = array_reverse( $domainParts ); 00124 00125 $domain = ''; 00126 for ( $i = 0; $i < count( $domainParts ); $i++ ) { 00127 $domainPart = $domainParts[$i]; 00128 if ( $i == 0 ) { 00129 $domain = $domainPart; 00130 } else { 00131 $domain = $domainPart . '.' . $domain; 00132 } 00133 00134 if ( $wgConf->isLocalVHost( $domain ) ) { 00135 return true; 00136 } 00137 } 00138 } 00139 00140 return false; 00141 } 00142 00147 public static function userAgent() { 00148 global $wgVersion; 00149 return "MediaWiki/$wgVersion"; 00150 } 00151 00164 public static function isValidURI( $uri ) { 00165 return preg_match( 00166 '/^https?:\/\/[^\/\s]\S*$/D', 00167 $uri 00168 ); 00169 } 00170 } 00171 00179 class MWHttpRequest { 00180 const SUPPORTS_FILE_POSTS = false; 00181 00182 protected $content; 00183 protected $timeout = 'default'; 00184 protected $headersOnly = null; 00185 protected $postData = null; 00186 protected $proxy = null; 00187 protected $noProxy = false; 00188 protected $sslVerifyHost = true; 00189 protected $sslVerifyCert = true; 00190 protected $caInfo = null; 00191 protected $method = "GET"; 00192 protected $reqHeaders = array(); 00193 protected $url; 00194 protected $parsedUrl; 00195 protected $callback; 00196 protected $maxRedirects = 5; 00197 protected $followRedirects = false; 00198 00202 protected $cookieJar; 00203 00204 protected $headerList = array(); 00205 protected $respVersion = "0.9"; 00206 protected $respStatus = "200 Ok"; 00207 protected $respHeaders = array(); 00208 00209 public $status; 00210 00215 protected function __construct( $url, $options = array() ) { 00216 global $wgHTTPTimeout; 00217 00218 $this->url = wfExpandUrl( $url, PROTO_HTTP ); 00219 $this->parsedUrl = wfParseUrl( $this->url ); 00220 00221 if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) { 00222 $this->status = Status::newFatal( 'http-invalid-url' ); 00223 } else { 00224 $this->status = Status::newGood( 100 ); // continue 00225 } 00226 00227 if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) { 00228 $this->timeout = $options['timeout']; 00229 } else { 00230 $this->timeout = $wgHTTPTimeout; 00231 } 00232 if( isset( $options['userAgent'] ) ) { 00233 $this->setUserAgent( $options['userAgent'] ); 00234 } 00235 00236 $members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo", 00237 "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ); 00238 00239 foreach ( $members as $o ) { 00240 if ( isset( $options[$o] ) ) { 00241 // ensure that MWHttpRequest::method is always 00242 // uppercased. Bug 36137 00243 if ( $o == 'method' ) { 00244 $options[$o] = strtoupper( $options[$o] ); 00245 } 00246 $this->$o = $options[$o]; 00247 } 00248 } 00249 00250 if ( $this->noProxy ) { 00251 $this->proxy = ''; // noProxy takes precedence 00252 } 00253 } 00254 00260 public static function canMakeRequests() { 00261 return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' ); 00262 } 00263 00272 public static function factory( $url, $options = null ) { 00273 if ( !Http::$httpEngine ) { 00274 Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php'; 00275 } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { 00276 throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . 00277 ' Http::$httpEngine is set to "curl"' ); 00278 } 00279 00280 switch( Http::$httpEngine ) { 00281 case 'curl': 00282 return new CurlHttpRequest( $url, $options ); 00283 case 'php': 00284 if ( !wfIniGetBool( 'allow_url_fopen' ) ) { 00285 throw new MWException( __METHOD__ . ': allow_url_fopen needs to be enabled for pure PHP' . 00286 ' http requests to work. If possible, curl should be used instead. See http://php.net/curl.' ); 00287 } 00288 return new PhpHttpRequest( $url, $options ); 00289 default: 00290 throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' ); 00291 } 00292 } 00293 00299 public function getContent() { 00300 return $this->content; 00301 } 00302 00309 public function setData( $args ) { 00310 $this->postData = $args; 00311 } 00312 00318 public function proxySetup() { 00319 global $wgHTTPProxy; 00320 00321 // If there is an explicit proxy set and proxies are not disabled, then use it 00322 if ( $this->proxy && !$this->noProxy ) { 00323 return; 00324 } 00325 00326 // Otherwise, fallback to $wgHTTPProxy/http_proxy (when set) if this is not a machine 00327 // local URL and proxies are not disabled 00328 if ( Http::isLocalURL( $this->url ) || $this->noProxy ) { 00329 $this->proxy = ''; 00330 } elseif ( $wgHTTPProxy ) { 00331 $this->proxy = $wgHTTPProxy; 00332 } elseif ( getenv( "http_proxy" ) ) { 00333 $this->proxy = getenv( "http_proxy" ); 00334 } 00335 } 00336 00340 public function setReferer( $url ) { 00341 $this->setHeader( 'Referer', $url ); 00342 } 00343 00348 public function setUserAgent( $UA ) { 00349 $this->setHeader( 'User-Agent', $UA ); 00350 } 00351 00357 public function setHeader( $name, $value ) { 00358 // I feel like I should normalize the case here... 00359 $this->reqHeaders[$name] = $value; 00360 } 00361 00366 public function getHeaderList() { 00367 $list = array(); 00368 00369 if ( $this->cookieJar ) { 00370 $this->reqHeaders['Cookie'] = 00371 $this->cookieJar->serializeToHttpRequest( 00372 $this->parsedUrl['path'], 00373 $this->parsedUrl['host'] 00374 ); 00375 } 00376 00377 foreach ( $this->reqHeaders as $name => $value ) { 00378 $list[] = "$name: $value"; 00379 } 00380 00381 return $list; 00382 } 00383 00402 public function setCallback( $callback ) { 00403 if ( !is_callable( $callback ) ) { 00404 throw new MWException( 'Invalid MwHttpRequest callback' ); 00405 } 00406 $this->callback = $callback; 00407 } 00408 00417 public function read( $fh, $content ) { 00418 $this->content .= $content; 00419 return strlen( $content ); 00420 } 00421 00427 public function execute() { 00428 global $wgTitle; 00429 00430 $this->content = ""; 00431 00432 if ( strtoupper( $this->method ) == "HEAD" ) { 00433 $this->headersOnly = true; 00434 } 00435 00436 if ( is_object( $wgTitle ) && !isset( $this->reqHeaders['Referer'] ) ) { 00437 $this->setReferer( wfExpandUrl( $wgTitle->getFullURL(), PROTO_CURRENT ) ); 00438 } 00439 00440 $this->proxySetup(); // set up any proxy as needed 00441 00442 if ( !$this->callback ) { 00443 $this->setCallback( array( $this, 'read' ) ); 00444 } 00445 00446 if ( !isset( $this->reqHeaders['User-Agent'] ) ) { 00447 $this->setUserAgent( Http::userAgent() ); 00448 } 00449 } 00450 00456 protected function parseHeader() { 00457 $lastname = ""; 00458 00459 foreach ( $this->headerList as $header ) { 00460 if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) { 00461 $this->respVersion = $match[1]; 00462 $this->respStatus = $match[2]; 00463 } elseif ( preg_match( "#^[ \t]#", $header ) ) { 00464 $last = count( $this->respHeaders[$lastname] ) - 1; 00465 $this->respHeaders[$lastname][$last] .= "\r\n$header"; 00466 } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) { 00467 $this->respHeaders[strtolower( $match[1] )][] = $match[2]; 00468 $lastname = strtolower( $match[1] ); 00469 } 00470 } 00471 00472 $this->parseCookies(); 00473 } 00474 00483 protected function setStatus() { 00484 if ( !$this->respHeaders ) { 00485 $this->parseHeader(); 00486 } 00487 00488 if ( (int)$this->respStatus > 399 ) { 00489 list( $code, $message ) = explode( " ", $this->respStatus, 2 ); 00490 $this->status->fatal( "http-bad-status", $code, $message ); 00491 } 00492 } 00493 00501 public function getStatus() { 00502 if ( !$this->respHeaders ) { 00503 $this->parseHeader(); 00504 } 00505 00506 return (int)$this->respStatus; 00507 } 00508 00514 public function isRedirect() { 00515 if ( !$this->respHeaders ) { 00516 $this->parseHeader(); 00517 } 00518 00519 $status = (int)$this->respStatus; 00520 00521 if ( $status >= 300 && $status <= 303 ) { 00522 return true; 00523 } 00524 00525 return false; 00526 } 00527 00536 public function getResponseHeaders() { 00537 if ( !$this->respHeaders ) { 00538 $this->parseHeader(); 00539 } 00540 00541 return $this->respHeaders; 00542 } 00543 00550 public function getResponseHeader( $header ) { 00551 if ( !$this->respHeaders ) { 00552 $this->parseHeader(); 00553 } 00554 00555 if ( isset( $this->respHeaders[strtolower ( $header ) ] ) ) { 00556 $v = $this->respHeaders[strtolower ( $header ) ]; 00557 return $v[count( $v ) - 1]; 00558 } 00559 00560 return null; 00561 } 00562 00568 public function setCookieJar( $jar ) { 00569 $this->cookieJar = $jar; 00570 } 00571 00577 public function getCookieJar() { 00578 if ( !$this->respHeaders ) { 00579 $this->parseHeader(); 00580 } 00581 00582 return $this->cookieJar; 00583 } 00584 00594 public function setCookie( $name, $value = null, $attr = null ) { 00595 if ( !$this->cookieJar ) { 00596 $this->cookieJar = new CookieJar; 00597 } 00598 00599 $this->cookieJar->setCookie( $name, $value, $attr ); 00600 } 00601 00605 protected function parseCookies() { 00606 if ( !$this->cookieJar ) { 00607 $this->cookieJar = new CookieJar; 00608 } 00609 00610 if ( isset( $this->respHeaders['set-cookie'] ) ) { 00611 $url = parse_url( $this->getFinalUrl() ); 00612 foreach ( $this->respHeaders['set-cookie'] as $cookie ) { 00613 $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] ); 00614 } 00615 } 00616 } 00617 00630 public function getFinalUrl() { 00631 $headers = $this->getResponseHeaders(); 00632 00633 //return full url (fix for incorrect but handled relative location) 00634 if ( isset( $headers[ 'location' ] ) ) { 00635 $locations = $headers[ 'location' ]; 00636 $domain = ''; 00637 $foundRelativeURI = false; 00638 $countLocations = count( $locations ); 00639 00640 for ( $i = $countLocations - 1; $i >= 0; $i-- ) { 00641 $url = parse_url( $locations[ $i ] ); 00642 00643 if ( isset( $url['host'] ) ) { 00644 $domain = $url[ 'scheme' ] . '://' . $url[ 'host' ]; 00645 break; //found correct URI (with host) 00646 } else { 00647 $foundRelativeURI = true; 00648 } 00649 } 00650 00651 if ( $foundRelativeURI ) { 00652 if ( $domain ) { 00653 return $domain . $locations[ $countLocations - 1 ]; 00654 } else { 00655 $url = parse_url( $this->url ); 00656 if ( isset($url[ 'host' ]) ) { 00657 return $url[ 'scheme' ] . '://' . $url[ 'host' ] . $locations[ $countLocations - 1 ]; 00658 } 00659 } 00660 } else { 00661 return $locations[ $countLocations - 1 ]; 00662 } 00663 } 00664 00665 return $this->url; 00666 } 00667 00673 public function canFollowRedirects() { 00674 return true; 00675 } 00676 } 00677 00681 class CurlHttpRequest extends MWHttpRequest { 00682 const SUPPORTS_FILE_POSTS = true; 00683 00684 static $curlMessageMap = array( 00685 6 => 'http-host-unreachable', 00686 28 => 'http-timed-out' 00687 ); 00688 00689 protected $curlOptions = array(); 00690 protected $headerText = ""; 00691 00697 protected function readHeader( $fh, $content ) { 00698 $this->headerText .= $content; 00699 return strlen( $content ); 00700 } 00701 00702 public function execute() { 00703 parent::execute(); 00704 00705 if ( !$this->status->isOK() ) { 00706 return $this->status; 00707 } 00708 00709 $this->curlOptions[CURLOPT_PROXY] = $this->proxy; 00710 $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout; 00711 $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; 00712 $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback; 00713 $this->curlOptions[CURLOPT_HEADERFUNCTION] = array( $this, "readHeader" ); 00714 $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects; 00715 $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression 00716 00717 /* not sure these two are actually necessary */ 00718 if ( isset( $this->reqHeaders['Referer'] ) ) { 00719 $this->curlOptions[CURLOPT_REFERER] = $this->reqHeaders['Referer']; 00720 } 00721 $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent']; 00722 00723 $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0; 00724 $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert; 00725 00726 if ( $this->caInfo ) { 00727 $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo; 00728 } 00729 00730 if ( $this->headersOnly ) { 00731 $this->curlOptions[CURLOPT_NOBODY] = true; 00732 $this->curlOptions[CURLOPT_HEADER] = true; 00733 } elseif ( $this->method == 'POST' ) { 00734 $this->curlOptions[CURLOPT_POST] = true; 00735 $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData; 00736 // Suppress 'Expect: 100-continue' header, as some servers 00737 // will reject it with a 417 and Curl won't auto retry 00738 // with HTTP 1.0 fallback 00739 $this->reqHeaders['Expect'] = ''; 00740 } else { 00741 $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method; 00742 } 00743 00744 $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList(); 00745 00746 $curlHandle = curl_init( $this->url ); 00747 00748 if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) { 00749 throw new MWException( "Error setting curl options." ); 00750 } 00751 00752 if ( $this->followRedirects && $this->canFollowRedirects() ) { 00753 wfSuppressWarnings(); 00754 if ( ! curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) { 00755 wfDebug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " . 00756 "Probably safe_mode or open_basedir is set.\n" ); 00757 // Continue the processing. If it were in curl_setopt_array, 00758 // processing would have halted on its entry 00759 } 00760 wfRestoreWarnings(); 00761 } 00762 00763 if ( false === curl_exec( $curlHandle ) ) { 00764 $code = curl_error( $curlHandle ); 00765 00766 if ( isset( self::$curlMessageMap[$code] ) ) { 00767 $this->status->fatal( self::$curlMessageMap[$code] ); 00768 } else { 00769 $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) ); 00770 } 00771 } else { 00772 $this->headerList = explode( "\r\n", $this->headerText ); 00773 } 00774 00775 curl_close( $curlHandle ); 00776 00777 $this->parseHeader(); 00778 $this->setStatus(); 00779 00780 return $this->status; 00781 } 00782 00786 public function canFollowRedirects() { 00787 if ( strval( ini_get( 'open_basedir' ) ) !== '' || wfIniGetBool( 'safe_mode' ) ) { 00788 wfDebug( "Cannot follow redirects in safe mode\n" ); 00789 return false; 00790 } 00791 00792 if ( !defined( 'CURLOPT_REDIR_PROTOCOLS' ) ) { 00793 wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" ); 00794 return false; 00795 } 00796 00797 return true; 00798 } 00799 } 00800 00801 class PhpHttpRequest extends MWHttpRequest { 00802 00807 protected function urlToTcp( $url ) { 00808 $parsedUrl = parse_url( $url ); 00809 00810 return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port']; 00811 } 00812 00813 public function execute() { 00814 parent::execute(); 00815 00816 if ( is_array( $this->postData ) ) { 00817 $this->postData = wfArrayToCgi( $this->postData ); 00818 } 00819 00820 if ( $this->parsedUrl['scheme'] != 'http' && 00821 $this->parsedUrl['scheme'] != 'https' ) { 00822 $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] ); 00823 } 00824 00825 $this->reqHeaders['Accept'] = "*/*"; 00826 if ( $this->method == 'POST' ) { 00827 // Required for HTTP 1.0 POSTs 00828 $this->reqHeaders['Content-Length'] = strlen( $this->postData ); 00829 if( !isset( $this->reqHeaders['Content-Type'] ) ) { 00830 $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded"; 00831 } 00832 } 00833 00834 $options = array(); 00835 if ( $this->proxy ) { 00836 $options['proxy'] = $this->urlToTCP( $this->proxy ); 00837 $options['request_fulluri'] = true; 00838 } 00839 00840 if ( !$this->followRedirects ) { 00841 $options['max_redirects'] = 0; 00842 } else { 00843 $options['max_redirects'] = $this->maxRedirects; 00844 } 00845 00846 $options['method'] = $this->method; 00847 $options['header'] = implode( "\r\n", $this->getHeaderList() ); 00848 // Note that at some future point we may want to support 00849 // HTTP/1.1, but we'd have to write support for chunking 00850 // in version of PHP < 5.3.1 00851 $options['protocol_version'] = "1.0"; 00852 00853 // This is how we tell PHP we want to deal with 404s (for example) ourselves. 00854 // Only works on 5.2.10+ 00855 $options['ignore_errors'] = true; 00856 00857 if ( $this->postData ) { 00858 $options['content'] = $this->postData; 00859 } 00860 00861 $options['timeout'] = $this->timeout; 00862 00863 $context = stream_context_create( array( 'http' => $options ) ); 00864 00865 $this->headerList = array(); 00866 $reqCount = 0; 00867 $url = $this->url; 00868 00869 $result = array(); 00870 00871 do { 00872 $reqCount++; 00873 wfSuppressWarnings(); 00874 $fh = fopen( $url, "r", false, $context ); 00875 wfRestoreWarnings(); 00876 00877 if ( !$fh ) { 00878 break; 00879 } 00880 00881 $result = stream_get_meta_data( $fh ); 00882 $this->headerList = $result['wrapper_data']; 00883 $this->parseHeader(); 00884 00885 if ( !$this->followRedirects ) { 00886 break; 00887 } 00888 00889 # Handle manual redirection 00890 if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) { 00891 break; 00892 } 00893 # Check security of URL 00894 $url = $this->getResponseHeader( "Location" ); 00895 00896 if ( !Http::isValidURI( $url ) ) { 00897 wfDebug( __METHOD__ . ": insecure redirection\n" ); 00898 break; 00899 } 00900 } while ( true ); 00901 00902 $this->setStatus(); 00903 00904 if ( $fh === false ) { 00905 $this->status->fatal( 'http-request-error' ); 00906 return $this->status; 00907 } 00908 00909 if ( $result['timed_out'] ) { 00910 $this->status->fatal( 'http-timed-out', $this->url ); 00911 return $this->status; 00912 } 00913 00914 // If everything went OK, or we received some error code 00915 // get the response body content. 00916 if ( $this->status->isOK() 00917 || (int)$this->respStatus >= 300) { 00918 while ( !feof( $fh ) ) { 00919 $buf = fread( $fh, 8192 ); 00920 00921 if ( $buf === false ) { 00922 $this->status->fatal( 'http-read-error' ); 00923 break; 00924 } 00925 00926 if ( strlen( $buf ) ) { 00927 call_user_func( $this->callback, $fh, $buf ); 00928 } 00929 } 00930 } 00931 fclose( $fh ); 00932 00933 return $this->status; 00934 } 00935 }