MediaWiki
REL1_20
|
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 00271 public static function factory( $url, $options = null ) { 00272 if ( !Http::$httpEngine ) { 00273 Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php'; 00274 } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { 00275 throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . 00276 ' Http::$httpEngine is set to "curl"' ); 00277 } 00278 00279 switch( Http::$httpEngine ) { 00280 case 'curl': 00281 return new CurlHttpRequest( $url, $options ); 00282 case 'php': 00283 if ( !wfIniGetBool( 'allow_url_fopen' ) ) { 00284 throw new MWException( __METHOD__ . ': allow_url_fopen needs to be enabled for pure PHP' . 00285 ' http requests to work. If possible, curl should be used instead. See http://php.net/curl.' ); 00286 } 00287 return new PhpHttpRequest( $url, $options ); 00288 default: 00289 throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' ); 00290 } 00291 } 00292 00298 public function getContent() { 00299 return $this->content; 00300 } 00301 00308 public function setData( $args ) { 00309 $this->postData = $args; 00310 } 00311 00317 public function proxySetup() { 00318 global $wgHTTPProxy; 00319 00320 if ( $this->proxy || !$this->noProxy ) { 00321 return; 00322 } 00323 00324 if ( Http::isLocalURL( $this->url ) || $this->noProxy ) { 00325 $this->proxy = ''; 00326 } elseif ( $wgHTTPProxy ) { 00327 $this->proxy = $wgHTTPProxy ; 00328 } elseif ( getenv( "http_proxy" ) ) { 00329 $this->proxy = getenv( "http_proxy" ); 00330 } 00331 } 00332 00336 public function setReferer( $url ) { 00337 $this->setHeader( 'Referer', $url ); 00338 } 00339 00344 public function setUserAgent( $UA ) { 00345 $this->setHeader( 'User-Agent', $UA ); 00346 } 00347 00353 public function setHeader( $name, $value ) { 00354 // I feel like I should normalize the case here... 00355 $this->reqHeaders[$name] = $value; 00356 } 00357 00362 public function getHeaderList() { 00363 $list = array(); 00364 00365 if ( $this->cookieJar ) { 00366 $this->reqHeaders['Cookie'] = 00367 $this->cookieJar->serializeToHttpRequest( 00368 $this->parsedUrl['path'], 00369 $this->parsedUrl['host'] 00370 ); 00371 } 00372 00373 foreach ( $this->reqHeaders as $name => $value ) { 00374 $list[] = "$name: $value"; 00375 } 00376 00377 return $list; 00378 } 00379 00397 public function setCallback( $callback ) { 00398 if ( !is_callable( $callback ) ) { 00399 throw new MWException( 'Invalid MwHttpRequest callback' ); 00400 } 00401 $this->callback = $callback; 00402 } 00403 00412 public function read( $fh, $content ) { 00413 $this->content .= $content; 00414 return strlen( $content ); 00415 } 00416 00422 public function execute() { 00423 global $wgTitle; 00424 00425 $this->content = ""; 00426 00427 if ( strtoupper( $this->method ) == "HEAD" ) { 00428 $this->headersOnly = true; 00429 } 00430 00431 if ( is_object( $wgTitle ) && !isset( $this->reqHeaders['Referer'] ) ) { 00432 $this->setReferer( wfExpandUrl( $wgTitle->getFullURL(), PROTO_CURRENT ) ); 00433 } 00434 00435 $this->proxySetup(); // set up any proxy as needed 00436 00437 if ( !$this->callback ) { 00438 $this->setCallback( array( $this, 'read' ) ); 00439 } 00440 00441 if ( !isset( $this->reqHeaders['User-Agent'] ) ) { 00442 $this->setUserAgent( Http::userAgent() ); 00443 } 00444 } 00445 00451 protected function parseHeader() { 00452 $lastname = ""; 00453 00454 foreach ( $this->headerList as $header ) { 00455 if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) { 00456 $this->respVersion = $match[1]; 00457 $this->respStatus = $match[2]; 00458 } elseif ( preg_match( "#^[ \t]#", $header ) ) { 00459 $last = count( $this->respHeaders[$lastname] ) - 1; 00460 $this->respHeaders[$lastname][$last] .= "\r\n$header"; 00461 } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) { 00462 $this->respHeaders[strtolower( $match[1] )][] = $match[2]; 00463 $lastname = strtolower( $match[1] ); 00464 } 00465 } 00466 00467 $this->parseCookies(); 00468 } 00469 00478 protected function setStatus() { 00479 if ( !$this->respHeaders ) { 00480 $this->parseHeader(); 00481 } 00482 00483 if ( (int)$this->respStatus > 399 ) { 00484 list( $code, $message ) = explode( " ", $this->respStatus, 2 ); 00485 $this->status->fatal( "http-bad-status", $code, $message ); 00486 } 00487 } 00488 00496 public function getStatus() { 00497 if ( !$this->respHeaders ) { 00498 $this->parseHeader(); 00499 } 00500 00501 return (int)$this->respStatus; 00502 } 00503 00504 00510 public function isRedirect() { 00511 if ( !$this->respHeaders ) { 00512 $this->parseHeader(); 00513 } 00514 00515 $status = (int)$this->respStatus; 00516 00517 if ( $status >= 300 && $status <= 303 ) { 00518 return true; 00519 } 00520 00521 return false; 00522 } 00523 00532 public function getResponseHeaders() { 00533 if ( !$this->respHeaders ) { 00534 $this->parseHeader(); 00535 } 00536 00537 return $this->respHeaders; 00538 } 00539 00546 public function getResponseHeader( $header ) { 00547 if ( !$this->respHeaders ) { 00548 $this->parseHeader(); 00549 } 00550 00551 if ( isset( $this->respHeaders[strtolower ( $header ) ] ) ) { 00552 $v = $this->respHeaders[strtolower ( $header ) ]; 00553 return $v[count( $v ) - 1]; 00554 } 00555 00556 return null; 00557 } 00558 00564 public function setCookieJar( $jar ) { 00565 $this->cookieJar = $jar; 00566 } 00567 00573 public function getCookieJar() { 00574 if ( !$this->respHeaders ) { 00575 $this->parseHeader(); 00576 } 00577 00578 return $this->cookieJar; 00579 } 00580 00590 public function setCookie( $name, $value = null, $attr = null ) { 00591 if ( !$this->cookieJar ) { 00592 $this->cookieJar = new CookieJar; 00593 } 00594 00595 $this->cookieJar->setCookie( $name, $value, $attr ); 00596 } 00597 00601 protected function parseCookies() { 00602 if ( !$this->cookieJar ) { 00603 $this->cookieJar = new CookieJar; 00604 } 00605 00606 if ( isset( $this->respHeaders['set-cookie'] ) ) { 00607 $url = parse_url( $this->getFinalUrl() ); 00608 foreach ( $this->respHeaders['set-cookie'] as $cookie ) { 00609 $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] ); 00610 } 00611 } 00612 } 00613 00626 public function getFinalUrl() { 00627 $headers = $this->getResponseHeaders(); 00628 00629 //return full url (fix for incorrect but handled relative location) 00630 if ( isset( $headers[ 'location' ] ) ) { 00631 $locations = $headers[ 'location' ]; 00632 $domain = ''; 00633 $foundRelativeURI = false; 00634 $countLocations = count($locations); 00635 00636 for ( $i = $countLocations - 1; $i >= 0; $i-- ) { 00637 $url = parse_url( $locations[ $i ] ); 00638 00639 if ( isset($url[ 'host' ]) ) { 00640 $domain = $url[ 'scheme' ] . '://' . $url[ 'host' ]; 00641 break; //found correct URI (with host) 00642 } else { 00643 $foundRelativeURI = true; 00644 } 00645 } 00646 00647 if ( $foundRelativeURI ) { 00648 if ( $domain ) { 00649 return $domain . $locations[ $countLocations - 1 ]; 00650 } else { 00651 $url = parse_url( $this->url ); 00652 if ( isset($url[ 'host' ]) ) { 00653 return $url[ 'scheme' ] . '://' . $url[ 'host' ] . $locations[ $countLocations - 1 ]; 00654 } 00655 } 00656 } else { 00657 return $locations[ $countLocations - 1 ]; 00658 } 00659 } 00660 00661 return $this->url; 00662 } 00663 00669 public function canFollowRedirects() { 00670 return true; 00671 } 00672 } 00673 00677 class CurlHttpRequest extends MWHttpRequest { 00678 const SUPPORTS_FILE_POSTS = true; 00679 00680 static $curlMessageMap = array( 00681 6 => 'http-host-unreachable', 00682 28 => 'http-timed-out' 00683 ); 00684 00685 protected $curlOptions = array(); 00686 protected $headerText = ""; 00687 00693 protected function readHeader( $fh, $content ) { 00694 $this->headerText .= $content; 00695 return strlen( $content ); 00696 } 00697 00698 public function execute() { 00699 parent::execute(); 00700 00701 if ( !$this->status->isOK() ) { 00702 return $this->status; 00703 } 00704 00705 $this->curlOptions[CURLOPT_PROXY] = $this->proxy; 00706 $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout; 00707 $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; 00708 $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback; 00709 $this->curlOptions[CURLOPT_HEADERFUNCTION] = array( $this, "readHeader" ); 00710 $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects; 00711 $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression 00712 00713 /* not sure these two are actually necessary */ 00714 if ( isset( $this->reqHeaders['Referer'] ) ) { 00715 $this->curlOptions[CURLOPT_REFERER] = $this->reqHeaders['Referer']; 00716 } 00717 $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent']; 00718 00719 $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0; 00720 $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert; 00721 00722 if ( $this->caInfo ) { 00723 $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo; 00724 } 00725 00726 if ( $this->headersOnly ) { 00727 $this->curlOptions[CURLOPT_NOBODY] = true; 00728 $this->curlOptions[CURLOPT_HEADER] = true; 00729 } elseif ( $this->method == 'POST' ) { 00730 $this->curlOptions[CURLOPT_POST] = true; 00731 $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData; 00732 // Suppress 'Expect: 100-continue' header, as some servers 00733 // will reject it with a 417 and Curl won't auto retry 00734 // with HTTP 1.0 fallback 00735 $this->reqHeaders['Expect'] = ''; 00736 } else { 00737 $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method; 00738 } 00739 00740 $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList(); 00741 00742 $curlHandle = curl_init( $this->url ); 00743 00744 if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) { 00745 throw new MWException( "Error setting curl options." ); 00746 } 00747 00748 if ( $this->followRedirects && $this->canFollowRedirects() ) { 00749 wfSuppressWarnings(); 00750 if ( ! curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) { 00751 wfDebug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " . 00752 "Probably safe_mode or open_basedir is set.\n" ); 00753 // Continue the processing. If it were in curl_setopt_array, 00754 // processing would have halted on its entry 00755 } 00756 wfRestoreWarnings(); 00757 } 00758 00759 if ( false === curl_exec( $curlHandle ) ) { 00760 $code = curl_error( $curlHandle ); 00761 00762 if ( isset( self::$curlMessageMap[$code] ) ) { 00763 $this->status->fatal( self::$curlMessageMap[$code] ); 00764 } else { 00765 $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) ); 00766 } 00767 } else { 00768 $this->headerList = explode( "\r\n", $this->headerText ); 00769 } 00770 00771 curl_close( $curlHandle ); 00772 00773 $this->parseHeader(); 00774 $this->setStatus(); 00775 00776 return $this->status; 00777 } 00778 00782 public function canFollowRedirects() { 00783 if ( strval( ini_get( 'open_basedir' ) ) !== '' || wfIniGetBool( 'safe_mode' ) ) { 00784 wfDebug( "Cannot follow redirects in safe mode\n" ); 00785 return false; 00786 } 00787 00788 if ( !defined( 'CURLOPT_REDIR_PROTOCOLS' ) ) { 00789 wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" ); 00790 return false; 00791 } 00792 00793 return true; 00794 } 00795 } 00796 00797 class PhpHttpRequest extends MWHttpRequest { 00798 00803 protected function urlToTcp( $url ) { 00804 $parsedUrl = parse_url( $url ); 00805 00806 return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port']; 00807 } 00808 00809 public function execute() { 00810 parent::execute(); 00811 00812 if ( is_array( $this->postData ) ) { 00813 $this->postData = wfArrayToCGI( $this->postData ); 00814 } 00815 00816 if ( $this->parsedUrl['scheme'] != 'http' && 00817 $this->parsedUrl['scheme'] != 'https' ) { 00818 $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] ); 00819 } 00820 00821 $this->reqHeaders['Accept'] = "*/*"; 00822 if ( $this->method == 'POST' ) { 00823 // Required for HTTP 1.0 POSTs 00824 $this->reqHeaders['Content-Length'] = strlen( $this->postData ); 00825 if( !isset( $this->reqHeaders['Content-Type'] ) ) { 00826 $this->reqHeaders['Content-Type'] = "application/x-www-form-urlencoded"; 00827 } 00828 } 00829 00830 $options = array(); 00831 if ( $this->proxy ) { 00832 $options['proxy'] = $this->urlToTCP( $this->proxy ); 00833 $options['request_fulluri'] = true; 00834 } 00835 00836 if ( !$this->followRedirects ) { 00837 $options['max_redirects'] = 0; 00838 } else { 00839 $options['max_redirects'] = $this->maxRedirects; 00840 } 00841 00842 $options['method'] = $this->method; 00843 $options['header'] = implode( "\r\n", $this->getHeaderList() ); 00844 // Note that at some future point we may want to support 00845 // HTTP/1.1, but we'd have to write support for chunking 00846 // in version of PHP < 5.3.1 00847 $options['protocol_version'] = "1.0"; 00848 00849 // This is how we tell PHP we want to deal with 404s (for example) ourselves. 00850 // Only works on 5.2.10+ 00851 $options['ignore_errors'] = true; 00852 00853 if ( $this->postData ) { 00854 $options['content'] = $this->postData; 00855 } 00856 00857 $options['timeout'] = $this->timeout; 00858 00859 $context = stream_context_create( array( 'http' => $options ) ); 00860 00861 $this->headerList = array(); 00862 $reqCount = 0; 00863 $url = $this->url; 00864 00865 $result = array(); 00866 00867 do { 00868 $reqCount++; 00869 wfSuppressWarnings(); 00870 $fh = fopen( $url, "r", false, $context ); 00871 wfRestoreWarnings(); 00872 00873 if ( !$fh ) { 00874 break; 00875 } 00876 00877 $result = stream_get_meta_data( $fh ); 00878 $this->headerList = $result['wrapper_data']; 00879 $this->parseHeader(); 00880 00881 if ( !$this->followRedirects ) { 00882 break; 00883 } 00884 00885 # Handle manual redirection 00886 if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) { 00887 break; 00888 } 00889 # Check security of URL 00890 $url = $this->getResponseHeader( "Location" ); 00891 00892 if ( !Http::isValidURI( $url ) ) { 00893 wfDebug( __METHOD__ . ": insecure redirection\n" ); 00894 break; 00895 } 00896 } while ( true ); 00897 00898 $this->setStatus(); 00899 00900 if ( $fh === false ) { 00901 $this->status->fatal( 'http-request-error' ); 00902 return $this->status; 00903 } 00904 00905 if ( $result['timed_out'] ) { 00906 $this->status->fatal( 'http-timed-out', $this->url ); 00907 return $this->status; 00908 } 00909 00910 // If everything went OK, or we received some error code 00911 // get the response body content. 00912 if ( $this->status->isOK() 00913 || (int)$this->respStatus >= 300) { 00914 while ( !feof( $fh ) ) { 00915 $buf = fread( $fh, 8192 ); 00916 00917 if ( $buf === false ) { 00918 $this->status->fatal( 'http-read-error' ); 00919 break; 00920 } 00921 00922 if ( strlen( $buf ) ) { 00923 call_user_func( $this->callback, $fh, $buf ); 00924 } 00925 } 00926 } 00927 fclose( $fh ); 00928 00929 return $this->status; 00930 } 00931 }