MediaWiki
REL1_19
|
00001 <?php 00010 class Http { 00011 static $httpEngine = false; 00012 00036 public static function request( $method, $url, $options = array() ) { 00037 wfDebug( "HTTP: $method: $url\n" ); 00038 $options['method'] = strtoupper( $method ); 00039 00040 if ( !isset( $options['timeout'] ) ) { 00041 $options['timeout'] = 'default'; 00042 } 00043 00044 $req = MWHttpRequest::factory( $url, $options ); 00045 if( isset( $options['userAgent'] ) ) { 00046 $req->setUserAgent( $options['userAgent'] ); 00047 } 00048 $status = $req->execute(); 00049 00050 if ( $status->isOK() ) { 00051 return $req->getContent(); 00052 } else { 00053 return false; 00054 } 00055 } 00056 00066 public static function get( $url, $timeout = 'default', $options = array() ) { 00067 $options['timeout'] = $timeout; 00068 return Http::request( 'GET', $url, $options ); 00069 } 00070 00079 public static function post( $url, $options = array() ) { 00080 return Http::request( 'POST', $url, $options ); 00081 } 00082 00089 public static function isLocalURL( $url ) { 00090 global $wgCommandLineMode, $wgConf; 00091 00092 if ( $wgCommandLineMode ) { 00093 return false; 00094 } 00095 00096 // Extract host part 00097 $matches = array(); 00098 if ( preg_match( '!^http://([\w.-]+)[/:].*$!', $url, $matches ) ) { 00099 $host = $matches[1]; 00100 // Split up dotwise 00101 $domainParts = explode( '.', $host ); 00102 // Check if this domain or any superdomain is listed in $wgConf as a local virtual host 00103 $domainParts = array_reverse( $domainParts ); 00104 00105 $domain = ''; 00106 for ( $i = 0; $i < count( $domainParts ); $i++ ) { 00107 $domainPart = $domainParts[$i]; 00108 if ( $i == 0 ) { 00109 $domain = $domainPart; 00110 } else { 00111 $domain = $domainPart . '.' . $domain; 00112 } 00113 00114 if ( $wgConf->isLocalVHost( $domain ) ) { 00115 return true; 00116 } 00117 } 00118 } 00119 00120 return false; 00121 } 00122 00127 public static function userAgent() { 00128 global $wgVersion; 00129 return "MediaWiki/$wgVersion"; 00130 } 00131 00144 public static function isValidURI( $uri ) { 00145 return preg_match( 00146 '/^https?:\/\/[^\/\s]\S*$/D', 00147 $uri 00148 ); 00149 } 00150 } 00151 00159 class MWHttpRequest { 00160 const SUPPORTS_FILE_POSTS = false; 00161 00162 protected $content; 00163 protected $timeout = 'default'; 00164 protected $headersOnly = null; 00165 protected $postData = null; 00166 protected $proxy = null; 00167 protected $noProxy = false; 00168 protected $sslVerifyHost = true; 00169 protected $sslVerifyCert = true; 00170 protected $caInfo = null; 00171 protected $method = "GET"; 00172 protected $reqHeaders = array(); 00173 protected $url; 00174 protected $parsedUrl; 00175 protected $callback; 00176 protected $maxRedirects = 5; 00177 protected $followRedirects = false; 00178 00182 protected $cookieJar; 00183 00184 protected $headerList = array(); 00185 protected $respVersion = "0.9"; 00186 protected $respStatus = "200 Ok"; 00187 protected $respHeaders = array(); 00188 00189 public $status; 00190 00195 function __construct( $url, $options = array() ) { 00196 global $wgHTTPTimeout; 00197 00198 $this->url = wfExpandUrl( $url, PROTO_HTTP ); 00199 $this->parsedUrl = wfParseUrl( $this->url ); 00200 00201 if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) { 00202 $this->status = Status::newFatal( 'http-invalid-url' ); 00203 } else { 00204 $this->status = Status::newGood( 100 ); // continue 00205 } 00206 00207 if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) { 00208 $this->timeout = $options['timeout']; 00209 } else { 00210 $this->timeout = $wgHTTPTimeout; 00211 } 00212 00213 $members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo", 00214 "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ); 00215 00216 foreach ( $members as $o ) { 00217 if ( isset( $options[$o] ) ) { 00218 $this->$o = $options[$o]; 00219 } 00220 } 00221 } 00222 00228 public static function canMakeRequests() { 00229 return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' ); 00230 } 00231 00239 public static function factory( $url, $options = null ) { 00240 if ( !Http::$httpEngine ) { 00241 Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php'; 00242 } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { 00243 throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . 00244 ' Http::$httpEngine is set to "curl"' ); 00245 } 00246 00247 switch( Http::$httpEngine ) { 00248 case 'curl': 00249 return new CurlHttpRequest( $url, $options ); 00250 case 'php': 00251 if ( !wfIniGetBool( 'allow_url_fopen' ) ) { 00252 throw new MWException( __METHOD__ . ': allow_url_fopen needs to be enabled for pure PHP' . 00253 ' http requests to work. If possible, curl should be used instead. See http://php.net/curl.' ); 00254 } 00255 return new PhpHttpRequest( $url, $options ); 00256 default: 00257 throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' ); 00258 } 00259 } 00260 00266 public function getContent() { 00267 return $this->content; 00268 } 00269 00276 public function setData( $args ) { 00277 $this->postData = $args; 00278 } 00279 00286 public function proxySetup() { 00287 global $wgHTTPProxy; 00288 00289 if ( $this->proxy ) { 00290 return; 00291 } 00292 00293 if ( Http::isLocalURL( $this->url ) ) { 00294 $this->proxy = ''; 00295 } elseif ( $wgHTTPProxy ) { 00296 $this->proxy = $wgHTTPProxy ; 00297 } elseif ( getenv( "http_proxy" ) ) { 00298 $this->proxy = getenv( "http_proxy" ); 00299 } 00300 } 00301 00305 public function setReferer( $url ) { 00306 $this->setHeader( 'Referer', $url ); 00307 } 00308 00313 public function setUserAgent( $UA ) { 00314 $this->setHeader( 'User-Agent', $UA ); 00315 } 00316 00322 public function setHeader( $name, $value ) { 00323 // I feel like I should normalize the case here... 00324 $this->reqHeaders[$name] = $value; 00325 } 00326 00331 public function getHeaderList() { 00332 $list = array(); 00333 00334 if ( $this->cookieJar ) { 00335 $this->reqHeaders['Cookie'] = 00336 $this->cookieJar->serializeToHttpRequest( 00337 $this->parsedUrl['path'], 00338 $this->parsedUrl['host'] 00339 ); 00340 } 00341 00342 foreach ( $this->reqHeaders as $name => $value ) { 00343 $list[] = "$name: $value"; 00344 } 00345 00346 return $list; 00347 } 00348 00366 public function setCallback( $callback ) { 00367 if ( !is_callable( $callback ) ) { 00368 throw new MWException( 'Invalid MwHttpRequest callback' ); 00369 } 00370 $this->callback = $callback; 00371 } 00372 00380 public function read( $fh, $content ) { 00381 $this->content .= $content; 00382 return strlen( $content ); 00383 } 00384 00390 public function execute() { 00391 global $wgTitle; 00392 00393 $this->content = ""; 00394 00395 if ( strtoupper( $this->method ) == "HEAD" ) { 00396 $this->headersOnly = true; 00397 } 00398 00399 if ( is_object( $wgTitle ) && !isset( $this->reqHeaders['Referer'] ) ) { 00400 $this->setReferer( wfExpandUrl( $wgTitle->getFullURL(), PROTO_CURRENT ) ); 00401 } 00402 00403 if ( !$this->noProxy ) { 00404 $this->proxySetup(); 00405 } 00406 00407 if ( !$this->callback ) { 00408 $this->setCallback( array( $this, 'read' ) ); 00409 } 00410 00411 if ( !isset( $this->reqHeaders['User-Agent'] ) ) { 00412 $this->setUserAgent( Http::userAgent() ); 00413 } 00414 } 00415 00423 protected function parseHeader() { 00424 $lastname = ""; 00425 00426 foreach ( $this->headerList as $header ) { 00427 if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) { 00428 $this->respVersion = $match[1]; 00429 $this->respStatus = $match[2]; 00430 } elseif ( preg_match( "#^[ \t]#", $header ) ) { 00431 $last = count( $this->respHeaders[$lastname] ) - 1; 00432 $this->respHeaders[$lastname][$last] .= "\r\n$header"; 00433 } elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) { 00434 $this->respHeaders[strtolower( $match[1] )][] = $match[2]; 00435 $lastname = strtolower( $match[1] ); 00436 } 00437 } 00438 00439 $this->parseCookies(); 00440 } 00441 00452 protected function setStatus() { 00453 if ( !$this->respHeaders ) { 00454 $this->parseHeader(); 00455 } 00456 00457 if ( (int)$this->respStatus > 399 ) { 00458 list( $code, $message ) = explode( " ", $this->respStatus, 2 ); 00459 $this->status->fatal( "http-bad-status", $code, $message ); 00460 } 00461 } 00462 00470 public function getStatus() { 00471 if ( !$this->respHeaders ) { 00472 $this->parseHeader(); 00473 } 00474 00475 return (int)$this->respStatus; 00476 } 00477 00478 00484 public function isRedirect() { 00485 if ( !$this->respHeaders ) { 00486 $this->parseHeader(); 00487 } 00488 00489 $status = (int)$this->respStatus; 00490 00491 if ( $status >= 300 && $status <= 303 ) { 00492 return true; 00493 } 00494 00495 return false; 00496 } 00497 00506 public function getResponseHeaders() { 00507 if ( !$this->respHeaders ) { 00508 $this->parseHeader(); 00509 } 00510 00511 return $this->respHeaders; 00512 } 00513 00520 public function getResponseHeader( $header ) { 00521 if ( !$this->respHeaders ) { 00522 $this->parseHeader(); 00523 } 00524 00525 if ( isset( $this->respHeaders[strtolower ( $header ) ] ) ) { 00526 $v = $this->respHeaders[strtolower ( $header ) ]; 00527 return $v[count( $v ) - 1]; 00528 } 00529 00530 return null; 00531 } 00532 00538 public function setCookieJar( $jar ) { 00539 $this->cookieJar = $jar; 00540 } 00541 00547 public function getCookieJar() { 00548 if ( !$this->respHeaders ) { 00549 $this->parseHeader(); 00550 } 00551 00552 return $this->cookieJar; 00553 } 00554 00564 public function setCookie( $name, $value = null, $attr = null ) { 00565 if ( !$this->cookieJar ) { 00566 $this->cookieJar = new CookieJar; 00567 } 00568 00569 $this->cookieJar->setCookie( $name, $value, $attr ); 00570 } 00571 00575 protected function parseCookies() { 00576 if ( !$this->cookieJar ) { 00577 $this->cookieJar = new CookieJar; 00578 } 00579 00580 if ( isset( $this->respHeaders['set-cookie'] ) ) { 00581 $url = parse_url( $this->getFinalUrl() ); 00582 foreach ( $this->respHeaders['set-cookie'] as $cookie ) { 00583 $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] ); 00584 } 00585 } 00586 } 00587 00600 public function getFinalUrl() { 00601 $headers = $this->getResponseHeaders(); 00602 00603 //return full url (fix for incorrect but handled relative location) 00604 if ( isset( $headers[ 'location' ] ) ) { 00605 $locations = $headers[ 'location' ]; 00606 $domain = ''; 00607 $foundRelativeURI = false; 00608 $countLocations = count($locations); 00609 00610 for ( $i = $countLocations - 1; $i >= 0; $i-- ) { 00611 $url = parse_url( $locations[ $i ] ); 00612 00613 if ( isset($url[ 'host' ]) ) { 00614 $domain = $url[ 'scheme' ] . '://' . $url[ 'host' ]; 00615 break; //found correct URI (with host) 00616 } else { 00617 $foundRelativeURI = true; 00618 } 00619 } 00620 00621 if ( $foundRelativeURI ) { 00622 if ( $domain ) { 00623 return $domain . $locations[ $countLocations - 1 ]; 00624 } else { 00625 $url = parse_url( $this->url ); 00626 if ( isset($url[ 'host' ]) ) { 00627 return $url[ 'scheme' ] . '://' . $url[ 'host' ] . $locations[ $countLocations - 1 ]; 00628 } 00629 } 00630 } else { 00631 return $locations[ $countLocations - 1 ]; 00632 } 00633 } 00634 00635 return $this->url; 00636 } 00637 00643 public function canFollowRedirects() { 00644 return true; 00645 } 00646 } 00647 00651 class CurlHttpRequest extends MWHttpRequest { 00652 const SUPPORTS_FILE_POSTS = true; 00653 00654 static $curlMessageMap = array( 00655 6 => 'http-host-unreachable', 00656 28 => 'http-timed-out' 00657 ); 00658 00659 protected $curlOptions = array(); 00660 protected $headerText = ""; 00661 00667 protected function readHeader( $fh, $content ) { 00668 $this->headerText .= $content; 00669 return strlen( $content ); 00670 } 00671 00672 public function execute() { 00673 parent::execute(); 00674 00675 if ( !$this->status->isOK() ) { 00676 return $this->status; 00677 } 00678 00679 $this->curlOptions[CURLOPT_PROXY] = $this->proxy; 00680 $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout; 00681 $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; 00682 $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback; 00683 $this->curlOptions[CURLOPT_HEADERFUNCTION] = array( $this, "readHeader" ); 00684 $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects; 00685 $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression 00686 00687 /* not sure these two are actually necessary */ 00688 if ( isset( $this->reqHeaders['Referer'] ) ) { 00689 $this->curlOptions[CURLOPT_REFERER] = $this->reqHeaders['Referer']; 00690 } 00691 $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent']; 00692 00693 $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost ? 2 : 0; 00694 $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert; 00695 00696 if ( $this->caInfo ) { 00697 $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo; 00698 } 00699 00700 if ( $this->headersOnly ) { 00701 $this->curlOptions[CURLOPT_NOBODY] = true; 00702 $this->curlOptions[CURLOPT_HEADER] = true; 00703 } elseif ( $this->method == 'POST' ) { 00704 $this->curlOptions[CURLOPT_POST] = true; 00705 $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData; 00706 // Suppress 'Expect: 100-continue' header, as some servers 00707 // will reject it with a 417 and Curl won't auto retry 00708 // with HTTP 1.0 fallback 00709 $this->reqHeaders['Expect'] = ''; 00710 } else { 00711 $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method; 00712 } 00713 00714 $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList(); 00715 00716 $curlHandle = curl_init( $this->url ); 00717 00718 if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) { 00719 throw new MWException( "Error setting curl options." ); 00720 } 00721 00722 if ( $this->followRedirects && $this->canFollowRedirects() ) { 00723 wfSuppressWarnings(); 00724 if ( ! curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) { 00725 wfDebug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " . 00726 "Probably safe_mode or open_basedir is set.\n" ); 00727 // Continue the processing. If it were in curl_setopt_array, 00728 // processing would have halted on its entry 00729 } 00730 wfRestoreWarnings(); 00731 } 00732 00733 if ( false === curl_exec( $curlHandle ) ) { 00734 $code = curl_error( $curlHandle ); 00735 00736 if ( isset( self::$curlMessageMap[$code] ) ) { 00737 $this->status->fatal( self::$curlMessageMap[$code] ); 00738 } else { 00739 $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) ); 00740 } 00741 } else { 00742 $this->headerList = explode( "\r\n", $this->headerText ); 00743 } 00744 00745 curl_close( $curlHandle ); 00746 00747 $this->parseHeader(); 00748 $this->setStatus(); 00749 00750 return $this->status; 00751 } 00752 00756 public function canFollowRedirects() { 00757 if ( strval( ini_get( 'open_basedir' ) ) !== '' || wfIniGetBool( 'safe_mode' ) ) { 00758 wfDebug( "Cannot follow redirects in safe mode\n" ); 00759 return false; 00760 } 00761 00762 if ( !defined( 'CURLOPT_REDIR_PROTOCOLS' ) ) { 00763 wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" ); 00764 return false; 00765 } 00766 00767 return true; 00768 } 00769 } 00770 00771 class PhpHttpRequest extends MWHttpRequest { 00772 00777 protected function urlToTcp( $url ) { 00778 $parsedUrl = parse_url( $url ); 00779 00780 return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port']; 00781 } 00782 00783 public function execute() { 00784 parent::execute(); 00785 00786 if ( is_array( $this->postData ) ) { 00787 $this->postData = wfArrayToCGI( $this->postData ); 00788 } 00789 00790 if ( $this->parsedUrl['scheme'] != 'http' && 00791 $this->parsedUrl['scheme'] != 'https' ) { 00792 $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] ); 00793 } 00794 00795 $this->reqHeaders['Accept'] = "*/*"; 00796 if ( $this->method == 'POST' ) { 00797 // Required for HTTP 1.0 POSTs 00798 $this->reqHeaders['Content-Length'] = strlen( $this->postData ); 00799 $this->reqHeaders['Content-type'] = "application/x-www-form-urlencoded"; 00800 } 00801 00802 $options = array(); 00803 if ( $this->proxy && !$this->noProxy ) { 00804 $options['proxy'] = $this->urlToTCP( $this->proxy ); 00805 $options['request_fulluri'] = true; 00806 } 00807 00808 if ( !$this->followRedirects ) { 00809 $options['max_redirects'] = 0; 00810 } else { 00811 $options['max_redirects'] = $this->maxRedirects; 00812 } 00813 00814 $options['method'] = $this->method; 00815 $options['header'] = implode( "\r\n", $this->getHeaderList() ); 00816 // Note that at some future point we may want to support 00817 // HTTP/1.1, but we'd have to write support for chunking 00818 // in version of PHP < 5.3.1 00819 $options['protocol_version'] = "1.0"; 00820 00821 // This is how we tell PHP we want to deal with 404s (for example) ourselves. 00822 // Only works on 5.2.10+ 00823 $options['ignore_errors'] = true; 00824 00825 if ( $this->postData ) { 00826 $options['content'] = $this->postData; 00827 } 00828 00829 $options['timeout'] = $this->timeout; 00830 00831 $context = stream_context_create( array( 'http' => $options ) ); 00832 00833 $this->headerList = array(); 00834 $reqCount = 0; 00835 $url = $this->url; 00836 00837 $result = array(); 00838 00839 do { 00840 $reqCount++; 00841 wfSuppressWarnings(); 00842 $fh = fopen( $url, "r", false, $context ); 00843 wfRestoreWarnings(); 00844 00845 if ( !$fh ) { 00846 break; 00847 } 00848 00849 $result = stream_get_meta_data( $fh ); 00850 $this->headerList = $result['wrapper_data']; 00851 $this->parseHeader(); 00852 00853 if ( !$this->followRedirects ) { 00854 break; 00855 } 00856 00857 # Handle manual redirection 00858 if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) { 00859 break; 00860 } 00861 # Check security of URL 00862 $url = $this->getResponseHeader( "Location" ); 00863 00864 if ( !Http::isValidURI( $url ) ) { 00865 wfDebug( __METHOD__ . ": insecure redirection\n" ); 00866 break; 00867 } 00868 } while ( true ); 00869 00870 $this->setStatus(); 00871 00872 if ( $fh === false ) { 00873 $this->status->fatal( 'http-request-error' ); 00874 return $this->status; 00875 } 00876 00877 if ( $result['timed_out'] ) { 00878 $this->status->fatal( 'http-timed-out', $this->url ); 00879 return $this->status; 00880 } 00881 00882 // If everything went OK, or we recieved some error code 00883 // get the response body content. 00884 if ( $this->status->isOK() 00885 || (int)$this->respStatus >= 300) { 00886 while ( !feof( $fh ) ) { 00887 $buf = fread( $fh, 8192 ); 00888 00889 if ( $buf === false ) { 00890 $this->status->fatal( 'http-read-error' ); 00891 break; 00892 } 00893 00894 if ( strlen( $buf ) ) { 00895 call_user_func( $this->callback, $fh, $buf ); 00896 } 00897 } 00898 } 00899 fclose( $fh ); 00900 00901 return $this->status; 00902 } 00903 }