瀏覽代碼

Added GitHub Post-Receive URL feature.

Beau Simensen 13 年之前
父節點
當前提交
566720cd3a

+ 12 - 0
app/Resources/FOSUserBundle/views/Profile/show.html.twig

@@ -9,6 +9,18 @@
         <p><a href="{{ path('fos_user_change_password') }}">Change your password</a></p>
         <p><a href="{{ path('user_profile', {'name':app.user.username}) }}">View your public profile</a></p>
 
+        {% if app.user.apiToken %}
+        <h1>Your API Token</h1>
+        <p><pre><code>{{ app.user.apiToken }}</code></pre></p>
+        <p>
+        You can use your API token to interact with the Packagist API.
+        </p>
+        <p>Your <a href="http://help.github.com/post-receive-hooks/">GitHub Post-Receive URL</a> is:</p>
+        <p>
+        <pre><code>{{ url('github_postreceive', { 'username': app.user.username, 'apiToken': app.user.apiToken }) }}</code></pre>
+        </p>
+        {% endif %}
+
         <h1>Your packages</h1>
         {% if user.packages|length %}
             {{ macros.listPackages(user.packages) }}

+ 52 - 0
src/Packagist/WebBundle/Command/GenerateTokensCommand.php

@@ -0,0 +1,52 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Command;
+
+use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
+use Symfony\Bridge\Doctrine\RegistryInterface;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\HttpKernel\KernelInterface;
+use Symfony\Component\Finder\Finder;
+
+/**
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class GenerateTokensCommand extends ContainerAwareCommand
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function configure()
+    {
+        $this
+            ->setName('packagist:tokens:generate')
+            ->setDescription('Generates all missing user tokens')
+        ;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $doctrine = $this->getContainer()->get('doctrine');
+        $userRepo = $doctrine->getRepository('PackagistWebBundle:User');
+        $users = $userRepo->findUsersMissingApiToken();
+        foreach ($users as $user) {
+            $user->regenerateApiToken();
+        }
+        $doctrine->getEntityManager()->flush();
+    }
+}

+ 2 - 2
src/Packagist/WebBundle/Command/UpdatePackagesCommand.php

@@ -116,7 +116,7 @@ class UpdatePackagesCommand extends ContainerAwareCommand
                         $output->writeln('Storing '.$version->getPrettyVersion().' ('.$version->getVersion().')');
                     }
 
-                    $this->updateInformation($output, $doctrine, $package, $version);
+                    $this->updateInformation($doctrine, $package, $version);
                     $doctrine->getEntityManager()->flush();
                 }
 
@@ -139,7 +139,7 @@ class UpdatePackagesCommand extends ContainerAwareCommand
         }
     }
 
-    private function updateInformation(OutputInterface $output, RegistryInterface $doctrine, $package, PackageInterface $data)
+    private function updateInformation(RegistryInterface $doctrine, $package, PackageInterface $data)
     {
         $em = $doctrine->getEntityManager();
         $version = new Version();

+ 240 - 0
src/Packagist/WebBundle/Controller/ApiController.php

@@ -12,16 +12,34 @@
 
 namespace Packagist\WebBundle\Controller;
 
+use Composer\Package\PackageInterface;
+use Composer\Repository\VcsRepository;
+use Packagist\WebBundle\Entity\Author;
+use Packagist\WebBundle\Entity\Tag;
+use Packagist\WebBundle\Entity\Version;
+use Symfony\Bridge\Doctrine\RegistryInterface;
 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\Request;
 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
  */
 class ApiController extends Controller
 {
+
+    protected $supportedLinkTypes = array(
+        'require'   => 'RequireLink',
+        'conflict'  => 'ConflictLink',
+        'provide'   => 'ProvideLink',
+        'replace'   => 'ReplaceLink',
+        'recommend' => 'RecommendLink',
+        'suggest'   => 'SuggestLink',
+    );
+
     /**
      * @Template()
      * @Route("/packages.json", name="packages")
@@ -41,4 +59,226 @@ class ApiController extends Controller
         $response->setSharedMaxAge(60);
         return $response;
     }
+    /**
+     * @Route("/api/github.json", name="github_postreceive")
+     * @Method({"POST"})
+     */
+    public function githubPostReceive(Request $request)
+    {
+        $responseHeaders = array('Content-Type' => 'application/json');
+        $payload = json_decode($request->request->get('payload'), true);
+        if (!$payload or !isset($payload['repository']['url'])) {
+            return new Response(json_encode(array('status' => 'error', 'message' => 'Missing or invalid payload',)), 406, $responseHeaders);
+        }
+        $username = $request->query->get('username');
+        $apiToken = $request->query->get('apiToken');
+        $doctrine = $this->get('doctrine');
+        $user = $doctrine
+            ->getRepository('Packagist\WebBundle\Entity\User')
+            ->findOneBy(array('username' => $username, 'apiToken' => $apiToken));
+        if (!$user) {
+            return new Response(json_encode(array('status' => 'error', 'message' => 'Invalid credentials',)), 403, $responseHeaders);
+        }
+        if (! preg_match('~(github.com/[\w_\-\.]+/[\w_\-\.]+)$~', $payload['repository']['url'], $matches)) {
+            return new Response(json_encode(array('status' => 'error', 'message' => 'Could not parse payload repository URL',)), 406, $responseHeaders);
+        }
+        $payloadRepositoryChunk = $matches[1];
+        foreach ($user->getPackages() as $package) {
+            if (strpos($package->getRepository(), $payloadRepositoryChunk) !== false) {
+
+                //
+                // We found the package that was referenced.
+                //
+                
+                $force = true;
+                $start = new \DateTime();
+
+                $repository = new VcsRepository(array('url' => $package->getRepository()));
+                $versions = $repository->getPackages();
+            
+                usort($versions, function ($a, $b) {
+                    return version_compare($a->getVersion(), $b->getVersion());
+                });
+            
+                // clear existing versions to force a clean reloading if --force is enabled
+                if ($force) {
+                    $versionRepo = $doctrine->getRepository('PackagistWebBundle:Version');
+                    foreach ($package->getVersions() as $version) {
+                        $versionRepo->remove($version);
+                    }
+            
+                    $doctrine->getEntityManager()->flush();
+                    $doctrine->getEntityManager()->refresh($package);
+                }
+            
+                foreach ($versions as $version) {
+                    $this->updateInformation($doctrine, $package, $version);
+                    $doctrine->getEntityManager()->flush();
+                }
+            
+                // remove outdated -dev versions
+                foreach ($package->getVersions() as $version) {
+                    if ($version->getDevelopment() && $version->getUpdatedAt() < $start) {
+                        $doctrine->getRepository('PackagistWebBundle:Version')->remove($version);
+                    }
+                }
+            
+                $package->setUpdatedAt(new \DateTime);
+                $package->setCrawledAt(new \DateTime);
+                $doctrine->getEntityManager()->flush();
+
+                return new Response('{ "status": "success" }', 202, $responseHeaders);
+            }
+        }
+        return new Response(json_encode(array('status' => 'error', 'message' => 'Could not find a package that matches this request (does user maintain the package?)',)), 404, $responseHeaders);
+    }
+
+    private function updateInformation(RegistryInterface $doctrine, $package, PackageInterface $data)
+    {
+        $em = $doctrine->getEntityManager();
+        $version = new Version();
+    
+        $version->setName($package->getName());
+        $version->setNormalizedVersion(preg_replace('{-dev$}i', '', $data->getVersion()));
+    
+        // check if we have that version yet
+        foreach ($package->getVersions() as $existingVersion) {
+            if ($existingVersion->equals($version)) {
+                // avoid updating newer versions, in case two branches have the same version in their composer.json
+                if ($existingVersion->getReleasedAt() > $data->getReleaseDate()) {
+                    return;
+                }
+                if ($existingVersion->getDevelopment()) {
+                    $version = $existingVersion;
+                    break;
+                }
+                return;
+            }
+        }
+    
+        $version->setVersion($data->getPrettyVersion());
+        $version->setDevelopment(substr($data->getVersion(), -4) === '-dev');
+    
+        $em->persist($version);
+    
+        $version->setDescription($data->getDescription());
+        $package->setDescription($data->getDescription());
+        $version->setHomepage($data->getHomepage());
+        $version->setLicense($data->getLicense() ?: array());
+    
+        $version->setPackage($package);
+        $version->setUpdatedAt(new \DateTime);
+        $version->setReleasedAt($data->getReleaseDate());
+    
+        if ($data->getSourceType()) {
+            $source['type'] = $data->getSourceType();
+            $source['url'] = $data->getSourceUrl();
+            $source['reference'] = $data->getSourceReference();
+            $version->setSource($source);
+        }
+    
+        if ($data->getDistType()) {
+            $dist['type'] = $data->getDistType();
+            $dist['url'] = $data->getDistUrl();
+            $dist['reference'] = $data->getDistReference();
+            $dist['shasum'] = $data->getDistSha1Checksum();
+            $version->setDist($dist);
+        }
+    
+        if ($data->getType()) {
+            $version->setType($data->getType());
+            if ($data->getType() && $data->getType() !== $package->getType()) {
+                $package->setType($data->getType());
+            }
+        }
+    
+        $version->setTargetDir($data->getTargetDir());
+        $version->setAutoload($data->getAutoload());
+        $version->setExtra($data->getExtra());
+        $version->setBinaries($data->getBinaries());
+    
+        $version->getTags()->clear();
+        if ($data->getKeywords()) {
+            foreach ($data->getKeywords() as $keyword) {
+                $version->addTag(Tag::getByName($em, $keyword, true));
+            }
+        }
+    
+        $version->getAuthors()->clear();
+        if ($data->getAuthors()) {
+            foreach ($data->getAuthors() as $authorData) {
+                $author = null;
+                // skip authors with no information
+                if (empty($authorData['email']) && empty($authorData['name'])) {
+                    continue;
+                }
+    
+                if (!empty($authorData['email'])) {
+                    $author = $doctrine->getRepository('PackagistWebBundle:Author')->findOneByEmail($authorData['email']);
+                }
+    
+                if (!$author && !empty($authorData['homepage'])) {
+                    $author = $doctrine->getRepository('PackagistWebBundle:Author')->findOneBy(array(
+                            'name' => $authorData['name'],
+                            'homepage' => $authorData['homepage']
+                    ));
+                }
+    
+                if (!$author && !empty($authorData['name'])) {
+                    $author = $doctrine->getRepository('PackagistWebBundle:Author')->findOneByNameAndPackage($authorData['name'], $package);
+                }
+    
+                if (!$author) {
+                    $author = new Author();
+                    $em->persist($author);
+                }
+    
+                foreach (array('email', 'name', 'homepage') as $field) {
+                    if (isset($authorData[$field])) {
+                        $author->{'set'.$field}($authorData[$field]);
+                    }
+                }
+    
+                $author->setUpdatedAt(new \DateTime);
+                if (!$version->getAuthors()->contains($author)) {
+                    $version->addAuthor($author);
+                }
+                if (!$author->getVersions()->contains($version)) {
+                    $author->addVersion($version);
+                }
+            }
+        }
+    
+        foreach ($this->supportedLinkTypes as $linkType => $linkEntity) {
+            $links = array();
+            foreach ($data->{'get'.$linkType.'s'}() as $link) {
+                $links[$link->getTarget()] = $link->getPrettyConstraint();
+            }
+    
+            foreach ($version->{'get'.$linkType}() as $link) {
+                // clear links that have changed/disappeared (for updates)
+                if (!isset($links[$link->getPackageName()]) || $links[$link->getPackageName()] !== $link->getPackageVersion()) {
+                    $version->{'get'.$linkType}()->removeElement($link);
+                    $em->remove($link);
+                } else {
+                    // clear those that are already set
+                    unset($links[$link->getPackageName()]);
+                }
+            }
+    
+            foreach ($links as $linkPackageName => $linkPackageVersion) {
+                $class = 'Packagist\WebBundle\Entity\\'.$linkEntity;
+                $link = new $class;
+                $link->setPackageName($linkPackageName);
+                $link->setPackageVersion($linkPackageVersion);
+                $version->{'add'.$linkType.'Link'}($link);
+                $link->setVersion($version);
+                $em->persist($link);
+            }
+        }
+    
+        if (!$package->getVersions()->contains($version)) {
+            $package->addVersions($version);
+        }
+    }
 }

+ 46 - 1
src/Packagist/WebBundle/Entity/User.php

@@ -17,7 +17,7 @@ use Doctrine\ORM\Mapping as ORM;
 use Doctrine\Common\Collections\ArrayCollection;
 
 /**
- * @ORM\Entity
+ * @ORM\Entity(repositoryClass="Packagist\WebBundle\Entity\UserRepository")
  * @ORM\Table(name="fos_user")
  */
 class User extends BaseUser
@@ -44,11 +44,18 @@ class User extends BaseUser
      */
     private $createdAt;
 
+    /**
+     * @ORM\Column(type="string")
+     * @var string
+     */
+    private $apiToken;
+
     public function __construct()
     {
         $this->packages = new ArrayCollection();
         $this->authors = new ArrayCollection();
         $this->createdAt = new \DateTime();
+        $this->apiToken = $this->generateApiToken();
         parent::__construct();
     }
 
@@ -119,4 +126,42 @@ class User extends BaseUser
     {
         return $this->createdAt;
     }
+    
+    /**
+     * Set apiToken
+     *
+     * @param string $apiToken
+     */
+    public function setApiToken($apiToken)
+    {
+        $this->apiToken = $apiToken;
+    }
+
+    /**
+     * Get apiToken
+     * 
+     * @return string
+     */
+    public function getApiToken()
+    {
+        return $this->apiToken;
+    }
+
+    /**
+     * Regenerate the apiToken
+     */
+    public function regenerateApiToken()
+    {
+        $this->apiToken = $this->generateApiToken();
+    }
+
+    /**
+     * Generate an apiToken
+     *
+     * @return string
+     */
+    protected function generateApiToken()
+    {
+        return base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
+    }
 }

+ 31 - 0
src/Packagist/WebBundle/Entity/UserRepository.php

@@ -0,0 +1,31 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Entity;
+
+use Doctrine\ORM\EntityRepository;
+
+/**
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class UserRepository extends EntityRepository
+{
+    public function findUsersMissingApiToken()
+    {
+        $qb = $this->getEntityManager()->createQueryBuilder();
+        $qb->select('u')
+            ->from('Packagist\WebBundle\Entity\User', 'u')
+            ->where('u.apiToken IS NULL or u.apiToken = ?0')
+            ->setParameters(array(''));
+        return $qb->getQuery()->getResult();
+    }
+}

+ 23 - 23
src/Packagist/WebBundle/Tests/Controller/AboutControllerTest.php

@@ -1,24 +1,24 @@
-<?php
-
-namespace Packagist\WebBundle\Tests\Controller;
-
-use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
-
-class AboutControllerTest extends WebTestCase
-{
-    public function testPackagist()
-    {
-        $client = self::createClient();
-
-        $crawler = $client->request('GET', '/about');
-        $this->assertEquals('What is Packagist?', $crawler->filter('.box h1')->first()->text());
-    }
-    
-    public function testComposer()
-    {
-        $client = self::createClient();
-
-        $crawler = $client->request('GET', '/about-composer');
-        $this->assertEquals('What is Composer?', $crawler->filter('.box h1')->first()->text());
-    }
+<?php
+
+namespace Packagist\WebBundle\Tests\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+
+class AboutControllerTest extends WebTestCase
+{
+    public function testPackagist()
+    {
+        $client = self::createClient();
+
+        $crawler = $client->request('GET', '/about');
+        $this->assertEquals('What is Packagist?', $crawler->filter('.box h1')->first()->text());
+    }
+    
+    public function testComposer()
+    {
+        $client = self::createClient();
+
+        $crawler = $client->request('GET', '/about-composer');
+        $this->assertEquals('What is Composer?', $crawler->filter('.box h1')->first()->text());
+    }
 }

+ 38 - 15
src/Packagist/WebBundle/Tests/Controller/ApiControllerTest.php

@@ -1,16 +1,39 @@
-<?php
-
-namespace Packagist\WebBundle\Tests\Controller;
-
-use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
-
-class ApiControllerTest extends WebTestCase
-{
-    public function testPackages()
-    {
-        $client = self::createClient();
-
-        $client->request('GET', '/packages.json');
-        $this->assertTrue(count(json_decode($client->getResponse()->getContent())) > 0);
-    }
+<?php
+
+namespace Packagist\WebBundle\Tests\Controller;
+
+use Packagist\WebBundle\Entity\User;
+
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+
+class ApiControllerTest extends WebTestCase
+{
+    public function testPackages()
+    {
+        $client = self::createClient();
+
+        $client->request('GET', '/packages.json');
+        $this->assertTrue(count(json_decode($client->getResponse()->getContent())) > 0);
+    }
+
+    public function testGithubFailsCorrectly()
+    {
+        $client = self::createClient();
+        
+        $client->request('GET', '/api/github.json');
+        $this->assertEquals(405, $client->getResponse()->getStatusCode(), 'GET method should not be allowed for GitHub Post-Receive URL');
+
+        $doctrine = $client->getContainer()->get('doctrine');
+        $em = $doctrine->getEntityManager();
+        $userRepo = $doctrine->getRepository('PackagistWebBundle:User');
+        $testUser = new User();
+        $testUser->setUsername('ApiControllerTest');
+        $payload = json_encode(array('repository' => array('url' => 'git://github.com/composer/composer',)));
+
+        $client->request('POST', '/api/github.json?username='.$testUser->getUsername().'&apiToken=BAD'.$testUser->getApiToken(), array('payload' => $payload,));
+        $this->assertEquals(403, $client->getResponse()->getStatusCode(), 'POST method should return 403 "Forbidden" if invalid API Token is sent');
+
+        $client->request('POST', '/api/github.json?username=BAD'.$testUser->getUsername().'&apiToken='.$testUser->getApiToken(), array('payload' => $payload,));
+        $this->assertEquals(403, $client->getResponse()->getStatusCode(), 'POST method should return 403 "Forbidden" if invalid API Token is sent');
+    }
 }

+ 34 - 34
src/Packagist/WebBundle/Tests/Controller/WebControllerTest.php

@@ -1,35 +1,35 @@
-<?php
-
-namespace Packagist\WebBundle\Tests\Controller;
-
-use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
-
-class WebControllerTest extends WebTestCase
-{
-    public function testHomepage()
-    {
-        $client = self::createClient();
-
-        $crawler = $client->request('GET', '/');
-        $this->assertEquals('Getting Started', $crawler->filter('.getting-started h1')->text());
-    }
-    
-    public function testPackages()
-    {
-        $client = self::createClient();
-        //we expect at least one package
-        $crawler = $client->request('GET', '/packages/');
-        $this->assertTrue($crawler->filter('.packages li')->count() > 0);
-    }
-    
-    public function testPackage()
-    {
-        $client = self::createClient();
-        //we expect package to be clickable and showing at least 'package' div
-        $crawler = $client->request('GET', '/packages/');
-        $link = $crawler->filter('.packages li h1 a')->first()->attr('href');
-        
-        $crawler = $client->request('GET', $link);
-        $this->assertTrue($crawler->filter('.package')->count() > 0);
-    }
+<?php
+
+namespace Packagist\WebBundle\Tests\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+
+class WebControllerTest extends WebTestCase
+{
+    public function testHomepage()
+    {
+        $client = self::createClient();
+
+        $crawler = $client->request('GET', '/');
+        $this->assertEquals('Getting Started', $crawler->filter('.getting-started h1')->text());
+    }
+    
+    public function testPackages()
+    {
+        $client = self::createClient();
+        //we expect at least one package
+        $crawler = $client->request('GET', '/packages/');
+        $this->assertTrue($crawler->filter('.packages li')->count() > 0);
+    }
+    
+    public function testPackage()
+    {
+        $client = self::createClient();
+        //we expect package to be clickable and showing at least 'package' div
+        $crawler = $client->request('GET', '/packages/');
+        $link = $crawler->filter('.packages li h1 a')->first()->attr('href');
+        
+        $crawler = $client->request('GET', $link);
+        $this->assertTrue($crawler->filter('.package')->count() > 0);
+    }
 }