MediaWiki  REL1_21
HttpFunctions.php
Go to the documentation of this file.
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 }