From 1a725d5e1f7d24aeab9355b2174681b7927346dd Mon Sep 17 00:00:00 2001 From: Jonas Renaudot Date: Tue, 17 Jul 2018 12:04:27 +0200 Subject: [PATCH 1/3] Add support for authentication with mercurial repositories. --- src/Composer/Downloader/HgDownloader.php | 31 ++++---- src/Composer/Repository/Vcs/HgDriver.php | 15 ++-- src/Composer/Util/Hg.php | 91 ++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 src/Composer/Util/Hg.php diff --git a/src/Composer/Downloader/HgDownloader.php b/src/Composer/Downloader/HgDownloader.php index 32074be71..8660ec61d 100644 --- a/src/Composer/Downloader/HgDownloader.php +++ b/src/Composer/Downloader/HgDownloader.php @@ -14,6 +14,7 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\Util\ProcessExecutor; +use Composer\Util\Hg as HgUtils; /** * @author Per Bernhardt @@ -25,16 +26,15 @@ class HgDownloader extends VcsDownloader */ public function doDownload(PackageInterface $package, $path, $url) { - // Ensure we are allowed to use this URL by config - $this->config->prohibitUrlByConfig($url, $this->io); + $hgUtils = new HgUtils($this->io, $this->config, $this->process); + + $cloneCommand = function($url) use ($path) { + return sprintf('hg clone %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($path)); + }; + + $hgUtils->runCommand($cloneCommand, $url, $path); - $url = ProcessExecutor::escape($url); $ref = ProcessExecutor::escape($package->getSourceReference()); - $this->io->writeError("Cloning ".$package->getSourceReference()); - $command = sprintf('hg clone %s %s', $url, ProcessExecutor::escape($path)); - if (0 !== $this->process->execute($command, $ignoredOutput)) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); - } $command = sprintf('hg up %s', $ref); if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); @@ -46,21 +46,20 @@ class HgDownloader extends VcsDownloader */ public function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { - // Ensure we are allowed to use this URL by config - $this->config->prohibitUrlByConfig($url, $this->io); + $hgUtils = new HgUtils($this->io, $this->config, $this->process); - $url = ProcessExecutor::escape($url); - $ref = ProcessExecutor::escape($target->getSourceReference()); + $ref = $target->getSourceReference(); $this->io->writeError(" Updating to ".$target->getSourceReference()); if (!$this->hasMetadataRepository($path)) { throw new \RuntimeException('The .hg directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); } - $command = sprintf('hg pull %s && hg up %s', $url, $ref); - if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); - } + $command = function($url) use ($ref) { + return sprintf('hg pull %s && hg up %s', ProcessExecutor::escape($url), ProcessExecutor::escape($ref)); + }; + + $hgUtils->runCommand($command, $url, $path); } /** diff --git a/src/Composer/Repository/Vcs/HgDriver.php b/src/Composer/Repository/Vcs/HgDriver.php index bf7c91552..8141f453b 100644 --- a/src/Composer/Repository/Vcs/HgDriver.php +++ b/src/Composer/Repository/Vcs/HgDriver.php @@ -13,6 +13,7 @@ namespace Composer\Repository\Vcs; use Composer\Config; +use Composer\Util\Hg as HgUtils; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; use Composer\IO\IOInterface; @@ -49,6 +50,8 @@ class HgDriver extends VcsDriver // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($this->url, $this->io); + $hgUtils = new HgUtils($this->io, $this->config, $this->process); + // update the repo if it is a valid hg repository if (is_dir($this->repoDir) && 0 === $this->process->execute('hg summary', $output, $this->repoDir)) { if (0 !== $this->process->execute('hg pull', $output, $this->repoDir)) { @@ -58,15 +61,11 @@ class HgDriver extends VcsDriver // clean up directory and do a fresh clone into it $fs->removeDirectory($this->repoDir); - if (0 !== $this->process->execute(sprintf('hg clone --noupdate %s %s', ProcessExecutor::escape($this->url), ProcessExecutor::escape($this->repoDir)), $output, $cacheDir)) { - $output = $this->process->getErrorOutput(); + $command = function($url) { + return sprintf('hg clone --noupdate %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($this->repoDir)); + }; - if (0 !== $this->process->execute('hg --version', $ignoredOutput)) { - throw new \RuntimeException('Failed to clone '.$this->url.', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()); - } - - throw new \RuntimeException('Failed to clone '.$this->url.', could not read packages from it' . "\n\n" .$output); - } + $hgUtils->runCommand($command, $this->url, $this->repoDir); } } diff --git a/src/Composer/Util/Hg.php b/src/Composer/Util/Hg.php new file mode 100644 index 000000000..673ad783a --- /dev/null +++ b/src/Composer/Util/Hg.php @@ -0,0 +1,91 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util; + +use Composer\Config; +use Composer\IO\IOInterface; + +/** + * @author Jonas Renaudot + */ +class Hg +{ + + /** + * @var \Composer\IO\IOInterface + */ + private $io; + + /** + * @var \Composer\Config + */ + private $config; + + /** + * @var \Composer\Util\ProcessExecutor + */ + private $process; + + public function __construct(IOInterface $io, Config $config, ProcessExecutor $process) + { + $this->io = $io; + $this->config = $config; + $this->process = $process; + } + + public function runCommand($commandCallable, $url, $cwd) { + $this->config->prohibitUrlByConfig($url, $this->io); + + // Try as is + $command = call_user_func($commandCallable, $url); + + if (0 === $this->process->execute($command, $ignoredOutput, $cwd)){ + return; + } + + // Try with the authentication informations available + if (preg_match('{^(https?)://((.+)(?:\:(.+))?@)?([^/]+)(/.*)?}mi', $url, $match) && $this->io->hasAuthentication($match[5])) { + $auth = $this->io->getAuthentication($match[5]); + $authenticatedUrl = $match[1] . '://' . rawurldecode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[5] . (!empty($match[6])? $match[6]: null); + + $command = call_user_func($commandCallable, $authenticatedUrl); + + if (0 === $this->process->execute($command)) { + return; + } + } + + $this->throwException('Failed to clone ' . $url . ', aborting', $url); + + } + + public static function sanitizeUrl($message) + { + return preg_replace_callback('{://(?P[^@]+?):(?P.+?)@}', function ($m) { + if (preg_match('{^[a-f0-9]{12,}$}', $m[1])) { + return '://***:***@'; + } + + return '://' . $m[1] . ':***@'; + }, $message); + } + + private function throwException($message, $url) + { + if (0 !== $this->process->execute('hg --version', $ignoredOutput)) { + throw new \RuntimeException(self::sanitizeUrl('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); + } + + throw new \RuntimeException(self::sanitizeUrl($message)); + } +} From 5c2b34a1af4b6e6ac93c8f5389369a9605051a92 Mon Sep 17 00:00:00 2001 From: Elendev Date: Tue, 17 Jul 2018 19:46:25 +0200 Subject: [PATCH 2/3] Encode the username correctly (fix typo) --- src/Composer/Util/Hg.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/Hg.php b/src/Composer/Util/Hg.php index 673ad783a..00ab3b8e8 100644 --- a/src/Composer/Util/Hg.php +++ b/src/Composer/Util/Hg.php @@ -56,7 +56,7 @@ class Hg // Try with the authentication informations available if (preg_match('{^(https?)://((.+)(?:\:(.+))?@)?([^/]+)(/.*)?}mi', $url, $match) && $this->io->hasAuthentication($match[5])) { $auth = $this->io->getAuthentication($match[5]); - $authenticatedUrl = $match[1] . '://' . rawurldecode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[5] . (!empty($match[6])? $match[6]: null); + $authenticatedUrl = $match[1] . '://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[5] . (!empty($match[6])? $match[6]: null); $command = call_user_func($commandCallable, $authenticatedUrl); From ea5644281a1c4d27058a884ed684934875d14bde Mon Sep 17 00:00:00 2001 From: Elendev Date: Tue, 17 Jul 2018 20:03:07 +0200 Subject: [PATCH 3/3] Display the error output in the thrown exception --- src/Composer/Util/Hg.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/Hg.php b/src/Composer/Util/Hg.php index 00ab3b8e8..5afe7b3f4 100644 --- a/src/Composer/Util/Hg.php +++ b/src/Composer/Util/Hg.php @@ -63,9 +63,15 @@ class Hg if (0 === $this->process->execute($command)) { return; } + + $error = $this->process->getErrorOutput(); + } else { + $error = 'The given URL (' . $url . ') does not match the required format (http(s)://(username:password@)example.com/path-to-repository)'; } - $this->throwException('Failed to clone ' . $url . ', aborting', $url); + + + $this->throwException('Failed to clone ' . $url . ', ' . "\n\n" . $error, $url); }