Merge branch 'oauth', fixes #423
commit
1df17a4b51
|
@ -17,6 +17,7 @@ use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Composer\Config;
|
use Composer\Config;
|
||||||
|
use Composer\Config\JsonConfigSource;
|
||||||
use Composer\Factory;
|
use Composer\Factory;
|
||||||
use Composer\Json\JsonFile;
|
use Composer\Json\JsonFile;
|
||||||
use Composer\Json\JsonManipulator;
|
use Composer\Json\JsonManipulator;
|
||||||
|
@ -32,6 +33,11 @@ class ConfigCommand extends Command
|
||||||
*/
|
*/
|
||||||
protected $configFile;
|
protected $configFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Composer\Config\JsonConfigSource
|
||||||
|
*/
|
||||||
|
protected $configSource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
|
@ -94,11 +100,12 @@ EOT
|
||||||
|
|
||||||
// Get the local composer.json, global config.json, or if the user
|
// Get the local composer.json, global config.json, or if the user
|
||||||
// passed in a file to use
|
// passed in a file to use
|
||||||
$this->configFile = $input->getOption('global')
|
$configFile = $input->getOption('global')
|
||||||
? (Factory::createConfig()->get('home') . '/config.json')
|
? (Factory::createConfig()->get('home') . '/config.json')
|
||||||
: $input->getOption('file');
|
: $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
|
// initialize the global file if it's not there
|
||||||
if ($input->getOption('global') && !$this->configFile->exists()) {
|
if ($input->getOption('global') && !$this->configFile->exists()) {
|
||||||
|
@ -161,25 +168,17 @@ EOT
|
||||||
// handle repositories
|
// handle repositories
|
||||||
if (preg_match('/^repos?(?:itories)?\.(.+)/', $input->getArgument('setting-key'), $matches)) {
|
if (preg_match('/^repos?(?:itories)?\.(.+)/', $input->getArgument('setting-key'), $matches)) {
|
||||||
if ($input->getOption('unset')) {
|
if ($input->getOption('unset')) {
|
||||||
return $this->manipulateJson('removeRepository', $matches[1], function (&$config, $repo) {
|
return $this->configSource->removeRepository($matches[1]);
|
||||||
unset($config['repositories'][$repo]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (2 !== count($values)) {
|
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');
|
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(
|
return $this->configSource->addRepository($matches[1], array(
|
||||||
'addRepository',
|
|
||||||
$matches[1],
|
|
||||||
array(
|
|
||||||
'type' => $values[0],
|
'type' => $values[0],
|
||||||
'url' => $values[1],
|
'url' => $values[1],
|
||||||
), function (&$config, $repo, $repoConfig) {
|
));
|
||||||
$config['repositories'][$repo] = $repoConfig;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle config values
|
// handle config values
|
||||||
|
@ -217,9 +216,7 @@ EOT
|
||||||
foreach ($uniqueConfigValues as $name => $callbacks) {
|
foreach ($uniqueConfigValues as $name => $callbacks) {
|
||||||
if ($settingKey === $name) {
|
if ($settingKey === $name) {
|
||||||
if ($input->getOption('unset')) {
|
if ($input->getOption('unset')) {
|
||||||
return $this->manipulateJson('removeConfigSetting', $settingKey, function (&$config, $key) {
|
return $this->configSource->removeConfigSetting($settingKey);
|
||||||
unset($config['config'][$key]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
list($validator, $normalizer) = $callbacks;
|
list($validator, $normalizer) = $callbacks;
|
||||||
|
@ -234,18 +231,14 @@ EOT
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manipulateJson('addConfigSetting', $settingKey, $normalizer($values[0]), function (&$config, $key, $val) {
|
return $this->configSource->addConfigSetting($settingKey, $normalizer($values[0]));
|
||||||
$config['config'][$key] = $val;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($multiConfigValues as $name => $callbacks) {
|
foreach ($multiConfigValues as $name => $callbacks) {
|
||||||
if ($settingKey === $name) {
|
if ($settingKey === $name) {
|
||||||
if ($input->getOption('unset')) {
|
if ($input->getOption('unset')) {
|
||||||
return $this->manipulateJson('removeConfigSetting', $settingKey, function (&$config, $key) {
|
return $this->configSource->removeConfigSetting($settingKey);
|
||||||
unset($config['config'][$key]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
list($validator, $normalizer) = $callbacks;
|
list($validator, $normalizer) = $callbacks;
|
||||||
|
@ -256,33 +249,11 @@ EOT
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manipulateJson('addConfigSetting', $settingKey, $normalizer($values), function (&$config, $key, $val) {
|
return $this->configSource->addConfigSetting($settingKey, $normalizer($values));
|
||||||
$config['config'][$key] = $val;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function manipulateJson($method, $args, $fallback)
|
throw new \InvalidArgumentException('Setting '.$settingKey.' does not exist or is not supported by this command');
|
||||||
{
|
|
||||||
$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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
|
|
||||||
namespace Composer;
|
namespace Composer;
|
||||||
|
|
||||||
|
use Composer\Config\ConfigSourceInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||||
*/
|
*/
|
||||||
|
@ -34,6 +36,7 @@ class Config
|
||||||
|
|
||||||
private $config;
|
private $config;
|
||||||
private $repositories;
|
private $repositories;
|
||||||
|
private $configSource;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
@ -42,6 +45,16 @@ class Config
|
||||||
$this->repositories = static::$defaultRepositories;
|
$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)
|
* Merges new config values with the existing ones (overriding)
|
||||||
*
|
*
|
||||||
|
@ -110,6 +123,10 @@ class Config
|
||||||
return rtrim($this->process($this->config[$key]), '/\\');
|
return rtrim($this->process($this->config[$key]), '/\\');
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
if (!isset($this->config[$key])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->process($this->config[$key]);
|
return $this->process($this->config[$key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,6 +152,10 @@ class Config
|
||||||
{
|
{
|
||||||
$config = $this;
|
$config = $this;
|
||||||
|
|
||||||
|
if (!is_string($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
return preg_replace_callback('#\{\$(.+)\}#', function ($match) use ($config) {
|
return preg_replace_callback('#\{\$(.+)\}#', function ($match) use ($config) {
|
||||||
return $config->get($match[1]);
|
return $config->get($match[1]);
|
||||||
}, $value);
|
}, $value);
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* 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 <j.boggiano@seld.be>
|
||||||
|
*/
|
||||||
|
interface ConfigSourceInterface
|
||||||
|
{
|
||||||
|
public function addRepository($name, $config);
|
||||||
|
|
||||||
|
public function removeRepository($name);
|
||||||
|
|
||||||
|
public function addConfigSetting($name, $value);
|
||||||
|
|
||||||
|
public function removeConfigSetting($name);
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* 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 <j.boggiano@seld.be>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,11 +51,13 @@ class GitDownloader extends VcsDownloader
|
||||||
$this->io->write(" Checking out ".$ref);
|
$this->io->write(" Checking out ".$ref);
|
||||||
$command = 'cd %s && git remote set-url composer %s && git fetch composer && git fetch --tags composer';
|
$command = 'cd %s && git remote set-url composer %s && git fetch composer && git fetch --tags composer';
|
||||||
|
|
||||||
|
if (!$this->io->hasAuthorization('github.com')) {
|
||||||
// capture username/password from github URL if there is one
|
// capture username/password from github URL if there is one
|
||||||
$this->process->execute(sprintf('cd %s && git remote -v', escapeshellarg($path)), $output);
|
$this->process->execute(sprintf('cd %s && git remote -v', escapeshellarg($path)), $output);
|
||||||
if (preg_match('{^composer\s+https://(.+):(.+)@github.com/}im', $output, $match)) {
|
if (preg_match('{^composer\s+https://(.+):(.+)@github.com/}im', $output, $match)) {
|
||||||
$this->io->setAuthorization('github.com', $match[1], $match[2]);
|
$this->io->setAuthorization('github.com', $match[1], $match[2]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$commandCallable = function($url) use ($ref, $path, $command) {
|
$commandCallable = function($url) use ($ref, $path, $command) {
|
||||||
return sprintf($command, escapeshellarg($path), escapeshellarg($url), escapeshellarg($ref));
|
return sprintf($command, escapeshellarg($path), escapeshellarg($url), escapeshellarg($ref));
|
||||||
|
@ -327,7 +329,7 @@ class GitDownloader extends VcsDownloader
|
||||||
|
|
||||||
protected function sanitizeUrl($message)
|
protected function sanitizeUrl($message)
|
||||||
{
|
{
|
||||||
return preg_match('{://(.+?):.+?@}', '://$1:***@', $message);
|
return preg_replace('{://(.+?):.+?@}', '://$1:***@', $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setPushUrl(PackageInterface $package, $path)
|
protected function setPushUrl(PackageInterface $package, $path)
|
||||||
|
|
|
@ -17,4 +17,15 @@ namespace Composer\Downloader;
|
||||||
*/
|
*/
|
||||||
class TransportException extends \Exception
|
class TransportException extends \Exception
|
||||||
{
|
{
|
||||||
|
protected $headers;
|
||||||
|
|
||||||
|
public function setHeaders($headers)
|
||||||
|
{
|
||||||
|
$this->headers = $headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeaders()
|
||||||
|
{
|
||||||
|
return $this->headers;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
|
|
||||||
namespace Composer;
|
namespace Composer;
|
||||||
|
|
||||||
|
use Composer\Config\JsonConfigSource;
|
||||||
use Composer\Json\JsonFile;
|
use Composer\Json\JsonFile;
|
||||||
use Composer\IO\IOInterface;
|
use Composer\IO\IOInterface;
|
||||||
use Composer\Repository\ComposerRepository;
|
use Composer\Repository\ComposerRepository;
|
||||||
|
@ -59,6 +60,7 @@ class Factory
|
||||||
if ($file->exists()) {
|
if ($file->exists()) {
|
||||||
$config->merge($file->read());
|
$config->merge($file->read());
|
||||||
}
|
}
|
||||||
|
$config->setConfigSource(new JsonConfigSource($file));
|
||||||
|
|
||||||
return $config;
|
return $config;
|
||||||
}
|
}
|
||||||
|
@ -138,6 +140,13 @@ class Factory
|
||||||
$config = static::createConfig();
|
$config = static::createConfig();
|
||||||
$config->merge($localConfig);
|
$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');
|
$vendorDir = $config->get('vendor-dir');
|
||||||
$binDir = $config->get('bin-dir');
|
$binDir = $config->get('bin-dir');
|
||||||
|
|
||||||
|
|
|
@ -235,6 +235,62 @@ class GitHubDriver extends VcsDriver
|
||||||
return 'git@github.com:'.$this->owner.'/'.$this->repository.'.git';
|
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 (<info>'.$this->url.'</info>):');
|
||||||
|
$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('<error>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</error>');
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->io->write('API limit exhausted. Enter your GitHub credentials to get a larger API limit (<info>'.$this->url.'</info>):');
|
||||||
|
$this->authorizeOauth();
|
||||||
|
|
||||||
|
return parent::getContents($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rateLimited) {
|
||||||
|
$this->io->write('<error>GitHub API limit exhausted. You are already authorized so you will have to wait a while before doing more requests</error>');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch root identifier from GitHub
|
* Fetch root identifier from GitHub
|
||||||
*
|
*
|
||||||
|
@ -243,13 +299,13 @@ class GitHubDriver extends VcsDriver
|
||||||
protected function fetchRootIdentifier()
|
protected function fetchRootIdentifier()
|
||||||
{
|
{
|
||||||
$repoDataUrl = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository;
|
$repoDataUrl = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository;
|
||||||
$attemptCounter = 0;
|
|
||||||
while (null === $this->rootIdentifier) {
|
$repoData = JsonFile::parseJson($this->getContents($repoDataUrl, true), $repoDataUrl);
|
||||||
if (5 == $attemptCounter++) {
|
if (null === $repoData && null !== $this->gitDriver) {
|
||||||
throw new \RuntimeException("Either you have entered invalid credentials or this GitHub repository does not exists (404)");
|
return;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
$repoData = JsonFile::parseJson($this->getContents($repoDataUrl), $repoDataUrl);
|
$this->isPrivate = !empty($repoData['private']);
|
||||||
if (isset($repoData['default_branch'])) {
|
if (isset($repoData['default_branch'])) {
|
||||||
$this->rootIdentifier = $repoData['default_branch'];
|
$this->rootIdentifier = $repoData['default_branch'];
|
||||||
} elseif (isset($repoData['master_branch'])) {
|
} elseif (isset($repoData['master_branch'])) {
|
||||||
|
@ -258,10 +314,10 @@ class GitHubDriver extends VcsDriver
|
||||||
$this->rootIdentifier = 'master';
|
$this->rootIdentifier = 'master';
|
||||||
}
|
}
|
||||||
$this->hasIssues = !empty($repoData['has_issues']);
|
$this->hasIssues = !empty($repoData['has_issues']);
|
||||||
} catch (TransportException $e) {
|
}
|
||||||
switch ($e->getCode()) {
|
|
||||||
case 401:
|
protected function attemptCloneFallback()
|
||||||
case 404:
|
{
|
||||||
$this->isPrivate = true;
|
$this->isPrivate = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -281,22 +337,60 @@ class GitHubDriver extends VcsDriver
|
||||||
return;
|
return;
|
||||||
} catch (\RuntimeException $e) {
|
} catch (\RuntimeException $e) {
|
||||||
$this->gitDriver = null;
|
$this->gitDriver = null;
|
||||||
if (!$this->io->isInteractive()) {
|
|
||||||
$this->io->write('<error>Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your username and password</error>');
|
$this->io->write('<error>Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your GitHub credentials</error>');
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->io->write('Authentication required (<info>'.$this->url.'</info>):');
|
|
||||||
|
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: ');
|
$username = $this->io->ask('Username: ');
|
||||||
$password = $this->io->askAndHideAnswer('Password: ');
|
$password = $this->io->askAndHideAnswer('Password: ');
|
||||||
$this->io->setAuthorization($this->originUrl, $username, $password);
|
$this->io->setAuthorization($this->originUrl, $username, $password);
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
// 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' => json_encode(array(
|
||||||
|
'scopes' => array('repo'),
|
||||||
|
'note' => $appName,
|
||||||
|
'note_url' => 'https://getcomposer.org/',
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
)));
|
||||||
|
} catch (TransportException $e) {
|
||||||
|
if (401 === $e->getCode()) {
|
||||||
|
$this->io->write('Invalid credentials.');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
throw $e;
|
throw $e;
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
}
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \RuntimeException("Invalid GitHub credentials 5 times in a row, aborting.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,12 +50,13 @@ class RemoteFilesystem
|
||||||
* @param string $fileUrl The file URL
|
* @param string $fileUrl The file URL
|
||||||
* @param string $fileName the local filename
|
* @param string $fileName the local filename
|
||||||
* @param boolean $progress Display the progression
|
* @param boolean $progress Display the progression
|
||||||
|
* @param array $options Additional context options
|
||||||
*
|
*
|
||||||
* @return bool true
|
* @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;
|
return $this->result;
|
||||||
}
|
}
|
||||||
|
@ -66,12 +67,13 @@ class RemoteFilesystem
|
||||||
* @param string $originUrl The origin URL
|
* @param string $originUrl The origin URL
|
||||||
* @param string $fileUrl The file URL
|
* @param string $fileUrl The file URL
|
||||||
* @param boolean $progress Display the progression
|
* @param boolean $progress Display the progression
|
||||||
|
* @param array $options Additional context options
|
||||||
*
|
*
|
||||||
* @return string The content
|
* @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;
|
return $this->result;
|
||||||
}
|
}
|
||||||
|
@ -81,12 +83,13 @@ class RemoteFilesystem
|
||||||
*
|
*
|
||||||
* @param string $originUrl The origin URL
|
* @param string $originUrl The origin URL
|
||||||
* @param string $fileUrl The file URL
|
* @param string $fileUrl The file URL
|
||||||
|
* @param array $additionalOptions context options
|
||||||
* @param string $fileName the local filename
|
* @param string $fileName the local filename
|
||||||
* @param boolean $progress Display the progression
|
* @param boolean $progress Display the progression
|
||||||
*
|
*
|
||||||
* @throws TransportException When the file could not be downloaded
|
* @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->bytesMax = 0;
|
||||||
$this->result = null;
|
$this->result = null;
|
||||||
|
@ -96,7 +99,7 @@ class RemoteFilesystem
|
||||||
$this->progress = $progress;
|
$this->progress = $progress;
|
||||||
$this->lastProgress = null;
|
$this->lastProgress = null;
|
||||||
|
|
||||||
$options = $this->getOptionsForUrl($originUrl);
|
$options = $this->getOptionsForUrl($originUrl, $additionalOptions);
|
||||||
$ctx = StreamContextFactory::getContext($options, array('notification' => array($this, 'callbackGet')));
|
$ctx = StreamContextFactory::getContext($options, array('notification' => array($this, 'callbackGet')));
|
||||||
|
|
||||||
if ($this->progress) {
|
if ($this->progress) {
|
||||||
|
@ -104,21 +107,32 @@ class RemoteFilesystem
|
||||||
}
|
}
|
||||||
|
|
||||||
$errorMessage = '';
|
$errorMessage = '';
|
||||||
|
$errorCode = 0;
|
||||||
set_error_handler(function ($code, $msg) use (&$errorMessage) {
|
set_error_handler(function ($code, $msg) use (&$errorMessage) {
|
||||||
if ($errorMessage) {
|
if ($errorMessage) {
|
||||||
$errorMessage .= "\n";
|
$errorMessage .= "\n";
|
||||||
}
|
}
|
||||||
$errorMessage .= preg_replace('{^file_get_contents\(.*?\): }', '', $msg);
|
$errorMessage .= preg_replace('{^file_get_contents\(.*?\): }', '', $msg);
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
$result = file_get_contents($fileUrl, false, $ctx);
|
$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')) {
|
if ($errorMessage && !ini_get('allow_url_fopen')) {
|
||||||
$errorMessage = 'allow_url_fopen must be enabled in php.ini ('.$errorMessage.')';
|
$errorMessage = 'allow_url_fopen must be enabled in php.ini ('.$errorMessage.')';
|
||||||
}
|
}
|
||||||
restore_error_handler();
|
restore_error_handler();
|
||||||
|
if (isset($e)) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
// fix for 5.4.0 https://bugs.php.net/bug.php?id=61336
|
// 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;
|
$result = false;
|
||||||
|
$errorCode = $match[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode gzip
|
// decode gzip
|
||||||
|
@ -169,7 +183,12 @@ class RemoteFilesystem
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === $this->result) {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,6 +226,12 @@ class RemoteFilesystem
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case STREAM_NOTIFY_AUTH_RESULT:
|
||||||
|
if (403 === $messageCode) {
|
||||||
|
throw new TransportException($message, 403);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case STREAM_NOTIFY_FILE_SIZE_IS:
|
case STREAM_NOTIFY_FILE_SIZE_IS:
|
||||||
if ($this->bytesMax < $bytesMax) {
|
if ($this->bytesMax < $bytesMax) {
|
||||||
$this->bytesMax = $bytesMax;
|
$this->bytesMax = $bytesMax;
|
||||||
|
@ -233,9 +258,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",
|
"User-Agent: Composer/%s (%s; %s; PHP %s.%s.%s)\r\n",
|
||||||
Composer::VERSION,
|
Composer::VERSION,
|
||||||
php_uname('s'),
|
php_uname('s'),
|
||||||
|
@ -245,16 +270,22 @@ class RemoteFilesystem
|
||||||
PHP_RELEASE_VERSION
|
PHP_RELEASE_VERSION
|
||||||
);
|
);
|
||||||
if (extension_loaded('zlib')) {
|
if (extension_loaded('zlib')) {
|
||||||
$options['http']['header'] .= 'Accept-Encoding: gzip'."\r\n";
|
$header .= 'Accept-Encoding: gzip'."\r\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->io->hasAuthorization($originUrl)) {
|
if ($this->io->hasAuthorization($originUrl)) {
|
||||||
$auth = $this->io->getAuthorization($originUrl);
|
$auth = $this->io->getAuthorization($originUrl);
|
||||||
$authStr = base64_encode($auth['username'] . ':' . $auth['password']);
|
$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;
|
return $options;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue