2022-02-23 15:58:18 +00:00
< ? php declare ( strict_types = 1 );
2011-05-06 17:55:49 +00:00
/*
* 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 ;
2022-03-29 18:36:39 +00:00
use Composer\Pcre\Preg ;
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 ;
2022-02-22 15:47:09 +00:00
use Symfony\Component\Process\Process ;
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-09-05 14:02:10 +00:00
/** @var array<int, array{0: string, 1: string}> */
2021-05-11 11:23:18 +00:00
private static $unzipCommands ;
2021-09-05 14:02:10 +00:00
/** @var bool */
2021-06-02 13:00:31 +00:00
private static $hasZipArchive ;
2021-09-05 14:02:10 +00:00
/** @var bool */
2017-03-30 07:24:48 +00:00
private static $isWindows ;
2019-01-07 15:22:41 +00:00
/** @var ZipArchive|null */
2021-11-14 19:42:24 +00:00
private $zipArchiveObject ; // @phpstan-ignore-line helper property that is set via reflection for testing purposes
2012-01-18 15:37:55 +00:00
2016-02-25 13:05:26 +00:00
/**
2021-10-27 13:47:42 +00:00
* @ inheritDoc
2016-02-25 13:05:26 +00:00
*/
2022-08-17 12:20:07 +00:00
public function download ( PackageInterface $package , string $path , ? PackageInterface $prevPackage = null , bool $output = true ) : PromiseInterface
2016-02-25 13:05:26 +00:00
{
2021-05-11 11:23:18 +00:00
if ( null === self :: $unzipCommands ) {
2022-08-17 12:20:07 +00:00
self :: $unzipCommands = [];
2016-02-25 13:05:26 +00:00
$finder = new ExecutableFinder ;
2022-08-17 12:20:07 +00:00
if ( Platform :: isWindows () && ( $cmd = $finder -> find ( '7z' , null , [ 'C:\Program Files\7-Zip' ]))) {
self :: $unzipCommands [] = [ '7z' , ProcessExecutor :: escape ( $cmd ) . ' x -bb0 -y %s -o%s' ];
2021-05-11 11:23:18 +00:00
}
if ( $cmd = $finder -> find ( 'unzip' )) {
2022-08-17 12:20:07 +00:00
self :: $unzipCommands [] = [ '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
2022-08-17 12:20:07 +00:00
self :: $unzipCommands [] = [ '7z' , ProcessExecutor :: escape ( $cmd ) . ' x -bb0 -y %s -o%s' ];
2021-07-12 12:49:44 +00:00
}
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
2022-08-17 12:20:07 +00:00
self :: $unzipCommands [] = [ '7zz' , ProcessExecutor :: escape ( $cmd ) . ' x -bb0 -y %s -o%s' ];
2021-07-12 13:50:02 +00:00
}
2016-02-25 13:05:26 +00:00
}
2021-08-18 08:15:47 +00:00
$procOpenMissing = false ;
if ( ! function_exists ( 'proc_open' )) {
2022-08-17 12:20:07 +00:00
self :: $unzipCommands = [];
2021-08-18 08:15:47 +00:00
$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> " );
2022-03-29 18:36:39 +00:00
$this -> io -> writeError ( " <warning>Enabling proc_open and installing 'unzip' or '7z' (21.01+) may remediate them.</warning> " );
2021-08-18 08:15:47 +00:00
} 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> " );
2022-03-29 18:36:39 +00:00
$this -> io -> writeError ( " <warning>Installing 'unzip' or '7z' (21.01+) may remediate them.</warning> " );
2021-08-18 08:15:47 +00:00
}
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
2017-02-13 12:43:36 +00:00
*/
2022-02-22 15:47:09 +00:00
private function extractWithSystemUnzip ( PackageInterface $package , string $file , string $path ) : PromiseInterface
2011-05-06 17:55:49 +00:00
{
2022-03-29 18:36:39 +00:00
static $warned7ZipLinux = false ;
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 ];
2022-08-17 12:20:07 +00:00
if ( ! $warned7ZipLinux && ! Platform :: isWindows () && in_array ( $executable , [ '7z' , '7zz' ], true )) {
2022-03-29 18:36:39 +00:00
$warned7ZipLinux = true ;
if ( 0 === $this -> process -> execute ( $executable , $output )) {
if ( Preg :: isMatch ( '{^\s*7-Zip(?: \[64\])? ([0-9.]+)}' , $output , $match ) && version_compare ( $match [ 1 ], '21.01' , '<' )) {
$this -> io -> writeError ( ' <warning>Unzipping using ' . $executable . ' ' . $match [ 1 ] . ' may result in incorrect file permissions. Install ' . $executable . ' 21.01+ or unzip to ensure you get correct permissions.</warning>' );
}
}
}
2020-06-05 07:07:40 +00:00
2021-06-02 13:00:31 +00:00
$io = $this -> io ;
2022-02-22 15:47:09 +00:00
$tryFallback = function ( \Throwable $processError ) use ( $isLastChance , $io , $file , $path , $package , $executable ) : \React\Promise\PromiseInterface {
2021-06-02 13:00:31 +00:00
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
2022-01-03 14:40:32 +00:00
return $this -> extractWithZipArchive ( $package , $file , $path );
2021-06-02 13:00:31 +00:00
};
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
2022-02-22 15:47:09 +00:00
return $promise -> then ( function ( Process $process ) use ( $tryFallback , $command , $package , $file ) {
2021-06-02 13:00:31 +00:00
if ( ! $process -> isSuccessful ()) {
2022-01-03 14:40:32 +00:00
if ( isset ( $this -> cleanupExecuted [ $package -> getName ()])) {
2021-10-15 08:42:44 +00:00
throw new \RuntimeException ( 'Failed to extract ' . $package -> getName () . ' as the installation was aborted by another package operation.' );
}
2021-06-02 13:00:31 +00:00
$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 ( \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
2017-02-13 13:00:48 +00:00
*/
2022-02-22 15:47:09 +00:00
private function extractWithZipArchive ( PackageInterface $package , string $file , string $path ) : PromiseInterface
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 {
2022-03-29 16:57:30 +00:00
if ( ! file_exists ( $file ) || ( $filesize = filesize ( $file )) === false || $filesize === 0 ) {
$retval = - 1 ;
} else {
$retval = $zipArchive -> open ( $file );
}
if ( true === $retval ) {
2017-03-14 22:43:48 +00:00
$extractResult = $zipArchive -> extractTo ( $path );
if ( true === $extractResult ) {
$zipArchive -> close ();
2017-03-30 07:24:48 +00:00
2022-03-18 08:20:42 +00:00
return \React\Promise\resolve ( null );
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 );
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
2017-02-13 13:00:48 +00:00
*/
2022-02-22 21:10:52 +00:00
protected function extract ( PackageInterface $package , string $file , string $path ) : PromiseInterface
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
*/
2022-02-22 15:47:09 +00:00
protected function getErrorMessage ( int $retval , string $file ) : string
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 );
2022-03-29 16:57:30 +00:00
case - 1 :
return sprintf ( " '%s' is a corrupted zip archive (0 bytes), try again. " , $file );
2012-03-29 13:08:47 +00:00
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
}