MediaWiki  REL1_20
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 
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 }