2011-05-06 17:55:49 +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\Downloader ;
2016-02-25 13:05:26 +00:00
use Composer\Package\PackageInterface ;
2016-11-19 20:50:15 +00:00
use Composer\Util\IniHelper ;
2016-02-03 21:39:16 +00:00
use Composer\Util\Platform ;
2012-01-18 15:37:55 +00:00
use Composer\Util\ProcessExecutor ;
2016-02-25 13:05:26 +00:00
use Symfony\Component\Process\ExecutableFinder ;
2021-03-09 14:49:40 +00:00
use React\Promise\PromiseInterface ;
2012-03-29 13:08:47 +00:00
use ZipArchive ;
2011-05-06 17:55:49 +00:00
/**
* @ author Jordi Boggiano < j . boggiano @ seld . be >
*/
2012-02-17 22:10:02 +00:00
class ZipDownloader extends ArchiveDownloader
2011-05-06 17:55:49 +00:00
{
2021-05-11 11:23:18 +00:00
private static $unzipCommands ;
2021-06-02 13:00:31 +00:00
private static $hasZipArchive ;
2017-03-30 07:24:48 +00:00
private static $isWindows ;
2019-01-07 15:22:41 +00:00
/** @var ZipArchive|null */
2017-03-14 22:43:48 +00:00
private $zipArchiveObject ;
2012-01-18 15:37:55 +00:00
2016-02-25 13:05:26 +00:00
/**
* { @ inheritDoc }
*/
2019-08-29 09:37:23 +00:00
public function download ( PackageInterface $package , $path , PackageInterface $prevPackage = null , $output = true )
2016-02-25 13:05:26 +00:00
{
2021-05-11 11:23:18 +00:00
if ( null === self :: $unzipCommands ) {
self :: $unzipCommands = array ();
2016-02-25 13:05:26 +00:00
$finder = new ExecutableFinder ;
2021-05-11 11:23:18 +00:00
if ( Platform :: isWindows () && ( $cmd = $finder -> find ( '7z' , null , array ( 'C:\Program Files\7-Zip' )))) {
2021-06-02 13:00:31 +00:00
self :: $unzipCommands [] = array ( '7z' , ProcessExecutor :: escape ( $cmd ) . ' x -bb0 -y %s -o%s' );
2021-05-11 11:23:18 +00:00
}
if ( $cmd = $finder -> find ( 'unzip' )) {
2021-06-02 13:00:31 +00:00
self :: $unzipCommands [] = array ( 'unzip' , ProcessExecutor :: escape ( $cmd ) . ' -qq %s -d %s' );
2021-05-11 11:23:18 +00:00
}
2021-07-12 12:49:44 +00:00
if ( ! Platform :: isWindows () && ( $cmd = $finder -> find ( '7z' ))) { // 7z linux/macOS support is only used if unzip is not present
self :: $unzipCommands [] = array ( '7z' , ProcessExecutor :: escape ( $cmd ) . ' x -bb0 -y %s -o%s' );
}
2021-07-12 13:50:02 +00:00
if ( ! Platform :: isWindows () && ( $cmd = $finder -> find ( '7zz' ))) { // 7zz linux/macOS support is only used if unzip is not present
self :: $unzipCommands [] = array ( '7zz' , ProcessExecutor :: escape ( $cmd ) . ' x -bb0 -y %s -o%s' );
}
2016-02-25 13:05:26 +00:00
}
2021-08-18 08:15:47 +00:00
$procOpenMissing = false ;
if ( ! function_exists ( 'proc_open' )) {
self :: $unzipCommands = array ();
$procOpenMissing = true ;
}
2017-02-13 14:54:55 +00:00
if ( null === self :: $hasZipArchive ) {
self :: $hasZipArchive = class_exists ( 'ZipArchive' );
}
2021-05-11 11:23:18 +00:00
if ( ! self :: $hasZipArchive && ! self :: $unzipCommands ) {
2018-06-07 09:08:50 +00:00
// php.ini path is added to the error message to help users find the correct file
$iniMessage = IniHelper :: getMessage ();
2021-08-18 08:15:47 +00:00
if ( $procOpenMissing ) {
$error = " The zip extension is missing and unzip/7z commands cannot be called as proc_open is disabled, skipping. \n " . $iniMessage ;
} else {
$error = " The zip extension and unzip/7z commands are both missing, skipping. \n " . $iniMessage ;
}
2018-06-07 09:08:50 +00:00
throw new \RuntimeException ( $error );
}
2017-03-14 22:43:48 +00:00
if ( null === self :: $isWindows ) {
self :: $isWindows = Platform :: isWindows ();
2018-06-01 10:47:22 +00:00
2021-05-11 11:23:18 +00:00
if ( ! self :: $isWindows && ! self :: $unzipCommands ) {
2021-08-18 08:15:47 +00:00
if ( $procOpenMissing ) {
$this -> io -> writeError ( " <warning>proc_open is disabled so 'unzip' and '7z' commands cannot be used, zip files are being unpacked using the PHP zip extension.</warning> " );
$this -> io -> writeError ( " <warning>This may cause invalid reports of corrupted archives. Besides, any UNIX permissions (e.g. executable) defined in the archives will be lost.</warning> " );
$this -> io -> writeError ( " <warning>Enabling proc_open and installing 'unzip' or '7z' may remediate them.</warning> " );
} else {
$this -> io -> writeError ( " <warning>As there is no 'unzip' nor '7z' command installed zip files are being unpacked using the PHP zip extension.</warning> " );
$this -> io -> writeError ( " <warning>This may cause invalid reports of corrupted archives. Besides, any UNIX permissions (e.g. executable) defined in the archives will be lost.</warning> " );
$this -> io -> writeError ( " <warning>Installing 'unzip' or '7z' may remediate them.</warning> " );
}
2018-06-01 10:47:22 +00:00
}
2017-03-14 22:43:48 +00:00
}
2019-08-29 09:37:23 +00:00
return parent :: download ( $package , $path , $prevPackage , $output );
2016-02-25 13:05:26 +00:00
}
2017-02-13 13:00:48 +00:00
/**
* extract $file to $path with " unzip " command
2017-02-13 12:43:36 +00:00
*
2021-06-03 08:38:38 +00:00
* @ param string $file File to extract
* @ param string $path Path where to extract file
2021-03-09 14:49:40 +00:00
* @ return PromiseInterface
2017-02-13 12:43:36 +00:00
*/
2021-06-02 13:00:31 +00:00
private function extractWithSystemUnzip ( PackageInterface $package , $file , $path )
2011-05-06 17:55:49 +00:00
{
2021-06-02 13:00:31 +00:00
// Force Exception throwing if the other alternative extraction method is not available
$isLastChance = ! self :: $hasZipArchive ;
2017-03-14 22:43:48 +00:00
2021-06-02 13:00:31 +00:00
if ( ! self :: $unzipCommands ) {
2017-03-14 22:43:48 +00:00
// This was call as the favorite extract way, but is not available
// We switch to the alternative
2021-06-02 13:00:31 +00:00
return $this -> extractWithZipArchive ( $package , $file , $path );
2017-03-14 22:43:48 +00:00
}
2021-06-02 13:00:31 +00:00
$commandSpec = reset ( self :: $unzipCommands );
$command = sprintf ( $commandSpec [ 1 ], ProcessExecutor :: escape ( $file ), ProcessExecutor :: escape ( $path ));
2021-08-18 09:55:51 +00:00
// normalize separators to backslashes to avoid problems with 7-zip on windows
// see https://github.com/composer/composer/issues/10058
if ( Platform :: isWindows ()) {
$command = sprintf ( $commandSpec [ 1 ], ProcessExecutor :: escape ( strtr ( $file , '/' , '\\' )), ProcessExecutor :: escape ( strtr ( $path , '/' , '\\' )));
}
2021-06-02 13:00:31 +00:00
$executable = $commandSpec [ 0 ];
2020-06-05 07:07:40 +00:00
2021-06-02 13:00:31 +00:00
$self = $this ;
$io = $this -> io ;
$tryFallback = function ( $processError ) use ( $isLastChance , $io , $self , $file , $path , $package , $executable ) {
if ( $isLastChance ) {
throw $processError ;
2020-06-05 07:07:40 +00:00
}
2021-06-02 13:00:31 +00:00
if ( ! is_file ( $file )) {
$io -> writeError ( ' <warning>' . $processError -> getMessage () . '</warning>' );
$io -> writeError ( ' <warning>This most likely is due to a custom installer plugin not handling the returned Promise from the downloader</warning>' );
$io -> writeError ( ' <warning>See https://github.com/composer/installers/commit/5006d0c28730ade233a8f42ec31ac68fb1c5c9bb for an example fix</warning>' );
} else {
$io -> writeError ( ' <warning>' . $processError -> getMessage () . '</warning>' );
$io -> writeError ( ' The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems)' );
$io -> writeError ( ' Unzip with ' . $executable . ' command failed, falling back to ZipArchive class' );
2014-05-20 08:15:44 +00:00
}
2013-01-31 09:57:59 +00:00
2021-06-02 13:00:31 +00:00
return $self -> extractWithZipArchive ( $package , $file , $path );
};
2017-03-14 22:43:48 +00:00
2021-06-02 13:00:31 +00:00
try {
$promise = $this -> process -> executeAsync ( $command );
2017-03-30 07:24:48 +00:00
2021-06-02 13:00:31 +00:00
return $promise -> then ( function ( $process ) use ( $tryFallback , $command , $package , $file ) {
if ( ! $process -> isSuccessful ()) {
$output = $process -> getErrorOutput ();
$output = str_replace ( ', ' . $file . '.zip or ' . $file . '.ZIP' , '' , $output );
2017-03-30 07:24:48 +00:00
2021-06-02 13:00:31 +00:00
return $tryFallback ( new \RuntimeException ( 'Failed to extract ' . $package -> getName () . ': (' . $process -> getExitCode () . ') ' . $command . " \n \n " . $output ));
}
});
} catch ( \Exception $e ) {
return $tryFallback ( $e );
} catch ( \Throwable $e ) {
return $tryFallback ( $e );
}
2017-02-13 13:00:48 +00:00
}
/**
* extract $file to $path with ZipArchive
*
2021-06-03 08:38:38 +00:00
* @ param string $file File to extract
* @ param string $path Path where to extract file
2021-03-09 14:49:40 +00:00
* @ return PromiseInterface
2020-06-05 07:07:40 +00:00
*
* TODO v3 should make this private once we can drop PHP 5.3 support
* @ protected
2017-02-13 13:00:48 +00:00
*/
2021-06-02 13:00:31 +00:00
public function extractWithZipArchive ( PackageInterface $package , $file , $path )
2017-02-13 13:00:48 +00:00
{
2017-03-14 22:43:48 +00:00
$processError = null ;
$zipArchive = $this -> zipArchiveObject ? : new ZipArchive ();
2017-02-13 14:54:55 +00:00
try {
2017-03-14 22:43:48 +00:00
if ( true === ( $retval = $zipArchive -> open ( $file ))) {
$extractResult = $zipArchive -> extractTo ( $path );
if ( true === $extractResult ) {
$zipArchive -> close ();
2017-03-30 07:24:48 +00:00
2020-06-05 07:07:40 +00:00
return \React\Promise\resolve ();
2017-03-14 22:43:48 +00:00
}
2017-03-30 07:24:48 +00:00
$processError = new \RuntimeException ( rtrim ( " There was an error extracting the ZIP file, it is either corrupted or using an invalid format. \n " ));
2017-03-14 22:43:48 +00:00
} else {
$processError = new \UnexpectedValueException ( rtrim ( $this -> getErrorMessage ( $retval , $file ) . " \n " ), $retval );
}
2017-05-16 20:44:25 +00:00
} catch ( \ErrorException $e ) {
$processError = new \RuntimeException ( 'The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems): ' . $e -> getMessage (), 0 , $e );
2017-03-14 22:43:48 +00:00
} catch ( \Exception $e ) {
$processError = $e ;
2020-06-05 07:07:40 +00:00
} catch ( \Throwable $e ) {
$processError = $e ;
2011-05-06 17:55:49 +00:00
}
2011-09-28 22:48:17 +00:00
2021-06-02 13:00:31 +00:00
throw $processError ;
2017-02-13 13:00:48 +00:00
}
/**
* extract $file to $path
*
2021-03-09 14:49:40 +00:00
* @ param string $file File to extract
* @ param string $path Path where to extract file
* @ return PromiseInterface | null
2020-09-21 11:19:30 +00:00
*
* TODO v3 should make this private once we can drop PHP 5.3 support
* @ protected
2017-02-13 13:00:48 +00:00
*/
2019-01-17 16:12:33 +00:00
public function extract ( PackageInterface $package , $file , $path )
2017-02-13 13:00:48 +00:00
{
2021-06-02 13:00:31 +00:00
return $this -> extractWithSystemUnzip ( $package , $file , $path );
2011-05-06 17:55:49 +00:00
}
2012-03-29 12:19:41 +00:00
2012-03-29 12:22:26 +00:00
/**
2012-03-29 13:08:47 +00:00
* Give a meaningful error message to the user .
2012-03-29 12:22:26 +00:00
*
2012-05-22 10:07:08 +00:00
* @ param int $retval
* @ param string $file
2012-03-29 13:08:47 +00:00
* @ return string
2012-03-29 12:22:26 +00:00
*/
2012-03-29 13:08:47 +00:00
protected function getErrorMessage ( $retval , $file )
2012-03-29 12:19:41 +00:00
{
switch ( $retval ) {
2012-03-29 13:08:47 +00:00
case ZipArchive :: ER_EXISTS :
return sprintf ( " File '%s' already exists. " , $file );
case ZipArchive :: ER_INCONS :
return sprintf ( " Zip archive '%s' is inconsistent. " , $file );
case ZipArchive :: ER_INVAL :
return sprintf ( " Invalid argument (%s) " , $file );
case ZipArchive :: ER_MEMORY :
return sprintf ( " Malloc failure (%s) " , $file );
case ZipArchive :: ER_NOENT :
return sprintf ( " No such zip file: '%s' " , $file );
case ZipArchive :: ER_NOZIP :
return sprintf ( " '%s' is not a zip archive. " , $file );
case ZipArchive :: ER_OPEN :
return sprintf ( " Can't open zip file: %s " , $file );
case ZipArchive :: ER_READ :
return sprintf ( " Zip read error (%s) " , $file );
case ZipArchive :: ER_SEEK :
return sprintf ( " Zip seek error (%s) " , $file );
default :
return sprintf ( " '%s' is not a valid zip archive, got error code: %s " , $file , $retval );
2012-03-29 12:19:41 +00:00
}
}
2011-09-17 13:12:45 +00:00
}