<?php /* * This file is part of PHP CS Fixer. * * (c) Fabien Potencier <fabien@symfony.com> * Dariusz Rumiński <dariusz.ruminski@gmail.com> * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Symfony\CS\Console\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\CS\ToolInfo; /** * @author Igor Wiedler <igor@wiedler.ch> * @author Stephane PY <py.stephane1@gmail.com> * @author Grégoire Pineau <lyrixx@lyrixx.info> * @author Dariusz Rumiński <dariusz.ruminski@gmail.com> * @author SpacePossum */ class SelfUpdateCommand extends Command { const COMMAND_NAME = 'self-update'; /** * {@inheritdoc} */ protected function configure() { $this ->setName(self::COMMAND_NAME) ->setAliases(array('selfupdate')) ->setDefinition( array( new InputOption('--force', '-f', InputOption::VALUE_NONE, 'Force update to next major version if available.'), ) ) ->setDescription('Update php-cs-fixer.phar to the latest stable version.') ->setHelp(<<<'EOT' The <info>%command.name%</info> command replace your php-cs-fixer.phar by the latest version released on: <comment>https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases</comment> <info>$ php php-cs-fixer.phar %command.name%</info> EOT ) ; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { if (!ToolInfo::isInstalledAsPhar()) { $output->writeln('<error>Self-update is available only for PHAR version.</error>'); return 1; } $remoteTag = $this->getLatestTag(); if (null === $remoteTag) { $output->writeln('<error>Unable to determine newest version.</error>'); return 0; } $currentVersion = 'v'.$this->getApplication()->getVersion(); if ($currentVersion === $remoteTag) { $output->writeln('<info>php-cs-fixer is already up to date.</info>'); return 0; } $remoteVersionParsed = $this->parseVersion($remoteTag); $currentVersionParsed = $this->parseVersion($currentVersion); if ($remoteVersionParsed[0] > $currentVersionParsed[0] && true !== $input->getOption('force')) { $output->writeln(sprintf('<info>A new major version of php-cs-fixer is available</info> (<comment>%s</comment>)', $remoteTag)); $output->writeln(sprintf('<info>Before upgrading please read</info> https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/%s/UPGRADE.md', $remoteTag)); $output->writeln('<info>If you are ready to upgrade run this command with</info> <comment>-f</comment>'); $output->writeln('<info>Checking for new minor/patch version...</info>'); // test if there is a new minor version available $remoteTag = $this->getLatestNotMajorUpdateTag($currentVersion); if ($currentVersion === $remoteTag) { $output->writeln('<info>no minor update for php-cs-fixer.</info>'); return 0; } } $remoteFilename = sprintf('https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/%s/php-cs-fixer.phar', $remoteTag); $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; $tempFilename = basename($localFilename, '.phar').'-tmp.phar'; try { $copyResult = @copy($remoteFilename, $tempFilename); if (false === $copyResult) { $output->writeln(sprintf('<error>Unable to download new version %s from the server.</error>', $remoteTag)); return 1; } chmod($tempFilename, 0777 & ~umask()); // test the phar validity $phar = new \Phar($tempFilename); // free the variable to unlock the file unset($phar); rename($tempFilename, $localFilename); $output->writeln(sprintf('<info>php-cs-fixer updated</info> (<comment>%s</comment>)', $remoteTag)); } catch (\Exception $e) { if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { throw $e; } unlink($tempFilename); $output->writeln(sprintf('<error>The download of %s is corrupt (%s).</error>', $remoteTag, $e->getMessage())); $output->writeln('<error>Please re-run the self-update command to try again.</error>'); return 1; } } /** * @return string|null */ private function getLatestTag() { $raw = file_get_contents( 'https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/releases/latest', null, stream_context_create($this->getStreamContextOptions()) ); if (false === $raw) { return null; } $json = json_decode($raw, true); if (null === $json) { return null; } return $json['tag_name']; } /** * @param string $currentTag in format v?\d.\d.\d * * @return string in format v?\d.\d.\d */ private function getLatestNotMajorUpdateTag($currentTag) { $currentTagParsed = $this->parseVersion($currentTag); $nextVersionParsed = $currentTagParsed; do { $nextTag = sprintf('v%d.%d.%d', $nextVersionParsed[0], ++$nextVersionParsed[1], 0); } while ($this->hasRemoteTag($nextTag)); $nextVersionParsed = $this->parseVersion($nextTag); --$nextVersionParsed[1]; // check if new minor found, otherwise start looking for new patch from the current patch number if ($currentTagParsed[1] === $nextVersionParsed[1]) { $nextVersionParsed[2] = $currentTagParsed[2]; } do { $nextTag = sprintf('v%d.%d.%d', $nextVersionParsed[0], $nextVersionParsed[1], ++$nextVersionParsed[2]); } while ($this->hasRemoteTag($nextTag)); return sprintf('v%d.%d.%d', $nextVersionParsed[0], $nextVersionParsed[1], $nextVersionParsed[2] - 1); } /** * @param string $method HTTP method * * @return array */ private function getStreamContextOptions($method = 'GET') { return array( 'http' => array( 'header' => 'User-Agent: FriendsOfPHP/PHP-CS-Fixer', 'method' => $method, ), ); } /** * @param string $tag * * @return bool */ private function hasRemoteTag($tag) { $url = 'https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/releases/tags/'.$tag; stream_context_set_default( $this->getStreamContextOptions('HEAD') ); $headers = get_headers($url); if (!is_array($headers) || count($headers) < 1) { throw new \RuntimeException(sprintf('Failed to get headers for "%s".', $url)); } return 1 === preg_match('#^HTTP\/\d.\d 200#', $headers[0]); } /** * @param string $tag version in format v?\d.\d.\d * * @return int[] */ private function parseVersion($tag) { $tag = explode('.', $tag); if ('v' === $tag[0][0]) { $tag[0] = substr($tag[0], 1); } return array((int) $tag[0], (int) $tag[1], (int) $tag[2]); } }