ZipDownloader.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Downloader;
  12. use Composer\Config;
  13. use Composer\Cache;
  14. use Composer\EventDispatcher\EventDispatcher;
  15. use Composer\Package\PackageInterface;
  16. use Composer\Util\IniHelper;
  17. use Composer\Util\Platform;
  18. use Composer\Util\ProcessExecutor;
  19. use Composer\Util\RemoteFilesystem;
  20. use Composer\IO\IOInterface;
  21. use Symfony\Component\Process\ExecutableFinder;
  22. use ZipArchive;
  23. /**
  24. * @author Jordi Boggiano <j.boggiano@seld.be>
  25. */
  26. class ZipDownloader extends ArchiveDownloader
  27. {
  28. protected static $hasSystemUnzip;
  29. private static $hasZipArchive;
  30. private static $isWindows;
  31. protected $process;
  32. private $zipArchiveObject;
  33. public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
  34. {
  35. $this->process = $process ?: new ProcessExecutor($io);
  36. parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
  37. }
  38. /**
  39. * {@inheritDoc}
  40. */
  41. public function download(PackageInterface $package, $path, $output = true)
  42. {
  43. if (null === self::$hasSystemUnzip) {
  44. $finder = new ExecutableFinder;
  45. self::$hasSystemUnzip = (bool) $finder->find('unzip');
  46. }
  47. if (null === self::$hasZipArchive) {
  48. self::$hasZipArchive = class_exists('ZipArchive');
  49. }
  50. if (!self::$hasZipArchive && !self::$hasSystemUnzip) {
  51. // php.ini path is added to the error message to help users find the correct file
  52. $iniMessage = IniHelper::getMessage();
  53. $error = "The zip extension and unzip command are both missing, skipping.\n" . $iniMessage;
  54. throw new \RuntimeException($error);
  55. }
  56. if (null === self::$isWindows) {
  57. self::$isWindows = Platform::isWindows();
  58. if (!self::$isWindows && !self::$hasSystemUnzip) {
  59. $this->io->writeError("<warning>As there is no 'unzip' command installed zip files are being unpacked using the PHP zip extension.</warning>");
  60. $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>");
  61. $this->io->writeError("<warning>Installing 'unzip' may remediate them.</warning>");
  62. }
  63. }
  64. return parent::download($package, $path, $output);
  65. }
  66. /**
  67. * extract $file to $path with "unzip" command
  68. *
  69. * @param string $file File to extract
  70. * @param string $path Path where to extract file
  71. * @param bool $isLastChance If true it is called as a fallback and should throw an exception
  72. * @return bool Success status
  73. */
  74. protected function extractWithSystemUnzip($file, $path, $isLastChance)
  75. {
  76. if (!self::$hasZipArchive) {
  77. // Force Exception throwing if the Other alternative is not available
  78. $isLastChance = true;
  79. }
  80. if (!self::$hasSystemUnzip && !$isLastChance) {
  81. // This was call as the favorite extract way, but is not available
  82. // We switch to the alternative
  83. return $this->extractWithZipArchive($file, $path, true);
  84. }
  85. $processError = null;
  86. // When called after a ZipArchive failed, perhaps there is some files to overwrite
  87. $overwrite = $isLastChance ? '-o' : '';
  88. $command = 'unzip -qq '.$overwrite.' '.ProcessExecutor::escape($file).' -d '.ProcessExecutor::escape($path);
  89. try {
  90. if (0 === $this->process->execute($command, $ignoredOutput)) {
  91. return true;
  92. }
  93. $processError = new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput());
  94. } catch (\Exception $e) {
  95. $processError = $e;
  96. }
  97. if ($isLastChance) {
  98. throw $processError;
  99. }
  100. $this->io->writeError(' '.$processError->getMessage());
  101. $this->io->writeError(' The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems)');
  102. $this->io->writeError(' Unzip with unzip command failed, falling back to ZipArchive class');
  103. return $this->extractWithZipArchive($file, $path, true);
  104. }
  105. /**
  106. * extract $file to $path with ZipArchive
  107. *
  108. * @param string $file File to extract
  109. * @param string $path Path where to extract file
  110. * @param bool $isLastChance If true it is called as a fallback and should throw an exception
  111. * @return bool Success status
  112. */
  113. protected function extractWithZipArchive($file, $path, $isLastChance)
  114. {
  115. if (!self::$hasSystemUnzip) {
  116. // Force Exception throwing if the Other alternative is not available
  117. $isLastChance = true;
  118. }
  119. if (!self::$hasZipArchive && !$isLastChance) {
  120. // This was call as the favorite extract way, but is not available
  121. // We switch to the alternative
  122. return $this->extractWithSystemUnzip($file, $path, true);
  123. }
  124. $processError = null;
  125. $zipArchive = $this->zipArchiveObject ?: new ZipArchive();
  126. try {
  127. if (true === ($retval = $zipArchive->open($file))) {
  128. $extractResult = $zipArchive->extractTo($path);
  129. if (true === $extractResult) {
  130. $zipArchive->close();
  131. return true;
  132. }
  133. $processError = new \RuntimeException(rtrim("There was an error extracting the ZIP file, it is either corrupted or using an invalid format.\n"));
  134. } else {
  135. $processError = new \UnexpectedValueException(rtrim($this->getErrorMessage($retval, $file)."\n"), $retval);
  136. }
  137. } catch (\ErrorException $e) {
  138. $processError = new \RuntimeException('The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems): '.$e->getMessage(), 0, $e);
  139. } catch (\Exception $e) {
  140. $processError = $e;
  141. }
  142. if ($isLastChance) {
  143. throw $processError;
  144. }
  145. $this->io->writeError(' '.$processError->getMessage());
  146. $this->io->writeError(' Unzip with ZipArchive class failed, falling back to unzip command');
  147. return $this->extractWithSystemUnzip($file, $path, true);
  148. }
  149. /**
  150. * extract $file to $path
  151. *
  152. * @param string $file File to extract
  153. * @param string $path Path where to extract file
  154. */
  155. public function extract($file, $path)
  156. {
  157. // Each extract calls its alternative if not available or fails
  158. if (self::$isWindows) {
  159. $this->extractWithZipArchive($file, $path, false);
  160. } else {
  161. $this->extractWithSystemUnzip($file, $path, false);
  162. }
  163. }
  164. /**
  165. * Give a meaningful error message to the user.
  166. *
  167. * @param int $retval
  168. * @param string $file
  169. * @return string
  170. */
  171. protected function getErrorMessage($retval, $file)
  172. {
  173. switch ($retval) {
  174. case ZipArchive::ER_EXISTS:
  175. return sprintf("File '%s' already exists.", $file);
  176. case ZipArchive::ER_INCONS:
  177. return sprintf("Zip archive '%s' is inconsistent.", $file);
  178. case ZipArchive::ER_INVAL:
  179. return sprintf("Invalid argument (%s)", $file);
  180. case ZipArchive::ER_MEMORY:
  181. return sprintf("Malloc failure (%s)", $file);
  182. case ZipArchive::ER_NOENT:
  183. return sprintf("No such zip file: '%s'", $file);
  184. case ZipArchive::ER_NOZIP:
  185. return sprintf("'%s' is not a zip archive.", $file);
  186. case ZipArchive::ER_OPEN:
  187. return sprintf("Can't open zip file: %s", $file);
  188. case ZipArchive::ER_READ:
  189. return sprintf("Zip read error (%s)", $file);
  190. case ZipArchive::ER_SEEK:
  191. return sprintf("Zip seek error (%s)", $file);
  192. default:
  193. return sprintf("'%s' is not a valid zip archive, got error code: %s", $file, $retval);
  194. }
  195. }
  196. }