* Dariusz Rumiński * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Console\Command; use PhpCsFixer\ToolInfo; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * @author Igor Wiedler * @author Stephane PY * @author Grégoire Pineau * @author Dariusz Rumiński * @author SpacePossum * * @internal */ final class SelfUpdateCommand extends Command { /** * {@inheritdoc} */ protected function configure() { $this ->setName('self-update') ->setAliases(['selfupdate']) ->setDefinition( [ 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 %command.name% command replace your php-cs-fixer.phar by the latest version released on: https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases $ php php-cs-fixer.phar %command.name% EOT ) ; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { if (!ToolInfo::isInstalledAsPhar()) { $output->writeln('Self-update is available only for PHAR version.'); return 1; } $remoteTag = $this->getLatestTag(); if (null === $remoteTag) { $output->writeln('Unable to determine newest version.'); return 0; } $currentVersion = 'v'.$this->getApplication()->getVersion(); if ($currentVersion === $remoteTag) { $output->writeln('php-cs-fixer is already up to date.'); return 0; } $remoteVersionParsed = $this->parseVersion($remoteTag); $currentVersionParsed = $this->parseVersion($currentVersion); if ($remoteVersionParsed[0] > $currentVersionParsed[0] && true !== $input->getOption('force')) { $output->writeln(sprintf('A new major version of php-cs-fixer is available (%s)', $remoteTag)); $output->writeln(sprintf('Before upgrading please read https://github.com/FriendsOfPHP/PHP-CS-Fixer/blob/%s/UPGRADE.md', $remoteTag)); $output->writeln('If you are ready to upgrade run this command with -f'); $output->writeln('Checking for new minor/patch version...'); // test if there is a new minor version available $remoteTag = $this->getLatestNotMajorUpdateTag($currentVersion); if ($currentVersion === $remoteTag) { $output->writeln('No minor update for php-cs-fixer.'); return 0; } } $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; if (!is_writable($localFilename)) { $output->writeln(sprintf('No permission to update %s file.', $localFilename)); return 1; } $tempFilename = basename($localFilename, '.phar').'-tmp.phar'; $remoteFilename = sprintf('https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/%s/php-cs-fixer.phar', $remoteTag); try { $copyResult = @copy($remoteFilename, $tempFilename); if (false === $copyResult) { $output->writeln(sprintf('Unable to download new version %s from the server.', $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('php-cs-fixer updated (%s)', $remoteTag)); } catch (\Exception $e) { if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { throw $e; } unlink($tempFilename); $output->writeln(sprintf('The download of %s is corrupt (%s).', $remoteTag, $e->getMessage())); $output->writeln('Please re-run the self-update command to try again.'); return 1; } } /** * @return null|string */ 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 [ 'http' => [ '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 [(int) $tag[0], (int) $tag[1], (int) $tag[2]]; } }