Merge "Add more test cases to OldChangesListTest"
[lhc/web/wiklou.git] / includes / HttpFunctions.php
index 1c79485..bbf3de6 100644 (file)
@@ -250,7 +250,9 @@ class MWHttpRequest {
         * @param string $caller The method making this request, for profiling
         * @param Profiler $profiler An instance of the profiler for profiling, or null
         */
-       protected function __construct( $url, $options = array(), $caller = __METHOD__, $profiler = null ) {
+       protected function __construct(
+               $url, $options = array(), $caller = __METHOD__, $profiler = null
+       ) {
                global $wgHTTPTimeout, $wgHTTPConnectTimeout;
 
                $this->url = wfExpandUrl( $url, PROTO_HTTP );
@@ -678,7 +680,7 @@ class MWHttpRequest {
        public function getFinalUrl() {
                $headers = $this->getResponseHeaders();
 
-               //return full url (fix for incorrect but handled relative location)
+               // return full url (fix for incorrect but handled relative location)
                if ( isset( $headers['location'] ) ) {
                        $locations = $headers['location'];
                        $domain = '';
@@ -690,7 +692,7 @@ class MWHttpRequest {
 
                                if ( isset( $url['host'] ) ) {
                                        $domain = $url['scheme'] . '://' . $url['host'];
-                                       break; //found correct URI (with host)
+                                       break; // found correct URI (with host)
                                } else {
                                        $foundRelativeURI = true;
                                }
@@ -838,23 +840,27 @@ class CurlHttpRequest extends MWHttpRequest {
         * @return bool
         */
        public function canFollowRedirects() {
-               if ( strval( ini_get( 'open_basedir' ) ) !== '' || wfIniGetBool( 'safe_mode' ) ) {
-                       wfDebug( "Cannot follow redirects in safe mode\n" );
-                       return false;
-               }
-
                $curlVersionInfo = curl_version();
                if ( $curlVersionInfo['version_number'] < 0x071304 ) {
                        wfDebug( "Cannot follow redirects with libcurl < 7.19.4 due to CVE-2009-0037\n" );
                        return false;
                }
 
+               if ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
+                       if ( strval( ini_get( 'open_basedir' ) ) !== '' || wfIniGetBool( 'safe_mode' ) ) {
+                               wfDebug( "Cannot follow redirects in safe mode\n" );
+                               return false;
+                       }
+               }
+
                return true;
        }
 }
 
 class PhpHttpRequest extends MWHttpRequest {
 
+       private $fopenErrors = array();
+
        /**
         * @param string $url
         * @return string
@@ -865,6 +871,67 @@ class PhpHttpRequest extends MWHttpRequest {
                return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
        }
 
+       /**
+        * Returns an array with a 'capath' or 'cafile' key
+        * that is suitable to be merged into the 'ssl' sub-array of
+        * a stream context options array.
+        * Uses the 'caInfo' option of the class if it is provided, otherwise uses the system
+        * default CA bundle if PHP supports that, or searches a few standard locations.
+        * @return array
+        * @throws DomainException
+        */
+       protected function getCertOptions() {
+               $certOptions = array();
+               $certLocations = array();
+               if ( $this->caInfo ) {
+                       $certLocations = array( 'manual' => $this->caInfo );
+               } elseif ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
+                       // @codingStandardsIgnoreStart Generic.Files.LineLength
+                       // Default locations, based on
+                       // https://www.happyassassin.net/2015/01/12/a-note-about-ssltls-trusted-certificate-stores-and-platforms/
+                       // PHP 5.5 and older doesn't have any defaults, so we try to guess ourselves.
+                       // PHP 5.6+ gets the CA location from OpenSSL as long as it is not set manually,
+                       // so we should leave capath/cafile empty there.
+                       // @codingStandardsIgnoreEnd
+                       $certLocations = array_filter( array(
+                               getenv( 'SSL_CERT_DIR' ),
+                               getenv( 'SSL_CERT_PATH' ),
+                               '/etc/pki/tls/certs/ca-bundle.crt', # Fedora et al
+                               '/etc/ssl/certs',  # Debian et al
+                               '/etc/pki/tls/certs/ca-bundle.trust.crt',
+                               '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
+                               '/System/Library/OpenSSL', # OSX
+                       ) );
+               }
+
+               foreach ( $certLocations as $key => $cert ) {
+                       if ( is_dir( $cert ) ) {
+                               $certOptions['capath'] = $cert;
+                               break;
+                       } elseif ( is_file( $cert ) ) {
+                               $certOptions['cafile'] = $cert;
+                               break;
+                       } elseif ( $key === 'manual' ) {
+                               // fail more loudly if a cert path was manually configured and it is not valid
+                               throw new DomainException( "Invalid CA info passed: $cert" );
+                       }
+               }
+
+               return $certOptions;
+       }
+
+       /**
+        * Custom error handler for dealing with fopen() errors.
+        * fopen() tends to fire multiple errors in succession, and the last one
+        * is completely useless (something like "fopen: failed to open stream")
+        * so normal methods of handling errors programmatically
+        * like get_last_error() don't work.
+        */
+       public function errorHandler( $errno, $errstr ) {
+               $n = count( $this->fopenErrors ) + 1;
+               $this->fopenErrors += array( "errno$n" => $errno, "errstr$n" => $errstr );
+       }
+
        public function execute() {
 
                parent::execute();
@@ -926,13 +993,7 @@ class PhpHttpRequest extends MWHttpRequest {
                        }
                }
 
-               if ( is_dir( $this->caInfo ) ) {
-                       $options['ssl']['capath'] = $this->caInfo;
-               } elseif ( is_file( $this->caInfo ) ) {
-                       $options['ssl']['cafile'] = $this->caInfo;
-               } elseif ( $this->caInfo ) {
-                       throw new MWException( "Invalid CA info passed: {$this->caInfo}" );
-               }
+               $options['ssl'] += $this->getCertOptions();
 
                $context = stream_context_create( $options );
 
@@ -949,9 +1010,10 @@ class PhpHttpRequest extends MWHttpRequest {
                }
                do {
                        $reqCount++;
-                       MediaWiki\suppressWarnings();
+                       $this->fopenErrors = array();
+                       set_error_handler( array( $this, 'errorHandler' ) );
                        $fh = fopen( $url, "r", false, $context );
-                       MediaWiki\restoreWarnings();
+                       restore_error_handler();
 
                        if ( !$fh ) {
                                // HACK for instant commons.
@@ -997,6 +1059,10 @@ class PhpHttpRequest extends MWHttpRequest {
                $this->setStatus();
 
                if ( $fh === false ) {
+                       if ( $this->fopenErrors ) {
+                               LoggerFactory::getInstance( 'http' )->warning( __CLASS__
+                                       . ': error opening connection: {errstr1}', $this->fopenErrors );
+                       }
                        $this->status->fatal( 'http-request-error' );
                        return $this->status;
                }