Browse Source

Auto-detect repo renames on github and update package URL automatically instead of failing auto-updates

Jordi Boggiano 5 years ago
parent
commit
a52964b64b

+ 50 - 9
src/Packagist/WebBundle/Controller/ApiController.php

@@ -104,21 +104,25 @@ class ApiController extends Controller
         if (isset($payload['project']['git_http_url'])) { // gitlab event payload
             $urlRegex = '{^(?:ssh://git@|https?://|git://|git@)?(?P<host>[a-z0-9.-]+)(?::[0-9]+/|[:/])(?P<path>[\w.-]+(?:/[\w.-]+?)+)(?:\.git|/)?$}i';
             $url = $payload['project']['git_http_url'];
+            $remoteId = null;
         } elseif (isset($payload['repository']['url'])) { // github/anything hook
             $urlRegex = '{^(?:ssh://git@|https?://|git://|git@)?(?P<host>[a-z0-9.-]+)(?::[0-9]+/|[:/])(?P<path>[\w.-]+(?:/[\w.-]+?)*)(?:\.git|/)?$}i';
             $url = $payload['repository']['url'];
             $url = str_replace('https://api.github.com/repos', 'https://github.com', $url);
+            $remoteId = $payload['repository']['id'] ?? null;
         } elseif (isset($payload['repository']['links']['html']['href'])) { // bitbucket push event payload
             $urlRegex = '{^(?:https?://|git://|git@)?(?:api\.)?(?P<host>bitbucket\.org)[/:](?P<path>[\w.-]+/[\w.-]+?)(\.git)?/?$}i';
             $url = $payload['repository']['links']['html']['href'];
+            $remoteId = null;
         } elseif (isset($payload['canon_url']) && isset($payload['repository']['absolute_url'])) { // bitbucket post hook (deprecated)
             $urlRegex = '{^(?:https?://|git://|git@)?(?P<host>bitbucket\.org)[/:](?P<path>[\w.-]+/[\w.-]+?)(\.git)?/?$}i';
             $url = $payload['canon_url'].$payload['repository']['absolute_url'];
+            $remoteId = null;
         } else {
             return new JsonResponse(array('status' => 'error', 'message' => 'Missing or invalid payload'), 406);
         }
 
-        return $this->receivePost($request, $url, $urlRegex);
+        return $this->receivePost($request, $url, $urlRegex, $remoteId);
     }
 
     /**
@@ -301,13 +305,17 @@ class ApiController extends Controller
      * @param string $urlRegex the regex used to split the user packages into domain and path
      * @return Response
      */
-    protected function receivePost(Request $request, $url, $urlRegex)
+    protected function receivePost(Request $request, $url, $urlRegex, $remoteId)
     {
         // try to parse the URL first to avoid the DB lookup on malformed requests
         if (!preg_match($urlRegex, $url, $match)) {
             return new Response(json_encode(array('status' => 'error', 'message' => 'Could not parse payload repository URL')), 406);
         }
 
+        if ($remoteId) {
+            $remoteId = $match['host'].'/'.$remoteId;
+        }
+
         $packages = null;
         $user = null;
         $autoUpdated = Package::AUTO_MANUAL_HOOK;
@@ -322,7 +330,7 @@ class ApiController extends Controller
                 list($algo, $sig) = explode('=', $sig);
                 $expected = hash_hmac($algo, $request->getContent(), $user->getApiToken());
                 if (hash_equals($expected, $sig)) {
-                    $packages = $this->findPackagesByRepository('https://github.com/'.$match['path']);
+                    $packages = $this->findPackagesByRepository('https://github.com/'.$match['path'], $remoteId, $user);
                     $autoUpdated = Package::AUTO_GITHUB_HOOK;
                     $receiveType = 'github_user_secret';
                 } else {
@@ -344,7 +352,7 @@ class ApiController extends Controller
                 list($algo, $sig) = explode('=', $sig);
                 $expected = hash_hmac($algo, $request->getContent(), $this->container->getParameter('github.webhook_secret'));
                 if (hash_equals($expected, $sig)) {
-                    $packages = $this->findPackagesByRepository('https://github.com/'.$match['path']);
+                    $packages = $this->findPackagesByRepository('https://github.com/'.$match['path'], $remoteId);
                     $autoUpdated = Package::AUTO_GITHUB_HOOK;
                     $receiveType = 'github_auto';
                 }
@@ -357,7 +365,7 @@ class ApiController extends Controller
             }
 
             // try to find the user package
-            $packages = $this->findPackagesByUrl($user, $url, $urlRegex);
+            $packages = $this->findPackagesByUrl($user, $url, $urlRegex, $remoteId);
         }
 
         if (!$packages) {
@@ -370,12 +378,13 @@ class ApiController extends Controller
         /** @var Package $package */
         foreach ($packages as $package) {
             $package->setAutoUpdated($autoUpdated);
-            $em->flush($package);
 
             $job = $this->get('scheduler')->scheduleUpdate($package);
             $jobs[] = $job->getId();
         }
 
+        $em->flush();
+
         return new JsonResponse(['status' => 'success', 'jobs' => $jobs, 'type' => $receiveType], 202);
     }
 
@@ -417,7 +426,7 @@ class ApiController extends Controller
      * @param string $urlRegex
      * @return array the packages found
      */
-    protected function findPackagesByUrl(User $user, $url, $urlRegex)
+    protected function findPackagesByUrl(User $user, $url, $urlRegex, $remoteId)
     {
         if (!preg_match($urlRegex, $url, $matched)) {
             return array();
@@ -434,6 +443,9 @@ class ApiController extends Controller
                 )
             ) {
                 $packages[] = $package;
+                if ($remoteId && !$package->getRemoteId()) {
+                    $package->setRemoteId($remoteId);
+                }
             }
         }
 
@@ -444,8 +456,37 @@ class ApiController extends Controller
      * @param string $url
      * @return array the packages found
      */
-    protected function findPackagesByRepository(string $url): array
+    protected function findPackagesByRepository(string $url, $remoteId, User $user = null): array
     {
-        return $this->getDoctrine()->getRepository(Package::class)->findBy(['repository' => $url]);
+        $packageRepo = $this->getDoctrine()->getRepository(Package::class);
+        $packages = $packageRepo->findBy(['repository' => $url]);
+        $updateUrl = false;
+
+        // maybe url changed, look up by remoteId
+        if (!$packages) {
+            $packages = $packageRepo->findBy(['remoteId' => $remoteId]);
+            $updateUrl = true;
+        }
+
+        if ($user) {
+            // need to check ownership if a user is provided as we can not trust that the request came from github in this case
+            $packages = array_filter($packages, function ($p) use ($user, $packageRepo) {
+                return $packageRepo->isPackageMaintainedBy($p, $user->getId());
+            });
+        }
+
+        foreach ($packages as $package) {
+            if ($remoteId && !$package->getRemoteId()) {
+                $package->setRemoteId($remoteId);
+            }
+        }
+
+        if ($updateUrl) {
+            foreach ($packages as $package) {
+                $package->setRepository($url);
+            }
+        }
+
+        return $packages;
     }
 }

+ 21 - 1
src/Packagist/WebBundle/Entity/Package.php

@@ -30,7 +30,8 @@ use Composer\Repository\Vcs\GitHubDriver;
  *         @ORM\Index(name="indexed_idx",columns={"indexedAt"}),
  *         @ORM\Index(name="crawled_idx",columns={"crawledAt"}),
  *         @ORM\Index(name="dumped_idx",columns={"dumpedAt"}),
- *         @ORM\Index(name="repository_idx",columns={"repository"})
+ *         @ORM\Index(name="repository_idx",columns={"repository"}),
+ *         @ORM\Index(name="remoteid_idx",columns={"remoteId"})
  *     }
  * )
  * @Assert\Callback(callback="isPackageUnique")
@@ -146,6 +147,11 @@ class Package
      */
     private $downloads;
 
+    /**
+     * @ORM\Column(type="string", nullable=true)
+     */
+    private $remoteId;
+
     /**
      * @ORM\Column(type="smallint")
      */
@@ -611,6 +617,7 @@ class Package
         $repoUrl = preg_replace_callback('{^(https?|git|svn)://}i', function ($match) { return strtolower($match[1]) . '://'; }, $repoUrl);
 
         $this->repository = $repoUrl;
+        $this->remoteId = null;
 
         // avoid user@host URLs
         if (preg_match('{https?://.+@}', $repoUrl)) {
@@ -636,6 +643,9 @@ class Package
             }
             if ($driver instanceof GitHubDriver) {
                 $this->repository = $driver->getRepositoryUrl();
+                if ($repoData = $driver->getRepoData()) {
+                    $this->remoteId = parse_url($this->repository, PHP_URL_HOST).'/'.$repoData['id'];
+                }
             }
         } catch (\Exception $e) {
             $this->vcsDriverError = '['.get_class($e).'] '.$e->getMessage();
@@ -826,6 +836,16 @@ class Package
         return $this->type;
     }
 
+    public function setRemoteId(?string $remoteId)
+    {
+        $this->remoteId = $remoteId;
+    }
+
+    public function getRemoteId(): ?string
+    {
+        return $this->remoteId;
+    }
+
     /**
      * Set autoUpdated
      *

+ 13 - 0
src/Packagist/WebBundle/Entity/PackageRepository.php

@@ -112,6 +112,19 @@ class PackageRepository extends ServiceEntityRepository
         return $query->getResult();
     }
 
+    public function isPackageMaintainedBy(Package $package, int $userId)
+    {
+        $query = $this->createQueryBuilder('p')
+            ->select('p.id')
+            ->join('p.maintainers', 'm')
+            ->where('m.id = :userId')
+            ->andWhere('p.id = :package')
+            ->getQuery()
+            ->setParameters(['userId' => $userId, 'package' => $package]);
+
+        return (bool) $query->getOneOrNullResult();
+    }
+
     public function getPackagesWithFields($filters, $fields)
     {
         $selector = '';