From 1bd5d88b02c282bc0846cdd01f7a45b98c5ffa78 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Tue, 16 Oct 2012 14:16:39 +0200 Subject: [PATCH 1/7] quick workaround for Github API limit --- src/Composer/Repository/Vcs/GitHubDriver.php | 13 +++++++++++++ src/Composer/Util/RemoteFilesystem.php | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 58cfd82d9..5d1965d10 100755 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -292,6 +292,19 @@ class GitHubDriver extends VcsDriver $this->io->setAuthorization($this->originUrl, $username, $password); break; + case 403: + if (!$this->io->hasAuthorization($this->originUrl)) { + if (!$this->io->isInteractive()) { + $this->io->write('API limit exhausted. Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your username and password to increase the API limit'); + throw $e; + } + $this->io->write('API limit exhausted. Authentication required for larger API limit ('.$this->url.'):'); + $username = $this->io->ask('Username: '); + $password = $this->io->askAndHideAnswer('Password: '); + $this->io->setAuthorization($this->originUrl, $username, $password); + } + break; + default: throw $e; break; diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index d013e3b66..3555557ee 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -207,6 +207,12 @@ class RemoteFilesystem } break; + case STREAM_NOTIFY_AUTH_RESULT: + if (403 === $messageCode) { + throw new TransportException($message, 403); + } + break; + case STREAM_NOTIFY_FILE_SIZE_IS: if ($this->bytesMax < $bytesMax) { $this->bytesMax = $bytesMax; From 3b01d26d67f385874b9a50246630d2a97be6d5cd Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 18 Oct 2012 16:02:24 +0200 Subject: [PATCH 2/7] Swap user credentials for an OAuth token from GitHub --- src/Composer/Downloader/GitDownloader.php | 12 +- .../Downloader/TransportException.php | 11 + src/Composer/Repository/Vcs/GitHubDriver.php | 192 ++++++++++++------ src/Composer/Util/RemoteFilesystem.php | 50 +++-- 4 files changed, 181 insertions(+), 84 deletions(-) diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 3162e62cb..c7a829f5e 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -51,10 +51,12 @@ class GitDownloader extends VcsDownloader $this->io->write(" Checking out ".$ref); $command = 'cd %s && git remote set-url composer %s && git fetch composer && git fetch --tags composer'; - // capture username/password from github URL if there is one - $this->process->execute(sprintf('cd %s && git remote -v', escapeshellarg($path)), $output); - if (preg_match('{^composer\s+https://(.+):(.+)@github.com/}im', $output, $match)) { - $this->io->setAuthorization('github.com', $match[1], $match[2]); + if (!$this->io->hasAuthorization('github.com')) { + // capture username/password from github URL if there is one + $this->process->execute(sprintf('cd %s && git remote -v', escapeshellarg($path)), $output); + if (preg_match('{^composer\s+https://(.+):(.+)@github.com/}im', $output, $match)) { + $this->io->setAuthorization('github.com', $match[1], $match[2]); + } } $commandCallable = function($url) use ($ref, $path, $command) { @@ -327,7 +329,7 @@ class GitDownloader extends VcsDownloader protected function sanitizeUrl($message) { - return preg_match('{://(.+?):.+?@}', '://$1:***@', $message); + return preg_replace('{://(.+?):.+?@}', '://$1:***@', $message); } protected function setPushUrl(PackageInterface $package, $path) diff --git a/src/Composer/Downloader/TransportException.php b/src/Composer/Downloader/TransportException.php index 61bd67d11..d157dde3c 100644 --- a/src/Composer/Downloader/TransportException.php +++ b/src/Composer/Downloader/TransportException.php @@ -17,4 +17,15 @@ namespace Composer\Downloader; */ class TransportException extends \Exception { + protected $headers; + + public function setHeaders($headers) + { + $this->headers = $headers; + } + + public function getHeaders() + { + return $this->headers; + } } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 5d1965d10..b5f8274bf 100755 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -235,6 +235,62 @@ class GitHubDriver extends VcsDriver return 'git@github.com:'.$this->owner.'/'.$this->repository.'.git'; } + /** + * {@inheritDoc} + */ + protected function getContents($url, $tryClone = false) + { + try { + return parent::getContents($url); + } catch (TransportException $e) { + switch ($e->getCode()) { + case 401: + case 404: + if (!$this->io->isInteractive() && $tryClone) { + return $this->attemptCloneFallback($e); + } + + $this->io->write('Your GitHub credentials are required to fetch private repository metadata ('.$this->url.'):'); + $this->authorizeOauth(); + + return parent::getContents($url); + + case 403: + if (!$this->io->isInteractive() && $tryClone) { + return $this->attemptCloneFallback($e); + } + + $rateLimited = false; + foreach ($e->getHeaders() as $header) { + if (preg_match('{^X-RateLimit-Remaining: *0$}i', trim($header))) { + $rateLimited = true; + } + } + + if (!$this->io->hasAuthorization($this->originUrl)) { + if (!$this->io->isInteractive()) { + $this->io->write('GitHub API limit exhausted. Failed to get metadata for the '.$this->url.' repository, try running in interactive mode so that you can enter your GitHub credentials to increase the API limit'); + throw $e; + } + + $this->io->write('API limit exhausted. Enter your GitHub credentials to get a larger API limit ('.$this->url.'):'); + $this->authorizeOauth(); + + return parent::getContents($url); + } + + if ($rateLimited) { + $this->io->write('GitHub API limit exhausted. You are already authorized so you will have to wait a while before doing more requests'); + } + + throw $e; + + default: + throw $e; + } + } + } + /** * Fetch root identifier from GitHub * @@ -243,73 +299,83 @@ class GitHubDriver extends VcsDriver protected function fetchRootIdentifier() { $repoDataUrl = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository; - $attemptCounter = 0; - while (null === $this->rootIdentifier) { - if (5 == $attemptCounter++) { - throw new \RuntimeException("Either you have entered invalid credentials or this GitHub repository does not exists (404)"); - } - try { - $repoData = JsonFile::parseJson($this->getContents($repoDataUrl), $repoDataUrl); - if (isset($repoData['default_branch'])) { - $this->rootIdentifier = $repoData['default_branch']; - } elseif (isset($repoData['master_branch'])) { - $this->rootIdentifier = $repoData['master_branch']; - } else { - $this->rootIdentifier = 'master'; - } - $this->hasIssues = !empty($repoData['has_issues']); - } catch (TransportException $e) { - switch ($e->getCode()) { - case 401: - case 404: - $this->isPrivate = true; - try { - // If this repository may be private (hard to say for sure, - // GitHub returns 404 for private repositories) and we - // cannot ask for authentication credentials (because we - // are not interactive) then we fallback to GitDriver. - $this->gitDriver = new GitDriver( - $this->generateSshUrl(), - $this->io, - $this->config, - $this->process, - $this->remoteFilesystem - ); - $this->gitDriver->initialize(); + $repoData = JsonFile::parseJson($this->getContents($repoDataUrl, true), $repoDataUrl); + if (null === $repoData && null !== $this->gitDriver) { + return; + } - return; - } catch (\RuntimeException $e) { - $this->gitDriver = null; - if (!$this->io->isInteractive()) { - $this->io->write('Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your username and password'); - throw $e; - } - } - $this->io->write('Authentication required ('.$this->url.'):'); - $username = $this->io->ask('Username: '); - $password = $this->io->askAndHideAnswer('Password: '); - $this->io->setAuthorization($this->originUrl, $username, $password); - break; + $this->isPrivate = !empty($repoData['private']); + if (isset($repoData['default_branch'])) { + $this->rootIdentifier = $repoData['default_branch']; + } elseif (isset($repoData['master_branch'])) { + $this->rootIdentifier = $repoData['master_branch']; + } else { + $this->rootIdentifier = 'master'; + } + $this->hasIssues = !empty($repoData['has_issues']); + } - case 403: - if (!$this->io->hasAuthorization($this->originUrl)) { - if (!$this->io->isInteractive()) { - $this->io->write('API limit exhausted. Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your username and password to increase the API limit'); - throw $e; - } - $this->io->write('API limit exhausted. Authentication required for larger API limit ('.$this->url.'):'); - $username = $this->io->ask('Username: '); - $password = $this->io->askAndHideAnswer('Password: '); - $this->io->setAuthorization($this->originUrl, $username, $password); - } - break; + protected function attemptCloneFallback() + { + $this->isPrivate = true; - default: - throw $e; - break; - } - } + try { + // If this repository may be private (hard to say for sure, + // GitHub returns 404 for private repositories) and we + // cannot ask for authentication credentials (because we + // are not interactive) then we fallback to GitDriver. + $this->gitDriver = new GitDriver( + $this->generateSshUrl(), + $this->io, + $this->config, + $this->process, + $this->remoteFilesystem + ); + $this->gitDriver->initialize(); + + return; + } catch (\RuntimeException $e) { + $this->gitDriver = null; + + $this->io->write('Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your GitHub credentials'); + throw $e; } } + + protected function authorizeOAuth() + { + $attemptCounter = 0; + + $this->io->write('The credentials will be swapped for an OAuth token stored in '.$this->config->get('home').'/config.json, your password will not be stored'); + $this->io->write('To revoke access to this token you can visit https://github.com/settings/applications'); + while ($attemptCounter++ < 5) { + try { + $username = $this->io->ask('Username: '); + $password = $this->io->askAndHideAnswer('Password: '); + $this->io->setAuthorization($this->originUrl, $username, $password); + + $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($this->originUrl, 'https://api.github.com/authorizations', false, array( + 'http' => array( + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => '{"scopes":["repo"],"note":"Composer","note_url":"https://getcomposer.org/"}', + ) + ))); + } catch (TransportException $e) { + if (401 === $e->getCode()) { + $this->io->write('Invalid credentials.'); + continue; + } + + throw $e; + } + + $this->io->setAuthorization($this->originUrl, $contents['token'], 'x-oauth-basic'); + + return; + } + + throw new \RuntimeException("Invalid GitHub credentials 5 times in a row, aborting."); + } } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 3555557ee..f7f7f93f9 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -50,12 +50,13 @@ class RemoteFilesystem * @param string $fileUrl The file URL * @param string $fileName the local filename * @param boolean $progress Display the progression + * @param array $options Additional context options * * @return bool true */ - public function copy($originUrl, $fileUrl, $fileName, $progress = true) + public function copy($originUrl, $fileUrl, $fileName, $progress = true, $options = array()) { - $this->get($originUrl, $fileUrl, $fileName, $progress); + $this->get($originUrl, $fileUrl, $options, $fileName, $progress); return $this->result; } @@ -66,12 +67,13 @@ class RemoteFilesystem * @param string $originUrl The origin URL * @param string $fileUrl The file URL * @param boolean $progress Display the progression + * @param array $options Additional context options * * @return string The content */ - public function getContents($originUrl, $fileUrl, $progress = true) + public function getContents($originUrl, $fileUrl, $progress = true, $options = array()) { - $this->get($originUrl, $fileUrl, null, $progress); + $this->get($originUrl, $fileUrl, $options, null, $progress); return $this->result; } @@ -79,14 +81,15 @@ class RemoteFilesystem /** * Get file content or copy action. * - * @param string $originUrl The origin URL - * @param string $fileUrl The file URL - * @param string $fileName the local filename - * @param boolean $progress Display the progression + * @param string $originUrl The origin URL + * @param string $fileUrl The file URL + * @param array $additionalOptions context options + * @param string $fileName the local filename + * @param boolean $progress Display the progression * * @throws TransportException When the file could not be downloaded */ - protected function get($originUrl, $fileUrl, $fileName = null, $progress = true) + protected function get($originUrl, $fileUrl, $additionalOptions = array(), $fileName = null, $progress = true) { $this->bytesMax = 0; $this->result = null; @@ -96,7 +99,7 @@ class RemoteFilesystem $this->progress = $progress; $this->lastProgress = null; - $options = $this->getOptionsForUrl($originUrl); + $options = $this->getOptionsForUrl($originUrl, $additionalOptions); $ctx = StreamContextFactory::getContext($options, array('notification' => array($this, 'callbackGet'))); if ($this->progress) { @@ -110,11 +113,20 @@ class RemoteFilesystem } $errorMessage .= preg_replace('{^file_get_contents\(.*?\): }', '', $msg); }); - $result = file_get_contents($fileUrl, false, $ctx); + try { + $result = file_get_contents($fileUrl, false, $ctx); + } catch (\Exception $e) { + if ($e instanceof TransportException && !empty($http_response_header[0])) { + $e->setHeaders($http_response_header); + } + } if ($errorMessage && !ini_get('allow_url_fopen')) { $errorMessage = 'allow_url_fopen must be enabled in php.ini ('.$errorMessage.')'; } restore_error_handler(); + if (isset($e)) { + throw $e; + } // fix for 5.4.0 https://bugs.php.net/bug.php?id=61336 if (!empty($http_response_header[0]) && preg_match('{^HTTP/\S+ 404}i', $http_response_header[0])) { @@ -239,9 +251,9 @@ class RemoteFilesystem } } - protected function getOptionsForUrl($originUrl) + protected function getOptionsForUrl($originUrl, $additionalOptions) { - $options['http']['header'] = sprintf( + $header = sprintf( "User-Agent: Composer/%s (%s; %s; PHP %s.%s.%s)\r\n", Composer::VERSION, php_uname('s'), @@ -251,16 +263,22 @@ class RemoteFilesystem PHP_RELEASE_VERSION ); if (extension_loaded('zlib')) { - $options['http']['header'] .= 'Accept-Encoding: gzip'."\r\n"; + $header .= 'Accept-Encoding: gzip'."\r\n"; } if ($this->io->hasAuthorization($originUrl)) { $auth = $this->io->getAuthorization($originUrl); $authStr = base64_encode($auth['username'] . ':' . $auth['password']); - $options['http']['header'] .= "Authorization: Basic $authStr\r\n"; + $header .= "Authorization: Basic $authStr\r\n"; } - $options = array_replace_recursive($options, $this->options); + $options = array_replace_recursive($this->options, $additionalOptions); + + if (isset($options['http']['header'])) { + $options['http']['header'] = rtrim($options['http']['header'], "\r\n") . "\r\n" . $header; + } else { + $options['http']['header'] = $header; + } return $options; } From e410da786e267e7bb22070b42ffdbe3104dc3f9d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 18 Oct 2012 16:39:47 +0200 Subject: [PATCH 3/7] Add ConfigSourceInterface and matching class --- src/Composer/Config.php | 21 +++++ src/Composer/Config/ConfigSourceInterface.php | 29 +++++++ src/Composer/Config/JsonConfigSource.php | 80 +++++++++++++++++++ src/Composer/Factory.php | 2 + 4 files changed, 132 insertions(+) create mode 100644 src/Composer/Config/ConfigSourceInterface.php create mode 100644 src/Composer/Config/JsonConfigSource.php diff --git a/src/Composer/Config.php b/src/Composer/Config.php index ced0007c6..df8563e81 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -12,6 +12,8 @@ namespace Composer; +use Composer\Config\ConfigSourceInterface; + /** * @author Jordi Boggiano */ @@ -34,6 +36,7 @@ class Config private $config; private $repositories; + private $configSource; public function __construct() { @@ -42,6 +45,16 @@ class Config $this->repositories = static::$defaultRepositories; } + public function setConfigSource(ConfigSourceInterface $source) + { + $this->configSource = $source; + } + + public function getConfigSource() + { + return $this->configSource; + } + /** * Merges new config values with the existing ones (overriding) * @@ -110,6 +123,10 @@ class Config return rtrim($this->process($this->config[$key]), '/\\'); default: + if (!isset($this->config[$key])) { + return null; + } + return $this->process($this->config[$key]); } } @@ -135,6 +152,10 @@ class Config { $config = $this; + if (!is_string($value)) { + return $value; + } + return preg_replace_callback('#\{\$(.+)\}#', function ($match) use ($config) { return $config->get($match[1]); }, $value); diff --git a/src/Composer/Config/ConfigSourceInterface.php b/src/Composer/Config/ConfigSourceInterface.php new file mode 100644 index 000000000..08ac973a2 --- /dev/null +++ b/src/Composer/Config/ConfigSourceInterface.php @@ -0,0 +1,29 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Config; + +use Composer\Json\JsonManipulator; + +/** + * @author Jordi Boggiano + */ +interface ConfigSourceInterface +{ + public function addRepository($name, $config); + + public function removeRepository($name); + + public function addConfigSetting($name, $value); + + public function removeConfigSetting($name); +} diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php new file mode 100644 index 000000000..8de10a51e --- /dev/null +++ b/src/Composer/Config/JsonConfigSource.php @@ -0,0 +1,80 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Config; + +use Composer\Json\JsonManipulator; +use Composer\Json\JsonFile; + +/** + * @author Jordi Boggiano + */ +class JsonConfigSource implements ConfigSourceInterface +{ + private $file; + private $manipulator; + + public function __construct(JsonFile $file) + { + $this->file = $file; + } + + public function addRepository($name, $config) + { + return $this->manipulateJson('addRepository', $name, $config, function (&$config, $repo, $repoConfig) { + $config['repositories'][$repo] = $repoConfig; + }); + } + + public function removeRepository($name) + { + return $this->manipulateJson('removeRepository', $name, function (&$config, $repo) { + unset($config['repositories'][$repo]); + }); + } + + public function addConfigSetting($name, $value) + { + $this->manipulateJson('addConfigSetting', $name, $value, function (&$config, $key, $val) { + $config['config'][$key] = $val; + }); + } + + public function removeConfigSetting($name) + { + return $this->manipulateJson('removeConfigSetting', $name, function (&$config, $key) { + unset($config['config'][$key]); + }); + } + + protected function manipulateJson($method, $args, $fallback) + { + $args = func_get_args(); + // remove method & fallback + array_shift($args); + $fallback = array_pop($args); + + $contents = file_get_contents($this->file->getPath()); + $manipulator = new JsonManipulator($contents); + + // try to update cleanly + if (call_user_func_array(array($manipulator, $method), $args)) { + file_put_contents($this->file->getPath(), $manipulator->getContents()); + } else { + // on failed clean update, call the fallback and rewrite the whole file + $config = $this->file->read(); + array_unshift($args, $config); + call_user_func_array($fallback, $args); + $this->file->write($config); + } + } +} diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 64689833f..f9bc0d971 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -12,6 +12,7 @@ namespace Composer; +use Composer\Config\JsonConfigSource; use Composer\Json\JsonFile; use Composer\IO\IOInterface; use Composer\Repository\ComposerRepository; @@ -59,6 +60,7 @@ class Factory if ($file->exists()) { $config->merge($file->read()); } + $config->setConfigSource(new JsonConfigSource($file)); return $config; } From 503234451b087a719c30b5634a2f3eea5972cfc4 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 18 Oct 2012 16:40:12 +0200 Subject: [PATCH 4/7] Simplify ConfigCommand to use the JsonConfigSource --- src/Composer/Command/ConfigCommand.php | 67 ++++++++------------------ 1 file changed, 19 insertions(+), 48 deletions(-) diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index a539a2a5e..72b7ad548 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Config; +use Composer\Config\JsonConfigSource; use Composer\Factory; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; @@ -32,6 +33,11 @@ class ConfigCommand extends Command */ protected $configFile; + /** + * @var Composer\Config\JsonConfigSource + */ + protected $configSource; + /** * {@inheritDoc} */ @@ -94,11 +100,12 @@ EOT // Get the local composer.json, global config.json, or if the user // passed in a file to use - $this->configFile = $input->getOption('global') + $configFile = $input->getOption('global') ? (Factory::createConfig()->get('home') . '/config.json') : $input->getOption('file'); - $this->configFile = new JsonFile($this->configFile); + $this->configFile = new JsonFile($configFile); + $this->configSource = new JsonConfigSource($this->configFile); // initialize the global file if it's not there if ($input->getOption('global') && !$this->configFile->exists()) { @@ -161,25 +168,17 @@ EOT // handle repositories if (preg_match('/^repos?(?:itories)?\.(.+)/', $input->getArgument('setting-key'), $matches)) { if ($input->getOption('unset')) { - return $this->manipulateJson('removeRepository', $matches[1], function (&$config, $repo) { - unset($config['repositories'][$repo]); - }); + return $this->configSource->removeRepository($matches[1]); } if (2 !== count($values)) { throw new \RuntimeException('You must pass the type and a url. Example: php composer.phar config repositories.foo vcs http://bar.com'); } - return $this->manipulateJson( - 'addRepository', - $matches[1], - array( - 'type' => $values[0], - 'url' => $values[1], - ), function (&$config, $repo, $repoConfig) { - $config['repositories'][$repo] = $repoConfig; - } - ); + return $this->configSource->addRepository($matches[1], array( + 'type' => $values[0], + 'url' => $values[1], + )); } // handle config values @@ -217,9 +216,7 @@ EOT foreach ($uniqueConfigValues as $name => $callbacks) { if ($settingKey === $name) { if ($input->getOption('unset')) { - return $this->manipulateJson('removeConfigSetting', $settingKey, function (&$config, $key) { - unset($config['config'][$key]); - }); + return $this->configSource->removeConfigSetting($settingKey); } list($validator, $normalizer) = $callbacks; @@ -234,18 +231,14 @@ EOT )); } - return $this->manipulateJson('addConfigSetting', $settingKey, $normalizer($values[0]), function (&$config, $key, $val) { - $config['config'][$key] = $val; - }); + return $this->configSource->addConfigSetting($settingKey, $normalizer($values[0])); } } foreach ($multiConfigValues as $name => $callbacks) { if ($settingKey === $name) { if ($input->getOption('unset')) { - return $this->manipulateJson('removeConfigSetting', $settingKey, function (&$config, $key) { - unset($config['config'][$key]); - }); + return $this->configSource->removeConfigSetting($settingKey); } list($validator, $normalizer) = $callbacks; @@ -256,33 +249,11 @@ EOT )); } - return $this->manipulateJson('addConfigSetting', $settingKey, $normalizer($values), function (&$config, $key, $val) { - $config['config'][$key] = $val; - }); + return $this->configSource->addConfigSetting($settingKey, $normalizer($values)); } } - } - protected function manipulateJson($method, $args, $fallback) - { - $args = func_get_args(); - // remove method & fallback - array_shift($args); - $fallback = array_pop($args); - - $contents = file_get_contents($this->configFile->getPath()); - $manipulator = new JsonManipulator($contents); - - // try to update cleanly - if (call_user_func_array(array($manipulator, $method), $args)) { - file_put_contents($this->configFile->getPath(), $manipulator->getContents()); - } else { - // on failed clean update, call the fallback and rewrite the whole file - $config = $this->configFile->read(); - array_unshift($args, $config); - call_user_func_array($fallback, $args); - $this->configFile->write($config); - } + throw new \InvalidArgumentException('Setting '.$settingKey.' does not exist or is not supported by this command'); } /** From a9811c4e401476a305490cbc53f28bc92cb4b517 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 18 Oct 2012 16:40:36 +0200 Subject: [PATCH 5/7] Store and reload the github token to/from the config --- src/Composer/Factory.php | 7 +++++++ src/Composer/Repository/Vcs/GitHubDriver.php | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index f9bc0d971..4862ff995 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -140,6 +140,13 @@ class Factory $config = static::createConfig(); $config->merge($localConfig); + // reload oauth token from config if available + if ($tokens = $config->get('github-oauth')) { + foreach ($tokens as $domain => $token) { + $io->setAuthorization($domain, $token, 'x-oauth-basic'); + } + } + $vendorDir = $config->get('vendor-dir'); $binDir = $config->get('bin-dir'); diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index b5f8274bf..1f09a595a 100755 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -373,6 +373,11 @@ class GitHubDriver extends VcsDriver $this->io->setAuthorization($this->originUrl, $contents['token'], 'x-oauth-basic'); + // store value in user config + $githubTokens = $this->config->get('github-oauth') ?: array(); + $githubTokens[$this->originUrl] = $contents['token']; + $this->config->getConfigSource()->addConfigSetting('github-oauth', $githubTokens); + return; } From 32282e7461bb12ff2e8c7684ad896cbb471200a2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 18 Oct 2012 17:08:34 +0200 Subject: [PATCH 6/7] Add hostname to the OAuth app name --- src/Composer/Repository/Vcs/GitHubDriver.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 1f09a595a..bbe75f70f 100755 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -355,11 +355,21 @@ class GitHubDriver extends VcsDriver $password = $this->io->askAndHideAnswer('Password: '); $this->io->setAuthorization($this->originUrl, $username, $password); + // build up OAuth app name + $appName = 'Composer'; + if (0 === $this->process->execute('hostname', $output)) { + $appName .= ' on ' . trim($output); + } + $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($this->originUrl, 'https://api.github.com/authorizations', false, array( 'http' => array( 'method' => 'POST', 'header' => "Content-Type: application/json\r\n", - 'content' => '{"scopes":["repo"],"note":"Composer","note_url":"https://getcomposer.org/"}', + 'content' => json_encode(array( + 'scopes' => array('repo'), + 'note' => $appName, + 'note_url' => 'https://getcomposer.org/', + )), ) ))); } catch (TransportException $e) { From 7d4857fc5bcd08e4748944770e89ed6f724f7b48 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 18 Oct 2012 17:09:23 +0200 Subject: [PATCH 7/7] Catch all 4**/5** responses --- src/Composer/Util/RemoteFilesystem.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index f7f7f93f9..065ea3bee 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -107,6 +107,7 @@ class RemoteFilesystem } $errorMessage = ''; + $errorCode = 0; set_error_handler(function ($code, $msg) use (&$errorMessage) { if ($errorMessage) { $errorMessage .= "\n"; @@ -129,8 +130,9 @@ class RemoteFilesystem } // fix for 5.4.0 https://bugs.php.net/bug.php?id=61336 - if (!empty($http_response_header[0]) && preg_match('{^HTTP/\S+ 404}i', $http_response_header[0])) { + if (!empty($http_response_header[0]) && preg_match('{^HTTP/\S+ ([45]\d\d)}i', $http_response_header[0], $match)) { $result = false; + $errorCode = $match[1]; } // decode gzip @@ -181,7 +183,12 @@ class RemoteFilesystem } if (false === $this->result) { - throw new TransportException('The "'.$fileUrl.'" file could not be downloaded: '.$errorMessage); + $e = new TransportException('The "'.$fileUrl.'" file could not be downloaded: '.$errorMessage, $errorCode); + if (!empty($http_response_header[0])) { + $e->setHeaders($http_response_header); + } + + throw $e; } }