Browse Source

Refactor OAuth acquisition code to generalize it

Jordi Boggiano 12 years ago
parent
commit
39e69a3b12

+ 17 - 2
src/Composer/Downloader/FileDownloader.php

@@ -12,10 +12,12 @@
 
 namespace Composer\Downloader;
 
+use Composer\Config;
 use Composer\IO\IOInterface;
 use Composer\Package\PackageInterface;
 use Composer\Package\Version\VersionParser;
 use Composer\Util\Filesystem;
+use Composer\Util\GitHub;
 use Composer\Util\RemoteFilesystem;
 
 /**
@@ -28,6 +30,7 @@ use Composer\Util\RemoteFilesystem;
 class FileDownloader implements DownloaderInterface
 {
     protected $io;
+    protected $config;
     protected $rfs;
     protected $filesystem;
 
@@ -36,9 +39,10 @@ class FileDownloader implements DownloaderInterface
      *
      * @param IOInterface $io The IO instance
      */
-    public function __construct(IOInterface $io, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
+    public function __construct(IOInterface $io, Config $config, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
     {
         $this->io = $io;
+        $this->config = $config;
         $this->rfs = $rfs ?: new RemoteFilesystem($io);
         $this->filesystem = $filesystem ?: new Filesystem();
     }
@@ -70,7 +74,18 @@ class FileDownloader implements DownloaderInterface
         $processUrl = $this->processUrl($package, $url);
 
         try {
-            $this->rfs->copy(parse_url($processUrl, PHP_URL_HOST), $processUrl, $fileName);
+            try {
+                $this->rfs->copy(parse_url($processUrl, PHP_URL_HOST), $processUrl, $fileName);
+            } catch (TransportException $e) {
+                if (404 === $e->getCode() && 'github.com' === parse_url($processUrl, PHP_URL_HOST)) {
+                    $message = "\n".'Could not fetch '.$processUrl.', enter your GitHub credentials to access private repos';
+                    $gitHubUtil = new GitHub($this->io, $this->config, null, $this->rfs);
+                    $gitHubUtil->authorizeOAuth('github.com', $message);
+                    $this->rfs->copy(parse_url($processUrl, PHP_URL_HOST), $processUrl, $fileName);
+                } else {
+                    throw $e;
+                }
+            }
 
             if (!file_exists($fileName)) {
                 throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'

+ 14 - 24
src/Composer/Downloader/GitDownloader.php

@@ -13,6 +13,7 @@
 namespace Composer\Downloader;
 
 use Composer\Package\PackageInterface;
+use Composer\Util\GitHub;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -283,32 +284,21 @@ class GitDownloader extends VcsDownloader
 
         $command = call_user_func($commandCallable, $url);
         if (0 !== $this->process->execute($command, $handler)) {
-            if (preg_match('{^git@github.com:(.+?)\.git$}i', $url, $match) && $this->io->isInteractive()) {
-                // private github repository without git access, try https with auth
-                $retries = 3;
-                $retrying = false;
-                do {
-                    if ($retrying) {
-                        $this->io->write('Invalid credentials');
-                    }
-                    if (!$this->io->hasAuthorization('github.com') || $retrying) {
-                        $username = $this->io->ask('Username: ');
-                        $password = $this->io->askAndHideAnswer('Password: ');
-                        $this->io->setAuthorization('github.com', $username, $password);
-                    }
+            // private github repository without git access, try https with auth
+            if (preg_match('{^git@(github.com):(.+?)\.git$}i', $url, $match) && $this->io->isInteractive()) {
+                if (!$this->io->hasAuthorization($match[1])) {
+                    $message = 'Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos';
+                    $gitHubUtil = new GitHub($this->io, $this->config, $this->process);
+                    $gitHubUtil->authorizeOAuth($match[1], $message);
+                }
 
-                    $auth = $this->io->getAuthorization('github.com');
-                    $url = 'https://'.$auth['username'] . ':' . $auth['password'] . '@github.com/'.$match[1].'.git';
+                $auth = $this->io->getAuthorization($match[1]);
+                $url = 'https://'.$auth['username'] . ':' . $auth['password'] . '@'.$match[1].'/'.$match[2].'.git';
 
-                    $command = call_user_func($commandCallable, $url);
-                    if (0 === $this->process->execute($command, $handler)) {
-                        return;
-                    }
-                    if (null !== $path) {
-                        $this->filesystem->removeDirectory($path);
-                    }
-                    $retrying = true;
-                } while (--$retries);
+                $command = call_user_func($commandCallable, $url);
+                if (0 === $this->process->execute($command, $handler)) {
+                    return;
+                }
             }
 
             if (null !== $path) {

+ 3 - 2
src/Composer/Downloader/ZipDownloader.php

@@ -12,6 +12,7 @@
 
 namespace Composer\Downloader;
 
+use Composer\Config;
 use Composer\Util\ProcessExecutor;
 use Composer\IO\IOInterface;
 use ZipArchive;
@@ -23,10 +24,10 @@ class ZipDownloader extends ArchiveDownloader
 {
     protected $process;
 
-    public function __construct(IOInterface $io, ProcessExecutor $process = null)
+    public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null)
     {
         $this->process = $process ?: new ProcessExecutor;
-        parent::__construct($io);
+        parent::__construct($io, $config);
     }
 
     protected function extract($file, $path)

+ 4 - 4
src/Composer/Factory.php

@@ -234,10 +234,10 @@ class Factory
         $dm->setDownloader('git', new Downloader\GitDownloader($io, $config));
         $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config));
         $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config));
-        $dm->setDownloader('zip', new Downloader\ZipDownloader($io));
-        $dm->setDownloader('tar', new Downloader\TarDownloader($io));
-        $dm->setDownloader('phar', new Downloader\PharDownloader($io));
-        $dm->setDownloader('file', new Downloader\FileDownloader($io));
+        $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config));
+        $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config));
+        $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config));
+        $dm->setDownloader('file', new Downloader\FileDownloader($io, $config));
 
         return $dm;
     }

+ 6 - 59
src/Composer/Repository/Vcs/GitHubDriver.php

@@ -17,6 +17,7 @@ use Composer\Json\JsonFile;
 use Composer\Cache;
 use Composer\IO\IOInterface;
 use Composer\Util\RemoteFilesystem;
+use Composer\Util\GitHub;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -255,8 +256,7 @@ class GitHubDriver extends VcsDriver
                         return $this->attemptCloneFallback($e);
                     }
 
-                    $this->io->write('Your GitHub credentials are required to fetch private repository metadata (<info>'.$this->url.'</info>):');
-                    $this->authorizeOauth();
+                    $this->authorizeOAuth('Your GitHub credentials are required to fetch private repository metadata (<info>'.$this->url.'</info>)');
 
                     return parent::getContents($url);
 
@@ -278,8 +278,7 @@ class GitHubDriver extends VcsDriver
                             throw $e;
                         }
 
-                        $this->io->write('API limit exhausted. Enter your GitHub credentials to get a larger API limit (<info>'.$this->url.'</info>):');
-                        $this->authorizeOauth();
+                        $this->authorizeOAuth('API limit exhausted. Enter your GitHub credentials to get a larger API limit (<info>'.$this->url.'</info>)');
 
                         return parent::getContents($url);
                     }
@@ -348,61 +347,9 @@ class GitHubDriver extends VcsDriver
         }
     }
 
-    protected function authorizeOAuth()
+    protected function authorizeOAuth($message)
     {
-        // If available use token from git config
-        if (0 === $this->process->execute('git config github.accesstoken', $output)) {
-            $this->io->write('Using Github OAuth token stored in git config (github.accesstoken)');
-            $this->io->setAuthorization($this->originUrl, $output[0], 'x-oauth-basic');
-            return;
-        }
-
-        $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);
-
-                // 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;
-            }
-
-            $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.");
+        $gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->remoteFilesystem);
+        $gitHubUtil->authorizeOAuth($this->originUrl, $message);
     }
 }

+ 6 - 6
src/Composer/Repository/Vcs/VcsDriver.php

@@ -36,13 +36,13 @@ abstract class VcsDriver implements VcsDriverInterface
     /**
      * Constructor.
      *
-     * @param array           $repoConfig       The repository configuration
-     * @param IOInterface     $io               The IO instance
-     * @param Config          $config           The composer configuration
-     * @param ProcessExecutor $process          Process instance, injectable for mocking
-     * @param callable        $remoteFilesystem Remote Filesystem, injectable for mocking
+     * @param array            $repoConfig       The repository configuration
+     * @param IOInterface      $io               The IO instance
+     * @param Config           $config           The composer configuration
+     * @param ProcessExecutor  $process          Process instance, injectable for mocking
+     * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking
      */
-    final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, $remoteFilesystem = null)
+    final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null)
     {
         $this->url = $repoConfig['url'];
         $this->originUrl = $repoConfig['url'];

+ 113 - 0
src/Composer/Util/GitHub.php

@@ -0,0 +1,113 @@
+<?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\Util;
+
+use Composer\IO\IOInterface;
+use Composer\Config;
+use Composer\Downloader\TransportException;
+use Composer\Json\JsonFile;
+
+/**
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class GitHub
+{
+    protected $io;
+    protected $config;
+    protected $process;
+    protected $remoteFilesystem;
+
+    /**
+     * Constructor.
+     *
+     * @param IOInterface      $io               The IO instance
+     * @param Config           $config           The composer configuration
+     * @param ProcessExecutor  $process          Process instance, injectable for mocking
+     * @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking
+     */
+    public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null)
+    {
+        $this->io = $io;
+        $this->config = $config;
+        $this->process = $process ?: new ProcessExecutor;
+        $this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io);
+    }
+
+    /**
+     * Authorizes a GitHub domain via OAuth
+     *
+     * @param string $originUrl The host this GitHub instance is located at
+     * @param string $message   The reason this authorization is required
+     */
+    public function authorizeOAuth($originUrl, $message = null)
+    {
+        // if available use token from git config
+        if (0 === $this->process->execute('git config github.accesstoken', $output)) {
+            $this->io->write('Using Github OAuth token stored in git config (github.accesstoken)');
+            $this->io->setAuthorization($originUrl, trim($output), 'x-oauth-basic');
+
+            return;
+        }
+
+        $attemptCounter = 0;
+
+        if ($message) {
+            $this->io->write($message);
+        }
+        $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($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($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 (in_array($e->getCode(), array(403, 401))) {
+                    $this->io->write('Invalid credentials.');
+                    continue;
+                }
+
+                throw $e;
+            }
+
+            $this->io->setAuthorization($originUrl, $contents['token'], 'x-oauth-basic');
+
+            // store value in user config
+            $githubTokens = $this->config->get('github-oauth') ?: array();
+            $githubTokens[$originUrl] = $contents['token'];
+            $this->config->getConfigSource()->addConfigSetting('github-oauth', $githubTokens);
+
+            return;
+        }
+
+        throw new \RuntimeException("Invalid GitHub credentials 5 times in a row, aborting.");
+    }
+}

+ 2 - 2
tests/Composer/Test/Downloader/ArchiveDownloaderTest.php

@@ -22,7 +22,7 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase
             ->will($this->returnValue('http://example.com/script.js'))
         ;
 
-        $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface')));
+        $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'), $this->getMock('Composer\Config')));
         $method = new \ReflectionMethod($downloader, 'getFileName');
         $method->setAccessible(true);
 
@@ -33,7 +33,7 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase
 
     public function testProcessUrl()
     {
-        $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface')));
+        $downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'), $this->getMock('Composer\Config')));
         $method = new \ReflectionMethod($downloader, 'processUrl');
         $method->setAccessible(true);
 

+ 3 - 2
tests/Composer/Test/Downloader/FileDownloaderTest.php

@@ -17,12 +17,13 @@ use Composer\Util\Filesystem;
 
 class FileDownloaderTest extends \PHPUnit_Framework_TestCase
 {
-    protected function getDownloader($io = null, $rfs = null)
+    protected function getDownloader($io = null, $config = null, $rfs = null)
     {
         $io = $io ?: $this->getMock('Composer\IO\IOInterface');
+        $config = $config ?: $this->getMock('Composer\Config');
         $rfs = $rfs ?: $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock();
 
-        return new FileDownloader($io, $rfs);
+        return new FileDownloader($io, $config, $rfs);
     }
 
     /**

+ 2 - 1
tests/Composer/Test/Downloader/ZipDownloaderTest.php

@@ -32,7 +32,8 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase
         ;
 
         $io = $this->getMock('Composer\IO\IOInterface');
-        $downloader = new ZipDownloader($io);
+        $config = $this->getMock('Composer\Config');
+        $downloader = new ZipDownloader($io, $config);
 
         try {
             $downloader->download($packageMock, sys_get_temp_dir().'/composer-zip-test');