2018-09-12 16:58:54 +00:00
< ? php
/*
* This file is part of Composer .
*
* ( c ) Nils Adermann < naderman @ naderman . de >
* Jordi Boggiano < j . boggiano @ seld . be >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Composer\Util\Http ;
use Composer\Config ;
2020-08-14 14:55:32 +00:00
use Composer\Downloader\MaxFileSizeExceededException ;
2018-09-12 16:58:54 +00:00
use Composer\IO\IOInterface ;
use Composer\Downloader\TransportException ;
2018-11-14 16:54:19 +00:00
use Composer\Util\StreamContextFactory ;
use Composer\Util\AuthHelper ;
2018-11-16 13:28:00 +00:00
use Composer\Util\Url ;
2019-02-21 13:49:06 +00:00
use Composer\Util\HttpDownloader ;
2018-09-12 16:58:54 +00:00
use React\Promise\Promise ;
/**
2020-06-05 07:06:49 +00:00
* @ internal
2018-09-12 16:58:54 +00:00
* @ author Jordi Boggiano < j . boggiano @ seld . be >
* @ author Nicolas Grekas < p @ tchwork . com >
*/
class CurlDownloader
{
private $multiHandle ;
private $shareHandle ;
private $jobs = array ();
2018-11-14 16:54:19 +00:00
/** @var IOInterface */
2018-09-12 16:58:54 +00:00
private $io ;
2018-11-14 16:54:19 +00:00
/** @var Config */
private $config ;
/** @var AuthHelper */
private $authHelper ;
2018-09-12 16:58:54 +00:00
private $selectTimeout = 5.0 ;
2018-11-14 16:54:19 +00:00
private $maxRedirects = 20 ;
2020-09-24 15:48:22 +00:00
/** @var ProxyManager */
private $proxyManager ;
private $supportsSecureProxy ;
2018-09-12 16:58:54 +00:00
protected $multiErrors = array (
2020-11-22 13:48:56 +00:00
CURLM_BAD_HANDLE => array ( 'CURLM_BAD_HANDLE' , 'The passed-in handle is not a valid CURLM handle.' ),
2018-09-12 16:58:54 +00:00
CURLM_BAD_EASY_HANDLE => array ( 'CURLM_BAD_EASY_HANDLE' , " An easy handle was not good/valid. It could mean that it isn't an easy handle at all, or possibly that the handle already is in used by this or another multi handle. " ),
2020-11-22 13:48:56 +00:00
CURLM_OUT_OF_MEMORY => array ( 'CURLM_OUT_OF_MEMORY' , 'You are doomed.' ),
CURLM_INTERNAL_ERROR => array ( 'CURLM_INTERNAL_ERROR' , 'This can only be returned if libcurl bugs. Please report it to us!' ),
2018-09-12 16:58:54 +00:00
);
private static $options = array (
'http' => array (
'method' => CURLOPT_CUSTOMREQUEST ,
'content' => CURLOPT_POSTFIELDS ,
2018-11-14 16:54:19 +00:00
'header' => CURLOPT_HTTPHEADER ,
2020-10-27 12:49:33 +00:00
'timeout' => CURLOPT_TIMEOUT ,
2018-09-12 16:58:54 +00:00
),
'ssl' => array (
'cafile' => CURLOPT_CAINFO ,
'capath' => CURLOPT_CAPATH ,
2020-07-17 15:22:41 +00:00
'verify_peer' => CURLOPT_SSL_VERIFYPEER ,
'verify_peer_name' => CURLOPT_SSL_VERIFYHOST ,
2020-10-30 14:03:21 +00:00
'local_cert' => CURLOPT_SSLCERT ,
'local_pk' => CURLOPT_SSLKEY ,
'passphrase' => CURLOPT_SSLKEYPASSWD ,
2018-09-12 16:58:54 +00:00
),
);
private static $timeInfo = array (
'total_time' => true ,
'namelookup_time' => true ,
'connect_time' => true ,
'pretransfer_time' => true ,
'starttransfer_time' => true ,
'redirect_time' => true ,
);
public function __construct ( IOInterface $io , Config $config , array $options = array (), $disableTls = false )
{
$this -> io = $io ;
2018-11-14 16:54:19 +00:00
$this -> config = $config ;
2018-09-12 16:58:54 +00:00
$this -> multiHandle = $mh = curl_multi_init ();
if ( function_exists ( 'curl_multi_setopt' )) {
2019-12-07 17:58:17 +00:00
curl_multi_setopt ( $mh , CURLMOPT_PIPELINING , PHP_VERSION_ID >= 70400 ? /* CURLPIPE_MULTIPLEX */ 2 : /*CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX*/ 3 );
2020-06-02 13:52:38 +00:00
if ( defined ( 'CURLMOPT_MAX_HOST_CONNECTIONS' ) && ! defined ( 'HHVM_VERSION' )) {
2018-09-12 16:58:54 +00:00
curl_multi_setopt ( $mh , CURLMOPT_MAX_HOST_CONNECTIONS , 8 );
}
}
if ( function_exists ( 'curl_share_init' )) {
$this -> shareHandle = $sh = curl_share_init ();
curl_share_setopt ( $sh , CURLSHOPT_SHARE , CURL_LOCK_DATA_COOKIE );
curl_share_setopt ( $sh , CURLSHOPT_SHARE , CURL_LOCK_DATA_DNS );
curl_share_setopt ( $sh , CURLSHOPT_SHARE , CURL_LOCK_DATA_SSL_SESSION );
}
2018-11-14 16:54:19 +00:00
$this -> authHelper = new AuthHelper ( $io , $config );
2020-09-24 15:48:22 +00:00
$this -> proxyManager = ProxyManager :: getInstance ();
$version = curl_version ();
$features = $version [ 'features' ];
$this -> supportsSecureProxy = defined ( 'CURL_VERSION_HTTPS_PROXY' ) && ( $features & CURL_VERSION_HTTPS_PROXY );
2018-09-12 16:58:54 +00:00
}
2020-06-05 07:06:49 +00:00
/**
* @ return int internal job id
*/
2018-09-12 16:58:54 +00:00
public function download ( $resolve , $reject , $origin , $url , $options , $copyTo = null )
{
2018-11-16 14:38:01 +00:00
$attributes = array ();
if ( isset ( $options [ 'retry-auth-failure' ])) {
$attributes [ 'retryAuthFailure' ] = $options [ 'retry-auth-failure' ];
unset ( $options [ 'retry-auth-failure' ]);
}
return $this -> initDownload ( $resolve , $reject , $origin , $url , $options , $copyTo , $attributes );
2018-11-14 16:54:19 +00:00
}
2018-09-12 16:58:54 +00:00
2020-06-05 07:06:49 +00:00
/**
* @ return int internal job id
*/
2018-11-14 16:54:19 +00:00
private function initDownload ( $resolve , $reject , $origin , $url , $options , $copyTo = null , array $attributes = array ())
{
$attributes = array_merge ( array (
'retryAuthFailure' => true ,
2018-12-04 14:17:52 +00:00
'redirects' => 0 ,
2018-11-14 16:54:19 +00:00
'storeAuth' => false ,
), $attributes );
$originalOptions = $options ;
2018-12-04 16:27:23 +00:00
// check URL can be accessed (i.e. is not insecure), but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256
if ( ! preg_match ( '{^http://(repo\.)?packagist\.org/p/}' , $url ) || ( false === strpos ( $url , '$' ) && false === strpos ( $url , '%24' ))) {
$this -> config -> prohibitUrlByConfig ( $url , $this -> io );
}
2018-11-14 16:54:19 +00:00
$curlHandle = curl_init ();
$headerHandle = fopen ( 'php://temp/maxmemory:32768' , 'w+b' );
if ( $copyTo ) {
$errorMessage = '' ;
set_error_handler ( function ( $code , $msg ) use ( & $errorMessage ) {
if ( $errorMessage ) {
$errorMessage .= " \n " ;
}
$errorMessage .= preg_replace ( '{^fopen\(.*?\): }' , '' , $msg );
});
$bodyHandle = fopen ( $copyTo . '~' , 'w+b' );
restore_error_handler ();
if ( ! $bodyHandle ) {
throw new TransportException ( 'The "' . $url . '" file could not be written to ' . $copyTo . ': ' . $errorMessage );
}
} else {
$bodyHandle = @ fopen ( 'php://temp/maxmemory:524288' , 'w+b' );
}
2018-09-12 16:58:54 +00:00
2018-11-14 16:54:19 +00:00
curl_setopt ( $curlHandle , CURLOPT_URL , $url );
2018-11-16 13:28:00 +00:00
curl_setopt ( $curlHandle , CURLOPT_FOLLOWLOCATION , false );
2018-11-14 16:54:19 +00:00
curl_setopt ( $curlHandle , CURLOPT_CONNECTTIMEOUT , 10 );
2020-10-14 11:30:12 +00:00
curl_setopt ( $curlHandle , CURLOPT_TIMEOUT , 300 );
2018-11-14 16:54:19 +00:00
curl_setopt ( $curlHandle , CURLOPT_WRITEHEADER , $headerHandle );
curl_setopt ( $curlHandle , CURLOPT_FILE , $bodyHandle );
curl_setopt ( $curlHandle , CURLOPT_ENCODING , " gzip " );
2020-11-22 13:48:56 +00:00
curl_setopt ( $curlHandle , CURLOPT_PROTOCOLS , CURLPROTO_HTTP | CURLPROTO_HTTPS );
2018-11-14 16:54:19 +00:00
if ( function_exists ( 'curl_share_init' )) {
curl_setopt ( $curlHandle , CURLOPT_SHARE , $this -> shareHandle );
2018-09-12 16:58:54 +00:00
}
if ( ! isset ( $options [ 'http' ][ 'header' ])) {
$options [ 'http' ][ 'header' ] = array ();
}
2018-11-14 16:54:19 +00:00
$options [ 'http' ][ 'header' ] = array_diff ( $options [ 'http' ][ 'header' ], array ( 'Connection: close' ));
$options [ 'http' ][ 'header' ][] = 'Connection: keep-alive' ;
2018-09-12 16:58:54 +00:00
2018-11-14 16:54:19 +00:00
$version = curl_version ();
$features = $version [ 'features' ];
if ( 0 === strpos ( $url , 'https://' ) && \defined ( 'CURL_VERSION_HTTP2' ) && \defined ( 'CURL_HTTP_VERSION_2_0' ) && ( CURL_VERSION_HTTP2 & $features )) {
curl_setopt ( $curlHandle , CURLOPT_HTTP_VERSION , CURL_HTTP_VERSION_2_0 );
2018-09-12 16:58:54 +00:00
}
2018-11-14 16:54:19 +00:00
$options [ 'http' ][ 'header' ] = $this -> authHelper -> addAuthenticationHeader ( $options [ 'http' ][ 'header' ], $origin , $url );
2020-09-24 15:48:22 +00:00
// Merge in headers - we don't get any proxy values
$options = StreamContextFactory :: initOptions ( $url , $options , true );
2018-09-12 16:58:54 +00:00
foreach ( self :: $options as $type => $curlOptions ) {
foreach ( $curlOptions as $name => $curlOption ) {
if ( isset ( $options [ $type ][ $name ])) {
2020-07-17 15:22:41 +00:00
if ( $type === 'ssl' && $name === 'verify_peer_name' ) {
curl_setopt ( $curlHandle , $curlOption , $options [ $type ][ $name ] === true ? 2 : $options [ $type ][ $name ]);
} else {
curl_setopt ( $curlHandle , $curlOption , $options [ $type ][ $name ]);
}
2018-09-12 16:58:54 +00:00
}
}
}
2020-09-24 15:48:22 +00:00
// Always set CURLOPT_PROXY to enable/disable proxy handling
// Any proxy authorization is included in the proxy url
$proxy = $this -> proxyManager -> getProxyForRequest ( $url );
curl_setopt ( $curlHandle , CURLOPT_PROXY , $proxy -> getUrl ());
// Curl needs certificate locations for secure proxies.
// CURLOPT_PROXY_SSL_VERIFY_PEER/HOST are enabled by default
if ( $proxy -> isSecure ()) {
if ( ! $this -> supportsSecureProxy ) {
throw new TransportException ( 'Connecting to a secure proxy using curl is not supported on PHP versions below 7.3.0.' );
}
if ( ! empty ( $options [ 'ssl' ][ 'cafile' ])) {
curl_setopt ( $curlHandle , CURLOPT_PROXY_CAINFO , $options [ 'ssl' ][ 'cafile' ]);
}
if ( ! empty ( $options [ 'ssl' ][ 'capath' ])) {
curl_setopt ( $curlHandle , CURLOPT_PROXY_CAPATH , $options [ 'ssl' ][ 'capath' ]);
}
}
2018-11-14 16:54:19 +00:00
$progress = array_diff_key ( curl_getinfo ( $curlHandle ), self :: $timeInfo );
2018-09-12 16:58:54 +00:00
2018-11-14 16:54:19 +00:00
$this -> jobs [( int ) $curlHandle ] = array (
'url' => $url ,
'origin' => $origin ,
'attributes' => $attributes ,
'options' => $originalOptions ,
2018-09-12 16:58:54 +00:00
'progress' => $progress ,
2018-11-14 16:54:19 +00:00
'curlHandle' => $curlHandle ,
'filename' => $copyTo ,
'headerHandle' => $headerHandle ,
'bodyHandle' => $bodyHandle ,
2018-09-12 16:58:54 +00:00
'resolve' => $resolve ,
'reject' => $reject ,
);
2020-10-24 08:36:39 +00:00
$usingProxy = $proxy -> getFormattedUrl ( ' using proxy (%s)' );
2020-09-11 21:52:31 +00:00
$ifModified = false !== stripos ( implode ( ',' , $options [ 'http' ][ 'header' ]), 'if-modified-since:' ) ? ' if modified' : '' ;
2018-12-04 14:17:52 +00:00
if ( $attributes [ 'redirects' ] === 0 ) {
2020-01-30 14:50:46 +00:00
$this -> io -> writeError ( 'Downloading ' . Url :: sanitize ( $url ) . $usingProxy . $ifModified , true , IOInterface :: DEBUG );
2018-12-04 14:17:52 +00:00
}
2018-09-12 16:58:54 +00:00
2018-11-14 16:54:19 +00:00
$this -> checkCurlResult ( curl_multi_add_handle ( $this -> multiHandle , $curlHandle ));
2020-06-05 07:06:49 +00:00
// TODO progress
return ( int ) $curlHandle ;
}
public function abortRequest ( $id )
{
2020-09-11 21:51:55 +00:00
if ( isset ( $this -> jobs [ $id ], $this -> jobs [ $id ][ 'handle' ])) {
2020-06-05 07:06:49 +00:00
$job = $this -> jobs [ $id ];
curl_multi_remove_handle ( $this -> multiHandle , $job [ 'handle' ]);
curl_close ( $job [ 'handle' ]);
if ( is_resource ( $job [ 'headerHandle' ])) {
fclose ( $job [ 'headerHandle' ]);
}
if ( is_resource ( $job [ 'bodyHandle' ])) {
fclose ( $job [ 'bodyHandle' ]);
}
if ( $job [ 'filename' ]) {
@ unlink ( $job [ 'filename' ] . '~' );
}
unset ( $this -> jobs [ $id ]);
}
2018-09-12 16:58:54 +00:00
}
public function tick ()
{
if ( ! $this -> jobs ) {
return ;
}
$active = true ;
2018-12-04 16:03:56 +00:00
$this -> checkCurlResult ( curl_multi_exec ( $this -> multiHandle , $active ));
if ( - 1 === curl_multi_select ( $this -> multiHandle , $this -> selectTimeout )) {
// sleep in case select returns -1 as it can happen on old php versions or some platforms where curl does not manage to do the select
usleep ( 150 );
}
2018-09-12 16:58:54 +00:00
2018-12-04 16:03:56 +00:00
while ( $progress = curl_multi_info_read ( $this -> multiHandle )) {
$curlHandle = $progress [ 'handle' ];
2020-11-25 11:37:21 +00:00
$result = $progress [ 'result' ];
2018-12-04 16:03:56 +00:00
$i = ( int ) $curlHandle ;
if ( ! isset ( $this -> jobs [ $i ])) {
continue ;
}
2018-12-05 12:28:37 +00:00
2020-11-25 11:37:21 +00:00
$progress = curl_getinfo ( $curlHandle );
2018-12-04 16:03:56 +00:00
$job = $this -> jobs [ $i ];
unset ( $this -> jobs [ $i ]);
$error = curl_error ( $curlHandle );
$errno = curl_errno ( $curlHandle );
2020-11-25 11:37:21 +00:00
curl_multi_remove_handle ( $this -> multiHandle , $curlHandle );
2018-12-04 16:03:56 +00:00
curl_close ( $curlHandle );
$headers = null ;
$statusCode = null ;
$response = null ;
try {
2020-06-05 07:06:49 +00:00
// TODO progress
2020-11-25 11:37:21 +00:00
if ( CURLE_OK !== $errno || $error || $result !== CURLE_OK ) {
$errno = $errno ? : $result ;
2020-11-18 20:36:33 +00:00
if ( ! $error && function_exists ( 'curl_strerror' )) {
$error = curl_strerror ( $errno );
}
2021-01-26 08:41:02 +00:00
throw new TransportException ( 'curl error ' . $errno . ' while downloading ' . Url :: sanitize ( $progress [ 'url' ]) . ': ' . $error );
2018-12-04 16:03:56 +00:00
}
$statusCode = $progress [ 'http_code' ];
rewind ( $job [ 'headerHandle' ]);
$headers = explode ( " \r \n " , rtrim ( stream_get_contents ( $job [ 'headerHandle' ])));
fclose ( $job [ 'headerHandle' ]);
2020-11-25 11:40:05 +00:00
if ( $statusCode === 0 ) {
throw new \LogicException ( 'Received unexpected http status code 0 without error for ' . Url :: sanitize ( $progress [ 'url' ]) . ': headers ' . var_export ( $headers , true ) . ' curl info ' . var_export ( $progress , true ));
}
2018-12-04 16:03:56 +00:00
// prepare response object
if ( $job [ 'filename' ]) {
2019-02-21 14:28:50 +00:00
$contents = $job [ 'filename' ] . '~' ;
if ( $statusCode >= 300 ) {
rewind ( $job [ 'bodyHandle' ]);
$contents = stream_get_contents ( $job [ 'bodyHandle' ]);
}
2021-01-07 11:01:19 +00:00
$response = new CurlResponse ( array ( 'url' => $progress [ 'url' ]), $statusCode , $headers , $contents , $progress );
2020-01-30 14:50:46 +00:00
$this -> io -> writeError ( '[' . $statusCode . '] ' . Url :: sanitize ( $progress [ 'url' ]), true , IOInterface :: DEBUG );
2018-12-04 16:03:56 +00:00
} else {
rewind ( $job [ 'bodyHandle' ]);
$contents = stream_get_contents ( $job [ 'bodyHandle' ]);
2021-01-07 11:01:19 +00:00
$response = new CurlResponse ( array ( 'url' => $progress [ 'url' ]), $statusCode , $headers , $contents , $progress );
2020-01-30 14:50:46 +00:00
$this -> io -> writeError ( '[' . $statusCode . '] ' . Url :: sanitize ( $progress [ 'url' ]), true , IOInterface :: DEBUG );
2018-12-04 16:03:56 +00:00
}
2019-02-21 14:28:50 +00:00
fclose ( $job [ 'bodyHandle' ]);
2018-11-14 16:54:19 +00:00
2019-02-21 13:49:06 +00:00
if ( $response -> getStatusCode () >= 400 && $response -> getHeader ( 'content-type' ) === 'application/json' ) {
HttpDownloader :: outputWarnings ( $this -> io , $job [ 'origin' ], json_decode ( $response -> getBody (), true ));
}
2021-01-26 08:41:02 +00:00
$result = $this -> isAuthenticatedRetryNeeded ( $job , $response );
2018-12-04 16:03:56 +00:00
if ( $result [ 'retry' ]) {
$this -> restartJob ( $job , $job [ 'url' ], array ( 'storeAuth' => $result [ 'storeAuth' ]));
continue ;
}
2018-11-16 13:28:00 +00:00
2018-12-04 16:03:56 +00:00
// handle 3xx redirects, 304 Not Modified is excluded
if ( $statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job [ 'attributes' ][ 'redirects' ] < $this -> maxRedirects ) {
2021-01-26 08:41:02 +00:00
$location = $this -> handleRedirect ( $job , $response );
2018-12-04 16:03:56 +00:00
if ( $location ) {
$this -> restartJob ( $job , $location , array ( 'redirects' => $job [ 'attributes' ][ 'redirects' ] + 1 ));
2018-11-16 13:28:00 +00:00
continue ;
}
2018-12-04 16:03:56 +00:00
}
2018-11-14 16:54:19 +00:00
2018-12-04 16:03:56 +00:00
// fail 4xx and 5xx responses and capture the response
if ( $statusCode >= 400 && $statusCode <= 599 ) {
2021-01-26 08:41:02 +00:00
throw $this -> failResponse ( $job , $response , $response -> getStatusMessage ());
2018-12-04 16:03:56 +00:00
}
2018-11-14 16:54:19 +00:00
2018-12-04 16:03:56 +00:00
if ( $job [ 'attributes' ][ 'storeAuth' ]) {
$this -> authHelper -> storeAuth ( $job [ 'origin' ], $job [ 'attributes' ][ 'storeAuth' ]);
}
2018-11-14 16:54:19 +00:00
2018-12-04 16:03:56 +00:00
// resolve promise
if ( $job [ 'filename' ]) {
rename ( $job [ 'filename' ] . '~' , $job [ 'filename' ]);
2019-01-17 16:12:33 +00:00
call_user_func ( $job [ 'resolve' ], $response );
2018-12-04 16:03:56 +00:00
} else {
call_user_func ( $job [ 'resolve' ], $response );
}
} catch ( \Exception $e ) {
if ( $e instanceof TransportException && $headers ) {
$e -> setHeaders ( $headers );
$e -> setStatusCode ( $statusCode );
}
if ( $e instanceof TransportException && $response ) {
$e -> setResponse ( $response -> getBody ());
}
2021-01-26 08:41:02 +00:00
if ( $e instanceof TransportException && $progress ) {
$e -> setResponseInfo ( $progress );
}
2018-11-14 16:54:19 +00:00
2018-12-04 16:03:56 +00:00
if ( is_resource ( $job [ 'headerHandle' ])) {
fclose ( $job [ 'headerHandle' ]);
}
if ( is_resource ( $job [ 'bodyHandle' ])) {
fclose ( $job [ 'bodyHandle' ]);
2018-09-12 16:58:54 +00:00
}
2018-12-04 16:03:56 +00:00
if ( $job [ 'filename' ]) {
@ unlink ( $job [ 'filename' ] . '~' );
}
call_user_func ( $job [ 'reject' ], $e );
2018-09-12 16:58:54 +00:00
}
2018-12-04 16:03:56 +00:00
}
2018-09-12 16:58:54 +00:00
2018-12-04 16:03:56 +00:00
foreach ( $this -> jobs as $i => $curlHandle ) {
if ( ! isset ( $this -> jobs [ $i ])) {
continue ;
}
$curlHandle = $this -> jobs [ $i ][ 'curlHandle' ];
$progress = array_diff_key ( curl_getinfo ( $curlHandle ), self :: $timeInfo );
2018-09-12 16:58:54 +00:00
2018-12-04 16:03:56 +00:00
if ( $this -> jobs [ $i ][ 'progress' ] !== $progress ) {
$this -> jobs [ $i ][ 'progress' ] = $progress ;
2018-09-12 16:58:54 +00:00
2020-08-14 14:55:32 +00:00
if ( isset ( $this -> jobs [ $i ][ 'options' ][ 'max_file_size' ])) {
// Compare max_file_size with the content-length header this value will be -1 until the header is parsed
if ( $this -> jobs [ $i ][ 'options' ][ 'max_file_size' ] < $progress [ 'download_content_length' ]) {
throw new MaxFileSizeExceededException ( 'Maximum allowed download size reached. Content-length header indicates ' . $progress [ 'download_content_length' ] . ' bytes. Allowed ' . $this -> jobs [ $i ][ 'options' ][ 'max_file_size' ] . ' bytes' );
}
// Compare max_file_size with the download size in bytes
if ( $this -> jobs [ $i ][ 'options' ][ 'max_file_size' ] < $progress [ 'size_download' ]) {
throw new MaxFileSizeExceededException ( 'Maximum allowed download size reached. Downloaded ' . $progress [ 'size_download' ] . ' of allowed ' . $this -> jobs [ $i ][ 'options' ][ 'max_file_size' ] . ' bytes' );
}
}
2020-09-11 20:51:47 +00:00
// TODO progress
2018-09-12 16:58:54 +00:00
}
}
2018-11-14 16:54:19 +00:00
}
2021-01-26 08:41:02 +00:00
private function handleRedirect ( array $job , Response $response )
2018-11-16 13:28:00 +00:00
{
if ( $locationHeader = $response -> getHeader ( 'location' )) {
if ( parse_url ( $locationHeader , PHP_URL_SCHEME )) {
// Absolute URL; e.g. https://example.com/composer
$targetUrl = $locationHeader ;
} elseif ( parse_url ( $locationHeader , PHP_URL_HOST )) {
// Scheme relative; e.g. //example.com/foo
$targetUrl = parse_url ( $job [ 'url' ], PHP_URL_SCHEME ) . ':' . $locationHeader ;
} elseif ( '/' === $locationHeader [ 0 ]) {
// Absolute path; e.g. /foo
$urlHost = parse_url ( $job [ 'url' ], PHP_URL_HOST );
// Replace path using hostname as an anchor.
$targetUrl = preg_replace ( '{^(.+(?://|@)' . preg_quote ( $urlHost ) . '(?::\d+)?)(?:[/\?].*)?$}' , '\1' . $locationHeader , $job [ 'url' ]);
} else {
// Relative path; e.g. foo
// This actually differs from PHP which seems to add duplicate slashes.
$targetUrl = preg_replace ( '{^(.+/)[^/?]*(?:\?.*)?$}' , '\1' . $locationHeader , $job [ 'url' ]);
}
}
if ( ! empty ( $targetUrl )) {
2020-01-30 14:50:46 +00:00
$this -> io -> writeError ( sprintf ( 'Following redirect (%u) %s' , $job [ 'attributes' ][ 'redirects' ] + 1 , Url :: sanitize ( $targetUrl )), true , IOInterface :: DEBUG );
2018-11-16 13:28:00 +00:00
return $targetUrl ;
}
2021-01-26 08:41:02 +00:00
throw new TransportException ( 'The "' . $job [ 'url' ] . '" file could not be downloaded, got redirect without Location (' . $response -> getStatusMessage () . ')' );
2018-11-16 13:28:00 +00:00
}
2021-01-26 08:41:02 +00:00
private function isAuthenticatedRetryNeeded ( array $job , Response $response )
2018-11-14 16:54:19 +00:00
{
if ( in_array ( $response -> getStatusCode (), array ( 401 , 403 )) && $job [ 'attributes' ][ 'retryAuthFailure' ]) {
2019-02-21 13:49:06 +00:00
$result = $this -> authHelper -> promptAuthIfNeeded ( $job [ 'url' ], $job [ 'origin' ], $response -> getStatusCode (), $response -> getStatusMessage (), $response -> getHeaders ());
2018-11-14 16:54:19 +00:00
if ( $result [ 'retry' ]) {
2018-11-16 13:28:00 +00:00
return $result ;
2018-11-14 16:54:19 +00:00
}
}
2018-09-12 16:58:54 +00:00
2018-11-14 16:54:19 +00:00
$locationHeader = $response -> getHeader ( 'location' );
$needsAuthRetry = false ;
// check for bitbucket login page asking to authenticate
if (
$job [ 'origin' ] === 'bitbucket.org'
&& ! $this -> authHelper -> isPublicBitBucketDownload ( $job [ 'url' ])
&& substr ( $job [ 'url' ], - 4 ) === '.zip'
&& ( ! $locationHeader || substr ( $locationHeader , - 4 ) !== '.zip' )
&& preg_match ( '{^text/html\b}i' , $response -> getHeader ( 'content-type' ))
) {
$needsAuthRetry = 'Bitbucket requires authentication and it was not provided' ;
}
// check for gitlab 404 when downloading archives
if (
$response -> getStatusCode () === 404
&& $this -> config && in_array ( $job [ 'origin' ], $this -> config -> get ( 'gitlab-domains' ), true )
&& false !== strpos ( $job [ 'url' ], 'archive.zip' )
) {
$needsAuthRetry = 'GitLab requires authentication and it was not provided' ;
}
if ( $needsAuthRetry ) {
if ( $job [ 'attributes' ][ 'retryAuthFailure' ]) {
$result = $this -> authHelper -> promptAuthIfNeeded ( $job [ 'url' ], $job [ 'origin' ], 401 );
if ( $result [ 'retry' ]) {
2018-11-16 13:28:00 +00:00
return $result ;
2018-11-14 16:54:19 +00:00
}
}
2021-01-26 08:41:02 +00:00
throw $this -> failResponse ( $job , $response , $needsAuthRetry );
2018-11-14 16:54:19 +00:00
}
2018-11-16 13:28:00 +00:00
return array ( 'retry' => false , 'storeAuth' => false );
}
private function restartJob ( array $job , $url , array $attributes = array ())
{
2019-02-21 14:28:50 +00:00
if ( $job [ 'filename' ]) {
@ unlink ( $job [ 'filename' ] . '~' );
}
2018-11-16 13:28:00 +00:00
$attributes = array_merge ( $job [ 'attributes' ], $attributes );
$origin = Url :: getOrigin ( $this -> config , $url );
2018-12-04 14:17:52 +00:00
$this -> initDownload ( $job [ 'resolve' ], $job [ 'reject' ], $origin , $url , $job [ 'options' ], $job [ 'filename' ], $attributes );
2018-11-14 16:54:19 +00:00
}
2021-01-26 08:41:02 +00:00
private function failResponse ( array $job , Response $response , $errorMessage )
2018-11-14 16:54:19 +00:00
{
2019-02-21 14:28:50 +00:00
if ( $job [ 'filename' ]) {
@ unlink ( $job [ 'filename' ] . '~' );
}
2021-01-17 13:13:58 +00:00
$details = '' ;
if ( $response -> getHeader ( 'content-type' ) === 'application/json' ) {
$details = ':' . PHP_EOL . substr ( $response -> getBody (), 0 , 200 ) . ( strlen ( $response -> getBody ()) > 200 ? '...' : '' );
}
2021-01-26 08:41:02 +00:00
return new TransportException ( 'The "' . $job [ 'url' ] . '" file could not be downloaded (' . $errorMessage . ')' . $details , $response -> getStatusCode ());
2018-09-12 16:58:54 +00:00
}
private function checkCurlResult ( $code )
{
if ( $code != CURLM_OK && $code != CURLM_CALL_MULTI_PERFORM ) {
2020-11-22 13:48:56 +00:00
throw new \RuntimeException (
isset ( $this -> multiErrors [ $code ])
2018-09-12 16:58:54 +00:00
? " cURL error: { $code } ( { $this -> multiErrors [ $code ][ 0 ] } ): cURL message: { $this -> multiErrors [ $code ][ 1 ] } "
: 'Unexpected cURL error: ' . $code
);
}
}
}