diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 4074d8bad..598be7431 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -17,6 +17,7 @@ use Composer\Factory; use Composer\Util\RemoteFilesystem; use Composer\Downloader\FilesystemException; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -24,12 +25,32 @@ use Symfony\Component\Console\Output\OutputInterface; */ class SelfUpdateCommand extends Command { + const ROLLBACK = 'rollback'; + const HOMEPAGE = 'getcomposer.org'; + + protected $remoteFS; + protected $latestVersion; + protected $homepageURL; + protected $localFilename; + + public function __construct($name = null) + { + parent::__construct($name); + $protocol = (extension_loaded('openssl') ? 'https' : 'http') . '://'; + $this->homepageURL = $protocol . self::HOMEPAGE; + $this->remoteFS = new RemoteFilesystem($this->getIO()); + $this->localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; + } + protected function configure() { $this ->setName('self-update') ->setAliases(array('selfupdate')) ->setDescription('Updates composer.phar to the latest version.') + ->setDefinition(array( + new InputOption(self::ROLLBACK, 'r', InputOption::VALUE_OPTIONAL, 'Revert to an older installation of composer'), + )) ->setHelp(<<self-update command checks getcomposer.org for newer versions of composer and if found, installs the latest. @@ -44,57 +65,126 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { $config = Factory::createConfig(); - $cacheDir = $config->get('cache-dir'); - - $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; + $cacheDir = rtrim($config->get('cache-dir'), '/'); + $saveDir = rtrim($config->get('home'), '/'); // Check if current dir is writable and if not try the cache dir from settings - $tmpDir = is_writable(dirname($localFilename))? dirname($localFilename) : $cacheDir; - $tempFilename = $tmpDir . '/' . basename($localFilename, '.phar').'-temp.phar'; + $tmpDir = is_writable(dirname($this->localFilename))? dirname($this->localFilename) : $cacheDir; // check for permissions in local filesystem before start connection process if (!is_writable($tmpDir)) { throw new FilesystemException('Composer update failed: the "'.$tmpDir.'" directory used to download the temp file could not be written'); } - if (!is_writable($localFilename)) { - throw new FilesystemException('Composer update failed: the "'.$localFilename. '" file could not be written'); + if (!is_writable($this->localFilename)) { + throw new FilesystemException('Composer update failed: the "'.$this->localFilename.'" file could not be written'); } - $protocol = extension_loaded('openssl') ? 'https' : 'http'; - $rfs = new RemoteFilesystem($this->getIO()); - $latest = trim($rfs->getContents('getcomposer.org', $protocol . '://getcomposer.org/version', false)); + $rollback = $this->getOption(self::ROLLBACK); - if (Composer::VERSION !== $latest) { - $output->writeln(sprintf("Updating to version %s.", $latest)); + if (is_null($rollback)) { + $rollback = $this->getLastVersion(); + if (!$rollback) { + throw new FilesystemException('Composer rollback failed: no installation to roll back to in "'.$saveDir.'"'); - $remoteFilename = $protocol . '://getcomposer.org/composer.phar'; + return 1; + } + } - $rfs->copy('getcomposer.org', $remoteFilename, $tempFilename); + // if a rollback version is specified, check for permissions and rollback installation + if ($rollback) { + if (!is_writable($saveDir)) { + throw new FilesystemException('Composer rollback failed: the "'.$saveDir.'" dir could not be written to'); + } + $old = $saveDir . "/{$rollback}.phar"; + + if (!is_file($old)) { + throw new FilesystemException('Composer rollback failed: "'.$old.'" could not be found'); + } + if (!is_readable($old)) { + throw new FilesystemException('Composer rollback failed: "'.$old.'" could not be read'); + } + } + + $updateVersion = ($rollback)? $rollback : $this->getLatestVersion(); + + if (Composer::VERSION === $updateVersion) { + $output->writeln("You are already using composer v%s.", $updateVersion); + + return 0; + } + + $tempFilename = $tmpDir . '/' . basename($this->localFilename, '.phar').'-temp.phar'; + + if ($rollback) { + copy($saveDir . "/{$rollback}.phar", $tempFilename); + } else { + $endpoint = ($updateVersion === $this->getLatestVersion()) ? '/composer.phar' : "/download/{$updateVersion}/composer.phar"; + $remoteFilename = $this->homepageURL . $endpoint; + + $output->writeln(sprintf("Updating to version %s.", $updateVersion)); + + $this->remoteFS->copy(self::HOMEPAGE, $remoteFilename, $tempFilename); + + // @todo: handle snapshot versions not being found! if (!file_exists($tempFilename)) { $output->writeln('The download of the new composer version failed for an unexpected reason'); return 1; } + } - try { - @chmod($tempFilename, 0777 & ~umask()); - // test the phar validity - $phar = new \Phar($tempFilename); - // free the variable to unlock the file - unset($phar); - rename($tempFilename, $localFilename); - } catch (\Exception $e) { - @unlink($tempFilename); - if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { - throw $e; - } - $output->writeln('The download is corrupted ('.$e->getMessage().').'); - $output->writeln('Please re-run the self-update command to try again.'); - } - } else { - $output->writeln("You are using the latest composer version."); + if ($err = $this->setLocalPhar($tempFilename, $saveDir)) { + $output->writeln('The file is corrupted ('.$err->getMessage().').'); + $output->writeln('Please re-run the self-update command to try again.'); + + return 1; } } + + protected function setLocalPhar($filename, $saveDir) + { + try { + @chmod($filename, 0777 & ~umask()); + // test the phar validity + $phar = new \Phar($filename); + // copy current file into installations dir + copy($this->localFilename, $saveDir . Composer::VERSION . '.phar'); + // free the variable to unlock the file + unset($phar); + rename($filename, $this->localFilename); + } catch (\Exception $e) { + @unlink($filename); + if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { + throw $e; + } + + return $e; + } + } + + protected function getLastVersion($saveDir) + { + $config = Factory::createConfig(); + $files = glob($saveDir . '/*.phar'); + + if (empty($files)) { + return false; + } + + $fileTimes = array_map('filemtime', $files); + $map = array_combine($fileTimes, $files); + $latest = max($fileTimes); + return basename($map[$latest], '.phar'); + } + + protected function getLatestVersion() + { + if (!$this->latestVersion) { + $this->latestVersion = trim($this->remoteFS->getContents(self::HOMEPAGE, $this->homepageURL. '/version', false)); + } + + return $this->latestVersion; + } }