2019-01-27 20:00:39 +00:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Component\HttpClient\Response ;
2019-04-04 15:02:57 +01:00
use Psr\Log\LoggerInterface ;
2019-01-27 20:00:39 +00:00
use Symfony\Component\HttpClient\Chunk\FirstChunk ;
2019-08-30 11:36:56 +01:00
use Symfony\Component\HttpClient\Chunk\InformationalChunk ;
2019-01-27 20:00:39 +00:00
use Symfony\Component\HttpClient\Exception\TransportException ;
2019-04-07 10:02:11 +01:00
use Symfony\Component\HttpClient\Internal\CurlClientState ;
2019-01-27 20:00:39 +00:00
use Symfony\Contracts\HttpClient\ResponseInterface ;
/**
* @ author Nicolas Grekas < p @ tchwork . com >
*
* @ internal
*/
final class CurlResponse implements ResponseInterface
{
use ResponseTrait ;
private static $performing = false ;
2019-04-07 10:02:11 +01:00
private $multi ;
2019-05-27 19:15:39 +01:00
private $debugBuffer ;
2019-01-27 20:00:39 +00:00
/**
* @ internal
*/
2019-04-07 10:02:11 +01:00
public function __construct ( CurlClientState $multi , $ch , array $options = null , LoggerInterface $logger = null , string $method = 'GET' , callable $resolveRedirect = null )
2019-01-27 20:00:39 +00:00
{
$this -> multi = $multi ;
if ( \is_resource ( $ch )) {
$this -> handle = $ch ;
2019-05-27 19:15:39 +01:00
$this -> debugBuffer = fopen ( 'php://temp' , 'w+' );
curl_setopt ( $ch , CURLOPT_VERBOSE , true );
curl_setopt ( $ch , CURLOPT_STDERR , $this -> debugBuffer );
2019-01-27 20:00:39 +00:00
} else {
$this -> info [ 'url' ] = $ch ;
$ch = $this -> handle ;
}
$this -> id = $id = ( int ) $ch ;
2019-04-04 15:02:57 +01:00
$this -> logger = $logger ;
2019-01-27 20:00:39 +00:00
$this -> timeout = $options [ 'timeout' ] ? ? null ;
2019-03-08 13:46:03 +00:00
$this -> info [ 'http_method' ] = $method ;
2019-01-27 20:00:39 +00:00
$this -> info [ 'user_data' ] = $options [ 'user_data' ] ? ? null ;
$this -> info [ 'start_time' ] = $this -> info [ 'start_time' ] ? ? microtime ( true );
$info = & $this -> info ;
2019-03-08 13:46:03 +00:00
$headers = & $this -> headers ;
2019-06-18 12:47:18 +01:00
$debugBuffer = $this -> debugBuffer ;
2019-01-27 20:00:39 +00:00
2019-04-02 11:03:40 +01:00
if ( ! $info [ 'response_headers' ]) {
2019-01-27 20:00:39 +00:00
// Used to keep track of what we're waiting for
curl_setopt ( $ch , CURLOPT_PRIVATE , 'headers' );
}
if ( null === $content = & $this -> content ) {
$content = ( $options [ 'buffer' ] ? ? true ) ? fopen ( 'php://temp' , 'w+' ) : null ;
} else {
// Move the pushed response to the activity list
if ( ftell ( $content )) {
rewind ( $content );
$multi -> handlesActivity [ $id ][] = stream_get_contents ( $content );
}
$content = ( $options [ 'buffer' ] ? ? true ) ? $content : null ;
}
2019-04-04 15:02:57 +01:00
curl_setopt ( $ch , CURLOPT_HEADERFUNCTION , static function ( $ch , string $data ) use ( & $info , & $headers , $options , $multi , $id , & $location , $resolveRedirect , $logger ) : int {
return self :: parseHeaderLine ( $ch , $data , $info , $headers , $options , $multi , $id , $location , $resolveRedirect , $logger );
2019-01-27 20:00:39 +00:00
});
if ( null === $options ) {
// Pushed response: buffer until requested
curl_setopt ( $ch , CURLOPT_WRITEFUNCTION , static function ( $ch , string $data ) use ( & $content ) : int {
return fwrite ( $content , $data );
});
return ;
}
if ( $onProgress = $options [ 'on_progress' ]) {
$url = isset ( $info [ 'url' ]) ? [ 'url' => $info [ 'url' ]] : [];
curl_setopt ( $ch , CURLOPT_NOPROGRESS , false );
2019-06-18 12:47:18 +01:00
curl_setopt ( $ch , CURLOPT_PROGRESSFUNCTION , static function ( $ch , $dlSize , $dlNow ) use ( $onProgress , & $info , $url , $multi , $debugBuffer ) {
2019-01-27 20:00:39 +00:00
try {
2019-06-18 12:47:18 +01:00
rewind ( $debugBuffer );
$debug = [ 'debug' => stream_get_contents ( $debugBuffer )];
$onProgress ( $dlNow , $dlSize , $url + curl_getinfo ( $ch ) + $info + $debug );
2019-01-27 20:00:39 +00:00
} catch ( \Throwable $e ) {
2019-03-11 09:55:59 +00:00
$multi -> handlesActivity [( int ) $ch ][] = null ;
$multi -> handlesActivity [( int ) $ch ][] = $e ;
2019-01-27 20:00:39 +00:00
return 1 ; // Abort the request
}
2019-08-19 22:30:37 +01:00
return null ;
2019-01-27 20:00:39 +00:00
});
}
curl_setopt ( $ch , CURLOPT_WRITEFUNCTION , static function ( $ch , string $data ) use ( & $content , $multi , $id ) : int {
$multi -> handlesActivity [ $id ][] = $data ;
return null !== $content ? fwrite ( $content , $data ) : \strlen ( $data );
});
$this -> initializer = static function ( self $response ) {
if ( null !== $response -> info [ 'error' ]) {
throw new TransportException ( $response -> info [ 'error' ]);
}
2019-04-04 15:02:57 +01:00
$waitFor = curl_getinfo ( $ch = $response -> handle , CURLINFO_PRIVATE );
if ( \in_array ( $waitFor , [ 'headers' , 'destruct' ], true )) {
2019-01-27 20:00:39 +00:00
try {
self :: stream ([ $response ]) -> current ();
} catch ( \Throwable $e ) {
2019-03-11 09:55:59 +00:00
// Persist timeouts thrown during initialization
2019-01-27 20:00:39 +00:00
$response -> info [ 'error' ] = $e -> getMessage ();
$response -> close ();
throw $e ;
}
2019-04-04 15:02:57 +01:00
} elseif ( 'content' === $waitFor && ( $response -> multi -> handlesActivity [ $response -> id ][ 0 ] ? ? null ) instanceof FirstChunk ) {
self :: stream ([ $response ]) -> current ();
2019-01-27 20:00:39 +00:00
}
curl_setopt ( $ch , CURLOPT_HEADERFUNCTION , null );
curl_setopt ( $ch , CURLOPT_READFUNCTION , null );
curl_setopt ( $ch , CURLOPT_INFILE , null );
};
// Schedule the request in a non-blocking way
2019-09-03 13:17:24 +01:00
$multi -> openHandles [ $id ] = [ $ch , $options ];
2019-01-27 20:00:39 +00:00
curl_multi_add_handle ( $multi -> handle , $ch );
self :: perform ( $multi );
}
/**
* { @ inheritdoc }
*/
public function getInfo ( string $type = null )
{
if ( ! $info = $this -> finalInfo ) {
self :: perform ( $this -> multi );
2019-05-27 19:15:39 +01:00
2019-01-27 20:00:39 +00:00
$info = array_merge ( $this -> info , curl_getinfo ( $this -> handle ));
$info [ 'url' ] = $this -> info [ 'url' ] ? ? $info [ 'url' ];
$info [ 'redirect_url' ] = $this -> info [ 'redirect_url' ] ? ? null ;
// workaround curl not subtracting the time offset for pushed responses
if ( isset ( $this -> info [ 'url' ]) && $info [ 'start_time' ] / 1000 < $info [ 'total_time' ]) {
$info [ 'total_time' ] -= $info [ 'starttransfer_time' ] ? : $info [ 'total_time' ];
$info [ 'starttransfer_time' ] = 0.0 ;
}
2019-06-18 12:47:18 +01:00
rewind ( $this -> debugBuffer );
$info [ 'debug' ] = stream_get_contents ( $this -> debugBuffer );
2019-01-27 20:00:39 +00:00
if ( ! \in_array ( curl_getinfo ( $this -> handle , CURLINFO_PRIVATE ), [ 'headers' , 'content' ], true )) {
2019-06-04 07:38:41 +01:00
curl_setopt ( $this -> handle , CURLOPT_VERBOSE , false );
2019-06-12 14:33:27 +01:00
rewind ( $this -> debugBuffer );
ftruncate ( $this -> debugBuffer , 0 );
2019-01-27 20:00:39 +00:00
$this -> finalInfo = $info ;
}
}
return null !== $type ? $info [ $type ] ? ? null : $info ;
}
public function __destruct ()
{
try {
2019-04-04 15:02:57 +01:00
if ( null === $this -> timeout ) {
return ; // Unused pushed response
2019-01-27 20:00:39 +00:00
}
if ( 'content' === $waitFor = curl_getinfo ( $this -> handle , CURLINFO_PRIVATE )) {
$this -> close ();
} elseif ( 'headers' === $waitFor ) {
curl_setopt ( $this -> handle , CURLOPT_PRIVATE , 'destruct' );
}
$this -> doDestruct ();
} finally {
$this -> close ();
// Clear local caches when the only remaining handles are about pushed responses
2019-04-04 15:02:57 +01:00
if ( ! $this -> multi -> openHandles ) {
if ( $this -> logger ) {
foreach ( $this -> multi -> pushedResponses as $url => $response ) {
2019-04-05 14:04:18 +01:00
$this -> logger -> debug ( sprintf ( 'Unused pushed response: "%s"' , $url ));
2019-04-04 15:02:57 +01:00
}
}
2019-01-27 20:00:39 +00:00
$this -> multi -> pushedResponses = [];
// Schedule DNS cache eviction for the next request
2019-04-07 10:02:11 +01:00
$this -> multi -> dnsCache -> evictions = $this -> multi -> dnsCache -> evictions ? : $this -> multi -> dnsCache -> removals ;
$this -> multi -> dnsCache -> removals = $this -> multi -> dnsCache -> hostnames = [];
2019-01-27 20:00:39 +00:00
}
}
}
/**
* { @ inheritdoc }
*/
2019-04-07 10:02:11 +01:00
private function close () : void
2019-01-27 20:00:39 +00:00
{
unset ( $this -> multi -> openHandles [ $this -> id ], $this -> multi -> handlesActivity [ $this -> id ]);
curl_multi_remove_handle ( $this -> multi -> handle , $this -> handle );
curl_setopt_array ( $this -> handle , [
CURLOPT_PRIVATE => '' ,
CURLOPT_NOPROGRESS => true ,
CURLOPT_PROGRESSFUNCTION => null ,
CURLOPT_HEADERFUNCTION => null ,
CURLOPT_WRITEFUNCTION => null ,
CURLOPT_READFUNCTION => null ,
CURLOPT_INFILE => null ,
]);
}
/**
* { @ inheritdoc }
*/
2019-04-07 10:02:11 +01:00
private static function schedule ( self $response , array & $runningResponses ) : void
2019-01-27 20:00:39 +00:00
{
2019-03-11 09:55:59 +00:00
if ( isset ( $runningResponses [ $i = ( int ) $response -> multi -> handle ])) {
2019-01-27 20:00:39 +00:00
$runningResponses [ $i ][ 1 ][ $response -> id ] = $response ;
} else {
$runningResponses [ $i ] = [ $response -> multi , [ $response -> id => $response ]];
}
2019-03-11 09:55:59 +00:00
if ( '' === curl_getinfo ( $ch = $response -> handle , CURLINFO_PRIVATE )) {
// Response already completed
$response -> multi -> handlesActivity [ $response -> id ][] = null ;
$response -> multi -> handlesActivity [ $response -> id ][] = null !== $response -> info [ 'error' ] ? new TransportException ( $response -> info [ 'error' ]) : null ;
}
2019-01-27 20:00:39 +00:00
}
/**
* { @ inheritdoc }
*/
2019-04-07 10:02:11 +01:00
private static function perform ( CurlClientState $multi , array & $responses = null ) : void
2019-01-27 20:00:39 +00:00
{
if ( self :: $performing ) {
return ;
}
try {
self :: $performing = true ;
2019-08-06 14:20:03 +01:00
$active = 0 ;
2019-01-27 20:00:39 +00:00
while ( CURLM_CALL_MULTI_PERFORM === curl_multi_exec ( $multi -> handle , $active ));
while ( $info = curl_multi_info_read ( $multi -> handle )) {
$multi -> handlesActivity [( int ) $info [ 'handle' ]][] = null ;
2019-05-30 10:47:28 +01:00
$multi -> handlesActivity [( int ) $info [ 'handle' ]][] = \in_array ( $info [ 'result' ], [ \CURLE_OK , \CURLE_TOO_MANY_REDIRECTS ], true ) || ( \CURLE_WRITE_ERROR === $info [ 'result' ] && 'destruct' === @ curl_getinfo ( $info [ 'handle' ], CURLINFO_PRIVATE )) ? null : new TransportException ( sprintf ( '%s for "%s".' , curl_strerror ( $info [ 'result' ]), curl_getinfo ( $info [ 'handle' ], CURLINFO_EFFECTIVE_URL )));
2019-01-27 20:00:39 +00:00
}
} finally {
self :: $performing = false ;
}
}
/**
* { @ inheritdoc }
*/
2019-04-07 10:02:11 +01:00
private static function select ( CurlClientState $multi , float $timeout ) : int
2019-01-27 20:00:39 +00:00
{
return curl_multi_select ( $multi -> handle , $timeout );
}
/**
* Parses header lines as curl yields them to us .
*/
2019-04-07 10:02:11 +01:00
private static function parseHeaderLine ( $ch , string $data , array & $info , array & $headers , ? array $options , CurlClientState $multi , int $id , ? string & $location , ? callable $resolveRedirect , ? LoggerInterface $logger ) : int
2019-01-27 20:00:39 +00:00
{
if ( ! \in_array ( $waitFor = @ curl_getinfo ( $ch , CURLINFO_PRIVATE ), [ 'headers' , 'destruct' ], true )) {
return \strlen ( $data ); // Ignore HTTP trailers
}
if ( " \r \n " !== $data ) {
// Regular header line: add it to the list
2019-04-02 11:03:40 +01:00
self :: addResponseHeaders ([ substr ( $data , 0 , - 2 )], $info , $headers );
2019-03-08 13:46:03 +00:00
2019-06-23 18:28:28 +01:00
if ( 0 !== strpos ( $data , 'HTTP/' )) {
if ( 0 === stripos ( $data , 'Location:' )) {
$location = trim ( substr ( $data , 9 , - 2 ));
}
return \strlen ( $data );
}
if ( \function_exists ( 'openssl_x509_read' ) && $certinfo = curl_getinfo ( $ch , CURLINFO_CERTINFO )) {
$info [ 'peer_certificate_chain' ] = array_map ( 'openssl_x509_read' , array_column ( $certinfo , 'Cert' ));
}
if ( 300 <= $info [ 'http_code' ] && $info [ 'http_code' ] < 400 ) {
2019-03-08 13:46:03 +00:00
if ( curl_getinfo ( $ch , CURLINFO_REDIRECT_COUNT ) === $options [ 'max_redirects' ]) {
curl_setopt ( $ch , CURLOPT_FOLLOWLOCATION , false );
} elseif ( 303 === $info [ 'http_code' ] || ( 'POST' === $info [ 'http_method' ] && \in_array ( $info [ 'http_code' ], [ 301 , 302 ], true ))) {
$info [ 'http_method' ] = 'HEAD' === $info [ 'http_method' ] ? 'HEAD' : 'GET' ;
curl_setopt ( $ch , CURLOPT_POSTFIELDS , '' );
}
}
2019-01-27 20:00:39 +00:00
return \strlen ( $data );
}
2019-08-30 11:36:56 +01:00
// End of headers: handle informational responses, redirects, etc.
2019-06-23 18:28:28 +01:00
if ( 200 > $statusCode = curl_getinfo ( $ch , CURLINFO_RESPONSE_CODE )) {
2019-08-30 11:36:56 +01:00
$multi -> handlesActivity [ $id ][] = new InformationalChunk ( $statusCode , $headers );
2019-06-23 18:28:28 +01:00
return \strlen ( $data );
}
2019-01-27 20:00:39 +00:00
$info [ 'redirect_url' ] = null ;
if ( 300 <= $statusCode && $statusCode < 400 && null !== $location ) {
2019-06-04 09:18:38 +01:00
if ( null === $info [ 'redirect_url' ] = $resolveRedirect ( $ch , $location )) {
$options [ 'max_redirects' ] = curl_getinfo ( $ch , CURLINFO_REDIRECT_COUNT );
curl_setopt ( $ch , CURLOPT_FOLLOWLOCATION , false );
curl_setopt ( $ch , CURLOPT_MAXREDIRS , $options [ 'max_redirects' ]);
} else {
$url = parse_url ( $location ? ? ':' );
if ( isset ( $url [ 'host' ]) && null !== $ip = $multi -> dnsCache -> hostnames [ $url [ 'host' ] = strtolower ( $url [ 'host' ])] ? ? null ) {
// Populate DNS cache for redirects if needed
$port = $url [ 'port' ] ? ? ( 'http' === ( $url [ 'scheme' ] ? ? parse_url ( curl_getinfo ( $ch , CURLINFO_EFFECTIVE_URL ), PHP_URL_SCHEME )) ? 80 : 443 );
curl_setopt ( $ch , CURLOPT_RESOLVE , [ " { $url [ 'host' ] } : $port : $ip " ]);
$multi -> dnsCache -> removals [ " - { $url [ 'host' ] } : $port " ] = " - { $url [ 'host' ] } : $port " ;
}
2019-01-27 20:00:39 +00:00
}
}
$location = null ;
if ( $statusCode < 300 || 400 <= $statusCode || curl_getinfo ( $ch , CURLINFO_REDIRECT_COUNT ) === $options [ 'max_redirects' ]) {
// Headers and redirects completed, time to get the response's body
2019-08-30 11:36:56 +01:00
$multi -> handlesActivity [ $id ][] = new FirstChunk ();
2019-01-27 20:00:39 +00:00
if ( 'destruct' === $waitFor ) {
return 0 ;
}
curl_setopt ( $ch , CURLOPT_PRIVATE , 'content' );
2019-04-04 15:02:57 +01:00
} elseif ( null !== $info [ 'redirect_url' ] && $logger ) {
2019-04-05 14:04:18 +01:00
$logger -> info ( sprintf ( 'Redirecting: "%s %s"' , $info [ 'http_code' ], $info [ 'redirect_url' ]));
2019-01-27 20:00:39 +00:00
}
return \strlen ( $data );
}
}