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