* (bug 24563) Entries on Special:WhatLinksHere now have a link to their history
[lhc/web/wiklou.git] / includes / HttpFunctions.php
index 5bf7050..9464895 100644 (file)
@@ -12,23 +12,28 @@ class Http {
 
        /**
         * Perform an HTTP request
-        * @param $method string HTTP method. Usually GET/POST
-        * @param $url string Full URL to act on
-        * @param $options options to pass to HttpRequest object
-        *                               Possible keys for the array:
-        *                                      timeout                   Timeout length in seconds
-        *                                      postData                  An array of key-value pairs or a url-encoded form data
-        *                                      proxy                     The proxy to use.      Will use $wgHTTPProxy (if set) otherwise.
-        *                                      noProxy                   Override $wgHTTPProxy (if set) and don't use any proxy at all.
-        *                                      sslVerifyHost     (curl only) Verify the SSL certificate
-        *                                      caInfo                    (curl only) Provide CA information
-        *                                      maxRedirects      Maximum number of redirects to follow (defaults to 5)
-        *                                      followRedirects   Whether to follow redirects (defaults to true)
-        * @returns mixed (bool)false on failure or a string on success
+        *
+        * @param $method String: HTTP method. Usually GET/POST
+        * @param $url String: full URL to act on
+        * @param $options Array: options to pass to HttpRequest object.
+        *      Possible keys for the array:
+        *    - timeout             Timeout length in seconds
+        *    - postData            An array of key-value pairs or a url-encoded form data
+        *    - proxy               The proxy to use.
+        *                          Will use $wgHTTPProxy (if set) otherwise.
+        *    - noProxy             Override $wgHTTPProxy (if set) and don't use any proxy at all.
+        *    - sslVerifyHost       (curl only) Verify hostname against certificate
+        *    - sslVerifyCert       (curl only) Verify SSL certificate
+        *    - caInfo              (curl only) Provide CA information
+        *    - maxRedirects        Maximum number of redirects to follow (defaults to 5)
+        *    - followRedirects     Whether to follow redirects (defaults to false). 
+        *                                  Note: this should only be used when the target URL is trusted,
+        *                                  to avoid attacks on intranet services accessible by HTTP.
+        * @return Mixed: (bool)false on failure or a string on success
         */
        public static function request( $method, $url, $options = array() ) {
                $url = wfExpandUrl( $url );
-               wfDebug( "HTTP: $method: $url" );
+               wfDebug( "HTTP: $method: $url\n" );
                $options['method'] = strtoupper( $method );
                if ( !isset( $options['timeout'] ) ) {
                        $options['timeout'] = 'default';
@@ -61,8 +66,9 @@ class Http {
 
        /**
         * Check if the URL can be served by localhost
-        * @param $url string Full url to check
-        * @return bool
+        *
+        * @param $url String: full url to check
+        * @return Boolean
         */
        public static function isLocalURL( $url ) {
                global $wgCommandLineMode, $wgConf;
@@ -95,7 +101,7 @@ class Http {
 
        /**
         * A standard user-agent we can use for external requests.
-        * @returns string
+        * @return String
         */
        public static function userAgent() {
                global $wgVersion;
@@ -104,8 +110,9 @@ class Http {
 
        /**
         * Checks that the given URI is a valid one
+        *
         * @param $uri Mixed: URI to check for validity
-        * @returns bool
+        * @returns Boolean
         */
        public static function isValidURI( $uri ) {
                return preg_match(
@@ -128,6 +135,7 @@ class HttpRequest {
        protected $proxy = null;
        protected $noProxy = false;
        protected $sslVerifyHost = true;
+       protected $sslVerifyCert = true;
        protected $caInfo = null;
        protected $method = "GET";
        protected $reqHeaders = array();
@@ -135,7 +143,7 @@ class HttpRequest {
        protected $parsedUrl;
        protected $callback;
        protected $maxRedirects = 5;
-       protected $followRedirects = true;
+       protected $followRedirects = false;
 
        protected $cookieJar;
 
@@ -147,8 +155,8 @@ class HttpRequest {
        public $status;
 
        /**
-        * @param $url   string url to use
-        * @param $options array (optional) extra params to pass (see Http::request())
+        * @param $url String: url to use
+        * @param $options Array: (optional) extra params to pass (see Http::request())
         */
        function __construct( $url, $options = array() ) {
                global $wgHTTPTimeout;
@@ -169,7 +177,7 @@ class HttpRequest {
                }
 
                $members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
-                                                 "method", "followRedirects", "maxRedirects" );
+                                 "method", "followRedirects", "maxRedirects", "sslVerifyCert" );
                foreach ( $members as $o ) {
                        if ( isset($options[$o]) ) {
                                $this->$o = $options[$o];
@@ -205,7 +213,8 @@ class HttpRequest {
 
        /**
         * Get the body, or content, of the response to the request
-        * @return string
+        *
+        * @return String
         */
        public function getContent() {
                return $this->content;
@@ -213,7 +222,8 @@ class HttpRequest {
 
        /**
         * Set the parameters of the request
-        * @param $params array
+        
+        * @param $args Array
         * @todo overload the args param
         */
        public function setData($args) {
@@ -223,7 +233,8 @@ class HttpRequest {
        /**
         * Take care of setting up the proxy
         * (override in subclass)
-        * @return string
+        *
+        * @return String
         */
        public function proxySetup() {
                global $wgHTTPProxy;
@@ -281,7 +292,8 @@ class HttpRequest {
 
        /**
         * Set the callback
-        * @param $callback callback
+        *
+        * @param $callback Callback
         */
        public function setCallback( $callback ) {
                $this->callback = $callback;
@@ -290,8 +302,9 @@ class HttpRequest {
        /**
         * A generic callback to read the body of the response from a remote
         * server.
+        *
         * @param $fh handle
-        * @param $content string
+        * @param $content String
         */
        public function read( $fh, $content ) {
                $this->content .= $content;
@@ -300,6 +313,7 @@ class HttpRequest {
 
        /**
         * Take care of whatever is necessary to perform the URI request.
+        *
         * @return Status
         */
        public function execute() {
@@ -336,7 +350,8 @@ class HttpRequest {
         * Parses the headers, including the HTTP status code and any
         * Set-Cookie headers.  This function expectes the headers to be
         * found in an array in the member variable headerList.
-        * @returns nothing
+        *
+        * @return nothing
         */
        protected function parseHeader() {
                $lastname = "";
@@ -359,7 +374,8 @@ class HttpRequest {
        /**
         * Sets the member variable status to a fatal status if the HTTP
         * status code was not 200.
-        * @returns nothing
+        *
+        * @return nothing
         */
        protected function setStatus() {
                if( !$this->respHeaders ) {
@@ -375,7 +391,8 @@ class HttpRequest {
 
        /**
         * Returns true if the last status code was a redirect.
-        * @return bool
+        *
+        * @return Boolean
         */
        public function isRedirect() {
                if( !$this->respHeaders ) {
@@ -383,7 +400,7 @@ class HttpRequest {
                }
 
                $status = (int)$this->respStatus;
-               if ( $status >= 300 && $status < 400 ) {
+               if ( $status >= 300 && $status <= 303 ) {
                        return true;
                }
                return false;
@@ -394,7 +411,8 @@ class HttpRequest {
         * request has been executed.  Because some headers
         * (e.g. Set-Cookie) can appear more than once the, each value of
         * the associative array is an array of the values given.
-        * @return array
+        *
+        * @return Array
         */
        public function getResponseHeaders() {
                if( !$this->respHeaders ) {
@@ -405,8 +423,9 @@ class HttpRequest {
 
        /**
         * Returns the value of the given response header.
-        * @param $header string
-        * @return string
+        *
+        * @param $header String
+        * @return String
         */
        public function getResponseHeader($header) {
                if( !$this->respHeaders ) {
@@ -421,6 +440,7 @@ class HttpRequest {
 
        /**
         * Tells the HttpRequest object to use this pre-loaded CookieJar.
+        *
         * @param $jar CookieJar
         */
        public function setCookieJar( $jar ) {
@@ -429,6 +449,7 @@ class HttpRequest {
 
        /**
         * Returns the cookie jar in use.
+        *
         * @returns CookieJar
         */
        public function getCookieJar() {
@@ -468,7 +489,8 @@ class HttpRequest {
 
        /**
         * Returns the final URL after all redirections.
-        * @returns string
+        *
+        * @return String
         */
        public function getFinalUrl() {
                $location = $this->getResponseHeader("Location");
@@ -478,6 +500,14 @@ class HttpRequest {
 
                return $this->url;
        }
+
+       /**
+        * Returns true if the backend can follow redirects. Overridden by the 
+        * child classes.
+        */
+       public function canFollowRedirects() {
+               return true;
+       }
 }
 
 
@@ -502,9 +532,9 @@ class Cookie {
         * Sets a cookie.  Used before a request to set up any individual
         * cookies.      Used internally after a request to parse the
         * Set-Cookie headers.
-        * @param $name string the name of the cookie
-        * @param $value string the value of the cookie
-        * @param $attr array possible key/values:
+        *
+        * @param $value String: the value of the cookie
+        * @param $attr Array: possible key/values:
         *              expires  A date string
         *              path     The path this cookie is used on
         *              domain   Domain this cookie is used on
@@ -537,9 +567,9 @@ class Cookie {
         * A better method might be to use a blacklist like
         * http://publicsuffix.org/
         *
-        * @param $domain string the domain to validate
-        * @param $originDomain string (optional) the domain the cookie originates from
-        * @return bool
+        * @param $domain String: the domain to validate
+        * @param $originDomain String: (optional) the domain the cookie originates from
+        * @return Boolean
         */
        public static function validateCookieDomain( $domain, $originDomain = null) {
                // Don't allow a trailing dot
@@ -547,9 +577,6 @@ class Cookie {
 
                $dc = explode(".", $domain);
 
-               // Don't allow cookies for "localhost", "ls" or other dot-less hosts
-               if( count($dc) < 2 ) return false;
-
                // Only allow full, valid IP addresses
                if( preg_match( '/^[0-9.]+$/', $domain ) ) {
                        if( count( $dc ) != 4 ) return false;
@@ -588,9 +615,10 @@ class Cookie {
 
        /**
         * Serialize the cookie jar into a format useful for HTTP Request headers.
-        * @param $path string the path that will be used. Required.
-        * @param $domain string the domain that will be used. Required.
-        * @return string
+        *
+        * @param $path String: the path that will be used. Required.
+        * @param $domain String: the domain that will be used. Required.
+        * @return String
         */
        public function serializeToHttpRequest( $path, $domain ) {
                $ret = "";
@@ -666,7 +694,9 @@ class CookieJar {
 
        /**
         * Parse the content of an Set-Cookie HTTP Response header.
-        * @param $cookie string
+        *
+        * @param $cookie String
+        * @param $domain String: cookie's domain
         */
        public function parseCookieResponseHeader ( $cookie, $domain ) {
                $len = strlen( "Set-Cookie:" );
@@ -692,7 +722,6 @@ class CookieJar {
                        } elseif ( !Cookie::validateCookieDomain( $attr['domain'], $domain ) ) {
                                return null;
                        }
-
                        $this->setCookie( $name, $value, $attr );
                }
        }
@@ -726,8 +755,8 @@ class CurlHttpRequest extends HttpRequest {
                $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
                $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback;
                $this->curlOptions[CURLOPT_HEADERFUNCTION] = array($this, "readHeader");
-               $this->curlOptions[CURLOPT_FOLLOWLOCATION] = $this->followRedirects;
                $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects;
+               $this->curlOptions[CURLOPT_ENCODING] = ""; # Enable compression
 
                /* not sure these two are actually necessary */
                if(isset($this->reqHeaders['Referer'])) {
@@ -735,9 +764,13 @@ class CurlHttpRequest extends HttpRequest {
                }
                $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent'];
 
-               if ( $this->sslVerifyHost ) {
+               if ( isset( $this->sslVerifyHost ) ) {
                        $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost;
                }
+               
+               if ( isset( $this->sslVerifyCert ) ) {
+                       $this->curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->sslVerifyCert;
+               }
 
                if ( $this->caInfo ) {
                        $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo;
@@ -760,7 +793,17 @@ class CurlHttpRequest extends HttpRequest {
                $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList();
 
                $curlHandle = curl_init( $this->url );
-               curl_setopt_array( $curlHandle, $this->curlOptions );
+               if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) {
+                       throw new MWException("Error setting curl options.");
+               }
+               if ( $this->followRedirects && $this->canFollowRedirects() ) {
+                       if ( ! @curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) {
+                               wfDebug( __METHOD__.": Couldn't set CURLOPT_FOLLOWLOCATION. " .
+                                       "Probably safe_mode or open_basedir is set.\n");
+                               // Continue the processing. If it were in curl_setopt_array, 
+                               // processing would have halted on its entry
+                       }
+               }
 
                if ( false === curl_exec( $curlHandle ) ) {
                        $code = curl_error( $curlHandle );
@@ -780,11 +823,21 @@ class CurlHttpRequest extends HttpRequest {
                $this->setStatus();
                return $this->status;
        }
+
+       public function canFollowRedirects() {
+               if ( strval( ini_get( 'open_basedir' ) ) !== '' || wfIniGetBool( 'safe_mode' ) ) {
+                       wfDebug( "Cannot follow redirects in safe mode\n" );
+                       return false;
+               }
+               if ( !defined( 'CURLOPT_REDIR_PROTOCOLS' ) ) {
+                       wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" );
+                       return false;
+               }
+               return true;
+       }
 }
 
 class PhpHttpRequest extends HttpRequest {
-       protected $manuallyRedirect = false;
-
        protected function urlToTcp( $url ) {
                $parsedUrl = parse_url( $url );
 
@@ -796,9 +849,7 @@ class PhpHttpRequest extends HttpRequest {
 
                // At least on Centos 4.8 with PHP 5.1.6, using max_redirects to follow redirects
                // causes a segfault
-               if ( version_compare( '5.1.7', phpversion(), '>' ) ) {
-                       $this->manuallyRedirect = true;
-               }
+               $manuallyRedirect = version_compare( phpversion(), '5.1.7', '<' );
 
                if ( $this->parsedUrl['scheme'] != 'http' ) {
                        $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] );
@@ -817,7 +868,7 @@ class PhpHttpRequest extends HttpRequest {
                        $options['request_fulluri'] = true;
                }
 
-               if ( !$this->followRedirects || $this->manuallyRedirect ) {
+               if ( !$this->followRedirects || $manuallyRedirect ) {
                        $options['max_redirects'] = 0;
                } else {
                        $options['max_redirects'] = $this->maxRedirects;
@@ -851,20 +902,31 @@ class PhpHttpRequest extends HttpRequest {
                $reqCount = 0;
                $url = $this->url;
                do {
-                       $again = false;
                        $reqCount++;
                        wfSuppressWarnings();
                        $fh = fopen( $url, "r", false, $context );
                        wfRestoreWarnings();
-                       if ( $fh ) {
-                               $result = stream_get_meta_data( $fh );
-                               $this->headerList = $result['wrapper_data'];
-                               $this->parseHeader();
-                               $url = $this->getResponseHeader("Location");
-                               $again = $this->manuallyRedirect && $this->followRedirects && $url
-                                       && $this->isRedirect() && $this->maxRedirects > $reqCount;
+                       if ( !$fh ) {
+                               break;
+                       }
+                       $result = stream_get_meta_data( $fh );
+                       $this->headerList = $result['wrapper_data'];
+                       $this->parseHeader();
+                       if ( !$manuallyRedirect || !$this->followRedirects ) {
+                               break;
+                       }
+
+                       # Handle manual redirection
+                       if ( !$this->isRedirect() || $reqCount > $this->maxRedirects ) {
+                               break;
+                       }
+                       # Check security of URL
+                       $url = $this->getResponseHeader("Location");
+                       if ( substr( $url, 0, 7 ) !== 'http://' ) {
+                               wfDebug( __METHOD__.": insecure redirection\n" );
+                               break;
                        }
-               } while ( $again );
+               } while ( true );
 
                if ( $oldTimeout !== false ) {
                        ini_set('default_socket_timeout', $oldTimeout);