2012-02-14 10:25:00 +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 ;
2012-03-18 20:05:10 +00:00
use Composer\Composer ;
2014-04-11 12:27:14 +00:00
use Composer\Config ;
2012-02-14 10:25:00 +00:00
use Composer\IO\IOInterface ;
2012-03-08 20:59:02 +00:00
use Composer\Downloader\TransportException ;
2012-02-14 10:25:00 +00:00
/**
* @ author François Pluchino < francois . pluchino @ opendisplay . com >
2013-04-26 21:23:35 +00:00
* @ author Jordi Boggiano < j . boggiano @ seld . be >
2013-08-15 15:14:48 +00:00
* @ author Nils Adermann < naderman @ naderman . de >
2012-02-14 10:25:00 +00:00
*/
class RemoteFilesystem
{
2012-02-15 12:11:29 +00:00
private $io ;
2014-04-11 12:27:14 +00:00
private $config ;
2012-02-15 12:11:29 +00:00
private $firstCall ;
2012-02-14 10:25:00 +00:00
private $bytesMax ;
2012-02-17 10:50:36 +00:00
private $originUrl ;
private $fileUrl ;
2012-02-14 10:25:00 +00:00
private $fileName ;
2013-04-30 10:14:27 +00:00
private $retry ;
2012-03-05 10:28:23 +00:00
private $progress ;
2012-02-17 10:53:38 +00:00
private $lastProgress ;
2012-10-03 09:56:31 +00:00
private $options ;
2014-01-17 15:32:55 +00:00
private $retryAuthFailure ;
2014-04-24 14:41:42 +00:00
private $lastHeaders ;
2012-02-14 10:25:00 +00:00
/**
* Constructor .
*
2012-10-24 23:44:40 +00:00
* @ param IOInterface $io The IO instance
* @ param array $options The options
2012-02-14 10:25:00 +00:00
*/
2014-04-11 12:27:14 +00:00
public function __construct ( IOInterface $io , Config $config = null , array $options = array ())
2012-02-14 10:25:00 +00:00
{
$this -> io = $io ;
2014-04-11 12:27:14 +00:00
$this -> config = $config ;
2012-10-03 09:56:31 +00:00
$this -> options = $options ;
2012-02-14 10:25:00 +00:00
}
/**
* Copy the remote file in local .
*
2012-07-13 20:16:17 +00:00
* @ param string $originUrl The origin URL
2012-02-15 12:11:29 +00:00
* @ param string $fileUrl The file URL
* @ param string $fileName the local filename
2012-03-05 10:28:23 +00:00
* @ param boolean $progress Display the progression
2012-10-18 14:02:24 +00:00
* @ param array $options Additional context options
2012-02-17 11:35:42 +00:00
*
2012-06-23 09:58:18 +00:00
* @ return bool true
2012-02-15 12:11:29 +00:00
*/
2012-10-18 14:02:24 +00:00
public function copy ( $originUrl , $fileUrl , $fileName , $progress = true , $options = array ())
2012-02-15 12:11:29 +00:00
{
2013-04-30 10:14:27 +00:00
return $this -> get ( $originUrl , $fileUrl , $options , $fileName , $progress );
2012-02-15 12:11:29 +00:00
}
/**
* Get the content .
*
2012-07-13 20:16:17 +00:00
* @ param string $originUrl The origin URL
2012-02-15 12:11:29 +00:00
* @ param string $fileUrl The file URL
2012-03-05 10:28:23 +00:00
* @ param boolean $progress Display the progression
2012-10-18 14:02:24 +00:00
* @ param array $options Additional context options
2012-02-15 12:11:29 +00:00
*
2012-02-16 22:41:26 +00:00
* @ return string The content
2012-02-15 12:11:29 +00:00
*/
2012-10-18 14:02:24 +00:00
public function getContents ( $originUrl , $fileUrl , $progress = true , $options = array ())
2012-02-15 12:11:29 +00:00
{
2013-04-30 10:14:27 +00:00
return $this -> get ( $originUrl , $fileUrl , $options , null , $progress );
2012-02-15 12:11:29 +00:00
}
2013-08-14 16:47:25 +00:00
/**
* Retrieve the options set in the constructor
*
* @ return array Options
*/
public function getOptions ()
{
return $this -> options ;
}
2014-04-24 14:41:42 +00:00
/**
* Returns the headers of the last request
*
* @ return array
*/
public function getLastHeaders ()
{
return $this -> lastHeaders ;
}
2012-02-15 12:11:29 +00:00
/**
* Get file content or copy action .
*
2012-10-18 14:02:24 +00:00
* @ param string $originUrl The origin URL
* @ param string $fileUrl The file URL
* @ param array $additionalOptions context options
* @ param string $fileName the local filename
* @ param boolean $progress Display the progression
2012-02-14 10:25:00 +00:00
*
2013-06-13 00:05:44 +00:00
* @ throws TransportException | \Exception
2013-06-13 11:28:24 +00:00
* @ throws TransportException When the file could not be downloaded
2013-06-13 00:05:44 +00:00
*
* @ return bool | string
2012-02-14 10:25:00 +00:00
*/
2012-10-18 14:02:24 +00:00
protected function get ( $originUrl , $fileUrl , $additionalOptions = array (), $fileName = null , $progress = true )
2012-02-14 10:25:00 +00:00
{
2014-05-07 16:38:58 +00:00
if ( strpos ( $originUrl , '.github.com' ) === ( strlen ( $originUrl ) - 11 )) {
$originUrl = 'github.com' ;
}
2012-02-14 10:25:00 +00:00
$this -> bytesMax = 0 ;
2012-02-17 10:50:36 +00:00
$this -> originUrl = $originUrl ;
$this -> fileUrl = $fileUrl ;
2012-02-15 12:11:29 +00:00
$this -> fileName = $fileName ;
2012-03-05 10:28:23 +00:00
$this -> progress = $progress ;
2012-02-17 10:53:38 +00:00
$this -> lastProgress = null ;
2014-01-17 15:32:55 +00:00
$this -> retryAuthFailure = true ;
2014-04-24 14:41:42 +00:00
$this -> lastHeaders = array ();
2012-02-17 10:50:36 +00:00
2013-06-17 13:41:48 +00:00
// capture username/password from URL if there is one
if ( preg_match ( '{^https?://(.+):(.+)@([^/]+)}i' , $fileUrl , $match )) {
$this -> io -> setAuthentication ( $originUrl , urldecode ( $match [ 1 ]), urldecode ( $match [ 2 ]));
}
2014-01-17 15:32:55 +00:00
if ( isset ( $additionalOptions [ 'retry-auth-failure' ])) {
$this -> retryAuthFailure = ( bool ) $additionalOptions [ 'retry-auth-failure' ];
unset ( $additionalOptions [ 'retry-auth-failure' ]);
}
2012-10-18 14:02:24 +00:00
$options = $this -> getOptionsForUrl ( $originUrl , $additionalOptions );
2013-06-17 13:41:48 +00:00
2013-04-26 21:23:35 +00:00
if ( $this -> io -> isDebug ()) {
2013-08-10 12:22:11 +00:00
$this -> io -> write (( substr ( $fileUrl , 0 , 4 ) === 'http' ? 'Downloading ' : 'Reading ' ) . $fileUrl );
2013-04-26 21:23:35 +00:00
}
2013-02-27 11:34:18 +00:00
if ( isset ( $options [ 'github-token' ])) {
$fileUrl .= ( false === strpos ( $fileUrl , '?' ) ? '?' : '&' ) . 'access_token=' . $options [ 'github-token' ];
unset ( $options [ 'github-token' ]);
}
2014-02-26 14:51:06 +00:00
if ( isset ( $options [ 'http' ])) {
$options [ 'http' ][ 'ignore_errors' ] = true ;
}
2013-05-30 12:58:26 +00:00
$ctx = StreamContextFactory :: getContext ( $fileUrl , $options , array ( 'notification' => array ( $this , 'callbackGet' )));
2012-02-17 10:50:36 +00:00
2012-02-15 12:11:29 +00:00
if ( $this -> progress ) {
2012-03-06 18:08:15 +00:00
$this -> io -> write ( " Downloading: <comment>connection...</comment> " , false );
2012-02-15 12:11:29 +00:00
}
2012-10-18 08:30:32 +00:00
$errorMessage = '' ;
2012-10-18 15:09:23 +00:00
$errorCode = 0 ;
2013-04-30 20:39:08 +00:00
$result = false ;
2012-08-17 14:51:58 +00:00
set_error_handler ( function ( $code , $msg ) use ( & $errorMessage ) {
2012-10-18 08:30:32 +00:00
if ( $errorMessage ) {
$errorMessage .= " \n " ;
2012-08-17 14:51:58 +00:00
}
2012-10-18 08:30:32 +00:00
$errorMessage .= preg_replace ( '{^file_get_contents\(.*?\): }' , '' , $msg );
2012-08-17 14:51:58 +00:00
});
2012-10-18 14:02:24 +00:00
try {
$result = file_get_contents ( $fileUrl , false , $ctx );
} catch ( \Exception $e ) {
if ( $e instanceof TransportException && ! empty ( $http_response_header [ 0 ])) {
$e -> setHeaders ( $http_response_header );
}
2014-02-26 14:51:06 +00:00
if ( $e instanceof TransportException && $result !== false ) {
$e -> setResponse ( $result );
}
$result = false ;
2012-10-18 14:02:24 +00:00
}
2012-10-18 08:30:32 +00:00
if ( $errorMessage && ! ini_get ( 'allow_url_fopen' )) {
$errorMessage = 'allow_url_fopen must be enabled in php.ini (' . $errorMessage . ')' ;
}
2012-08-17 14:51:58 +00:00
restore_error_handler ();
2013-04-30 10:14:27 +00:00
if ( isset ( $e ) && ! $this -> retry ) {
2012-10-18 14:02:24 +00:00
throw $e ;
}
2012-02-17 11:35:42 +00:00
2014-02-26 14:51:06 +00:00
// fail 4xx and 5xx responses and capture the response
2012-10-18 15:09:23 +00:00
if ( ! empty ( $http_response_header [ 0 ]) && preg_match ( '{^HTTP/\S+ ([45]\d\d)}i' , $http_response_header [ 0 ], $match )) {
$errorCode = $match [ 1 ];
2014-02-26 14:51:06 +00:00
if ( ! $this -> retry ) {
$e = new TransportException ( 'The "' . $this -> fileUrl . '" file could not be downloaded (' . $http_response_header [ 0 ] . ')' , $errorCode );
$e -> setHeaders ( $http_response_header );
$e -> setResponse ( $result );
throw $e ;
}
$result = false ;
2012-03-09 22:44:10 +00:00
}
2012-03-18 20:05:10 +00:00
// decode gzip
2013-04-30 08:06:39 +00:00
if ( $result && extension_loaded ( 'zlib' ) && substr ( $fileUrl , 0 , 4 ) === 'http' ) {
2012-03-18 21:12:48 +00:00
$decode = false ;
2012-03-18 20:05:10 +00:00
foreach ( $http_response_header as $header ) {
if ( preg_match ( '{^content-encoding: *gzip *$}i' , $header )) {
2012-03-18 21:12:48 +00:00
$decode = true ;
continue ;
} elseif ( preg_match ( '{^HTTP/}i' , $header )) {
$decode = false ;
}
}
if ( $decode ) {
if ( version_compare ( PHP_VERSION , '5.4.0' , '>=' )) {
$result = zlib_decode ( $result );
} else {
// work around issue with gzuncompress & co that do not work with all gzip checksums
$result = file_get_contents ( 'compress.zlib://data:application/octet-stream;base64,' . base64_encode ( $result ));
2012-03-18 20:05:10 +00:00
}
}
}
2014-05-07 16:45:34 +00:00
if ( $this -> progress && ! $this -> retry ) {
2012-04-10 07:43:47 +00:00
$this -> io -> overwrite ( " Downloading: <comment>100%</comment> " );
}
2012-03-18 20:05:10 +00:00
// handle copy command if download was successful
if ( false !== $result && null !== $fileName ) {
2013-04-29 15:15:55 +00:00
if ( '' === $result ) {
throw new TransportException ( '"' . $this -> fileUrl . '" appears broken, and returned an empty 200 response' );
}
2012-10-18 08:30:32 +00:00
$errorMessage = '' ;
set_error_handler ( function ( $code , $msg ) use ( & $errorMessage ) {
if ( $errorMessage ) {
$errorMessage .= " \n " ;
}
$errorMessage .= preg_replace ( '{^file_put_contents\(.*?\): }' , '' , $msg );
});
$result = ( bool ) file_put_contents ( $fileName , $result );
restore_error_handler ();
2012-04-10 07:43:47 +00:00
if ( false === $result ) {
2013-03-23 18:43:08 +00:00
throw new TransportException ( 'The "' . $this -> fileUrl . '" file could not be written to ' . $fileName . ': ' . $errorMessage );
2012-04-10 07:43:47 +00:00
}
2012-03-18 20:05:10 +00:00
}
2013-04-30 10:14:27 +00:00
if ( $this -> retry ) {
$this -> retry = false ;
2013-04-30 20:39:08 +00:00
return $this -> get ( $this -> originUrl , $this -> fileUrl , $additionalOptions , $this -> fileName , $this -> progress );
2012-02-17 10:50:36 +00:00
}
2013-04-30 10:14:27 +00:00
if ( false === $result ) {
2013-03-23 18:43:08 +00:00
$e = new TransportException ( 'The "' . $this -> fileUrl . '" file could not be downloaded: ' . $errorMessage , $errorCode );
2012-10-18 15:09:23 +00:00
if ( ! empty ( $http_response_header [ 0 ])) {
$e -> setHeaders ( $http_response_header );
}
throw $e ;
2012-02-16 22:41:26 +00:00
}
2013-04-30 10:14:27 +00:00
2014-04-24 14:41:42 +00:00
if ( ! empty ( $http_response_header [ 0 ])) {
$this -> lastHeaders = $http_response_header ;
}
2013-04-30 10:14:27 +00:00
return $result ;
2012-02-14 10:25:00 +00:00
}
/**
* Get notification action .
*
2013-06-13 11:28:24 +00:00
* @ param integer $notificationCode The notification code
* @ param integer $severity The severity level
* @ param string $message The message
* @ param integer $messageCode The message code
* @ param integer $bytesTransferred The loaded size
* @ param integer $bytesMax The total size
2013-06-13 00:05:44 +00:00
* @ throws TransportException
2012-02-14 10:25:00 +00:00
*/
protected function callbackGet ( $notificationCode , $severity , $message , $messageCode , $bytesTransferred , $bytesMax )
{
switch ( $notificationCode ) {
case STREAM_NOTIFY_FAILURE :
2012-03-10 07:49:21 +00:00
case STREAM_NOTIFY_AUTH_REQUIRED :
if ( 401 === $messageCode ) {
2014-02-26 16:19:54 +00:00
// Bail if the caller is going to handle authentication failures itself.
if ( ! $this -> retryAuthFailure ) {
break ;
}
2014-04-11 12:27:14 +00:00
$this -> promptAuthAndRetry ( $messageCode );
2013-03-05 12:34:48 +00:00
break ;
2012-02-14 10:25:00 +00:00
}
2014-02-26 14:51:06 +00:00
break ;
2012-02-14 10:25:00 +00:00
2012-10-16 12:16:39 +00:00
case STREAM_NOTIFY_AUTH_RESULT :
if ( 403 === $messageCode ) {
2014-04-11 12:27:14 +00:00
$this -> promptAuthAndRetry ( $messageCode , $message );
2013-12-31 14:31:03 +00:00
break ;
2012-10-16 12:16:39 +00:00
}
break ;
2012-02-14 10:25:00 +00:00
case STREAM_NOTIFY_FILE_SIZE_IS :
if ( $this -> bytesMax < $bytesMax ) {
$this -> bytesMax = $bytesMax ;
}
break ;
case STREAM_NOTIFY_PROGRESS :
2012-02-15 12:11:29 +00:00
if ( $this -> bytesMax > 0 && $this -> progress ) {
2012-02-14 10:25:00 +00:00
$progression = 0 ;
if ( $this -> bytesMax > 0 ) {
$progression = round ( $bytesTransferred / $this -> bytesMax * 100 );
}
2012-02-17 10:53:38 +00:00
if (( 0 === $progression % 5 ) && $progression !== $this -> lastProgress ) {
$this -> lastProgress = $progression ;
2012-02-14 10:25:00 +00:00
$this -> io -> overwrite ( " Downloading: <comment> $progression %</comment> " , false );
}
}
break ;
default :
break ;
}
}
2012-03-05 10:28:23 +00:00
2014-04-20 17:34:54 +00:00
protected function promptAuthAndRetry ( $httpStatus , $reason = null )
2013-12-31 14:31:03 +00:00
{
2014-04-11 12:27:14 +00:00
if ( $this -> config && in_array ( $this -> originUrl , $this -> config -> get ( 'github-domains' ), true )) {
$message = " \n " . 'Could not fetch ' . $this -> fileUrl . ', enter your GitHub credentials ' . ( $httpStatus === 404 ? 'to access private repos' : 'to go over the API rate limit' );
$gitHubUtil = new GitHub ( $this -> io , $this -> config , null , $this );
if ( ! $gitHubUtil -> authorizeOAuth ( $this -> originUrl )
&& ( ! $this -> io -> isInteractive () || ! $gitHubUtil -> authorizeOAuthInteractively ( $this -> originUrl , $message ))
) {
2014-05-22 07:44:01 +00:00
throw new TransportException ( 'Could not authenticate against ' . $this -> originUrl , 401 );
2014-04-11 12:27:14 +00:00
}
} else {
// 404s are only handled for github
if ( $httpStatus === 404 ) {
return ;
}
2014-04-11 13:01:20 +00:00
// fail if the console is not interactive
if ( ! $this -> io -> isInteractive ()) {
2014-04-11 12:27:14 +00:00
if ( $httpStatus === 401 ) {
$message = " The ' " . $this -> fileUrl . " ' URL required authentication. \n You must be using the interactive console to authenticate " ;
}
if ( $httpStatus === 403 ) {
$message = " The ' " . $this -> fileUrl . " ' URL could not be accessed: " . $reason ;
}
throw new TransportException ( $message , $httpStatus );
}
2014-04-11 13:01:20 +00:00
// fail if we already have auth
if ( $this -> io -> hasAuthentication ( $this -> originUrl )) {
throw new TransportException ( " Invalid credentials for ' " . $this -> fileUrl . " ', aborting. " , $httpStatus );
}
2014-04-11 12:27:14 +00:00
$this -> io -> overwrite ( ' Authentication required (<info>' . parse_url ( $this -> fileUrl , PHP_URL_HOST ) . '</info>):' );
$username = $this -> io -> ask ( ' Username: ' );
$password = $this -> io -> askAndHideAnswer ( ' Password: ' );
$this -> io -> setAuthentication ( $this -> originUrl , $username , $password );
}
2013-12-31 14:31:03 +00:00
$this -> retry = true ;
throw new TransportException ( 'RETRY' );
}
2012-10-18 14:02:24 +00:00
protected function getOptionsForUrl ( $originUrl , $additionalOptions )
2012-03-05 10:28:23 +00:00
{
2012-10-19 09:02:18 +00:00
$headers = array (
sprintf (
'User-Agent: Composer/%s (%s; %s; PHP %s.%s.%s)' ,
Composer :: VERSION === '@package_version@' ? 'source' : Composer :: VERSION ,
php_uname ( 's' ),
php_uname ( 'r' ),
PHP_MAJOR_VERSION ,
PHP_MINOR_VERSION ,
PHP_RELEASE_VERSION
)
2012-06-01 12:05:24 +00:00
);
2012-10-19 09:02:18 +00:00
2012-03-18 20:05:10 +00:00
if ( extension_loaded ( 'zlib' )) {
2012-10-19 09:02:18 +00:00
$headers [] = 'Accept-Encoding: gzip' ;
2012-03-18 20:05:10 +00:00
}
2013-02-27 12:23:59 +00:00
$options = array_replace_recursive ( $this -> options , $additionalOptions );
2012-11-07 12:33:50 +00:00
if ( $this -> io -> hasAuthentication ( $originUrl )) {
$auth = $this -> io -> getAuthentication ( $originUrl );
2013-02-27 11:34:18 +00:00
if ( 'github.com' === $originUrl && 'x-oauth-basic' === $auth [ 'password' ]) {
$options [ 'github-token' ] = $auth [ 'username' ];
} else {
$authStr = base64_encode ( $auth [ 'username' ] . ':' . $auth [ 'password' ]);
$headers [] = 'Authorization: Basic ' . $authStr ;
}
2012-03-05 10:28:23 +00:00
}
2012-10-19 09:02:18 +00:00
if ( isset ( $options [ 'http' ][ 'header' ]) && ! is_array ( $options [ 'http' ][ 'header' ])) {
$options [ 'http' ][ 'header' ] = explode ( " \r \n " , trim ( $options [ 'http' ][ 'header' ], " \r \n " ));
}
foreach ( $headers as $header ) {
$options [ 'http' ][ 'header' ][] = $header ;
2012-10-18 14:02:24 +00:00
}
2012-10-03 09:56:31 +00:00
2012-03-05 10:28:23 +00:00
return $options ;
}
}