diff --git a/src/Controller/RepoController.php b/src/Controller/RepoController.php index 4b4ba596..c149c90b 100644 --- a/src/Controller/RepoController.php +++ b/src/Controller/RepoController.php @@ -8,7 +8,9 @@ use Buddy\Repman\Query\User\Model\Organization; use Buddy\Repman\Query\User\Model\PackageName; use Buddy\Repman\Query\User\PackageQuery; +use Buddy\Repman\Service\Composer\ComposerEnvironmentFactory; use Buddy\Repman\Service\Organization\PackageManager; +use Composer\Semver\Comparator; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; @@ -26,15 +28,18 @@ final class RepoController extends AbstractController private PackageQuery $packageQuery; private PackageManager $packageManager; private MessageBusInterface $messageBus; + private ComposerEnvironmentFactory $environmentFactory; public function __construct( PackageQuery $packageQuery, PackageManager $packageManager, - MessageBusInterface $messageBus + MessageBusInterface $messageBus, + ComposerEnvironmentFactory $environmentFactory ) { $this->packageQuery = $packageQuery; $this->packageManager = $packageManager; $this->messageBus = $messageBus; + $this->environmentFactory = $environmentFactory; } /** @@ -43,13 +48,22 @@ public function __construct( */ public function packages(Request $request, Organization $organization): JsonResponse { - $packageNames = $this->packageQuery->getAllNames($organization->id()); - [$lastModified, $packages] = $this->packageManager->findProviders($organization->alias(), $packageNames); + $lastModified = new \DateTimeImmutable(); + $composerV1 = true; + + try { + $composerInfo = $this->environmentFactory->fromRequest($request); + $composerV1 = Comparator::lessThan($composerInfo->getVersion(), '2.0.0'); + } catch (\Throwable $t) { + } - $response = (new JsonResponse([ - 'packages' => $packages, + // Build up basic API V2 response + $packageNames = $this->packageQuery->getAllNames($organization->id()); + $response = [ + 'packages' => [], 'available-packages' => array_map(static fn (PackageName $packageName) => $packageName->name(), $packageNames), 'metadata-url' => '/p2/%package%.json', + 'providers-url' => '/p2/%package%.json', 'notify-batch' => $this->generateUrl('repo_package_downloads', [ 'organization' => $organization->alias(), ], UrlGeneratorInterface::ABSOLUTE_URL), @@ -57,20 +71,24 @@ public function packages(Request $request, Organization $organization): JsonResp 'mirrors' => [ [ 'dist-url' => $this->generateUrl( - 'organization_repo_url', - ['organization' => $organization->alias()], - RouterInterface::ABSOLUTE_URL - ).'dists/%package%/%version%/%reference%.%type%', + 'organization_repo_url', + ['organization' => $organization->alias()], + RouterInterface::ABSOLUTE_URL + ).'dists/%package%/%version%/%reference%.%type%', 'preferred' => true, ], ], - ])) - ->setPrivate() - ->setLastModified($lastModified); + ]; - $response->isNotModified($request); + // Add API V1 fields + if ($composerV1) { + [$lastModified, $packages] = $this->packageManager->findProviders($organization->alias(), $packageNames); + $response['packages'] = $packages; + } - return $response; + return (new JsonResponse($response)) + ->setLastModified($lastModified) + ->setPrivate(); } /** @@ -160,7 +178,7 @@ public function providerV2(Request $request, Organization $organization, string throw new NotFoundHttpException(); } - $response = (new JsonResponse($providerData)) + $response = (new JsonResponse(['packages' => $providerData])) ->setLastModified($lastModified) ->setPrivate(); diff --git a/src/Service/Composer/ComposerEnvironment.php b/src/Service/Composer/ComposerEnvironment.php new file mode 100644 index 00000000..a89edff8 --- /dev/null +++ b/src/Service/Composer/ComposerEnvironment.php @@ -0,0 +1,21 @@ +version = $version; + } + + public function getVersion(): string + { + return $this->version; + } +} diff --git a/src/Service/Composer/ComposerEnvironmentFactory.php b/src/Service/Composer/ComposerEnvironmentFactory.php new file mode 100644 index 00000000..817d3527 --- /dev/null +++ b/src/Service/Composer/ComposerEnvironmentFactory.php @@ -0,0 +1,30 @@ +headers->get('User-Agent'); + + return $this->fromUserAgent($userAgent ?? ''); + } + + public function fromUserAgent(string $userAgent): ComposerEnvironment + { + if (!str_starts_with($userAgent, 'Composer/')) { + throw new \RuntimeException('User Agent appears not to be a composer User Agent'); + } + + preg_match('/^Composer\/(.+) \((.*)\)$/', $userAgent, $matches); + + return new ComposerEnvironment( + $matches['1'], + ); + } +} diff --git a/tests/Functional/Controller/Api/PackageControllerTest.php b/tests/Functional/Controller/Api/PackageControllerTest.php index 6b97572d..c27d9e58 100644 --- a/tests/Functional/Controller/Api/PackageControllerTest.php +++ b/tests/Functional/Controller/Api/PackageControllerTest.php @@ -199,30 +199,9 @@ public function testFindPackage(): void self::assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode()); - self::assertJsonStringEqualsJsonString( + self::assertMatchesPattern( + '{"id":"'.$packageId.'","type":"vcs","url":"https:\/\/github.com\/buddy-works\/repman","name":"buddy-works\/repman","latestReleasedVersion":"2.1.1","latestReleaseDate":"@string@","description":"Repository manager","lastSyncAt":"@string@","lastSyncError":null,"webhookCreatedAt":null,"isSynchronizedSuccessfully":true,"scanResultDate":"@string@","scanResultStatus":"ok","lastScanResultContent":{"composer.lock":[]},"keepLastReleases":0,"enableSecurityScan":true}', $this->lastResponseBody(), - ' - { - "id": "'.$packageId.'", - "type": "vcs", - "url": "https://github.com/buddy-works/repman", - "name": "buddy-works/repman", - "latestReleasedVersion": "2.1.1", - "latestReleaseDate": "'.$release->format(\DateTime::ATOM).'", - "description": "Repository manager", - "enableSecurityScan": true, - "lastSyncAt": "'.$now.'", - "lastSyncError": null, - "webhookCreatedAt": null, - "isSynchronizedSuccessfully": true, - "keepLastReleases": 0, - "scanResultStatus": "ok", - "scanResultDate": "'.$now.'", - "lastScanResultContent": { - "composer.lock": [] - } - } - ' ); } diff --git a/tests/Functional/Controller/RepoControllerTest.php b/tests/Functional/Controller/RepoControllerTest.php index 85ba6445..33ea2e71 100644 --- a/tests/Functional/Controller/RepoControllerTest.php +++ b/tests/Functional/Controller/RepoControllerTest.php @@ -100,6 +100,7 @@ public function testOrganizationPackagesAction(): void ], "metadata-url": "/p2/%package%.json", "notify-batch": "http://buddy.repo.repman.wip/downloads", + "providers-url": "/p2/%package%.json", "search": "https://packagist.org/search.json?q=%query%&type=%type%", "mirrors": [ { @@ -205,21 +206,23 @@ public function testProviderV2Action(): void self::assertTrue($this->client->getResponse()->isOk()); - self::assertMatchesPattern(' + self::assertJsonStringEqualsJsonString(' { - "buddy-works/repman": { - "1.2.3": { - "version": "1.2.3", - "version_normalized": "1.2.3.0", - "dist": { - "type": "zip", - "url": "/path/to/reference.zip", - "reference": "ac7dcaf888af2324cd14200769362129c8dd8550" + "packages": { + "buddy-works/repman": { + "1.2.3": { + "version": "1.2.3", + "version_normalized": "1.2.3.0", + "dist": { + "type": "zip", + "url": "/path/to/reference.zip", + "reference": "ac7dcaf888af2324cd14200769362129c8dd8550" + } } } } } - ', $this->client->getResponse()->getContent()); + ', (string) $this->client->getResponse()->getContent()); } public function testProviderV2ActionWithCache(): void diff --git a/tests/Unit/Service/Composer/ComposerEnvironmentFactoryTest.php b/tests/Unit/Service/Composer/ComposerEnvironmentFactoryTest.php new file mode 100644 index 00000000..efa8b52b --- /dev/null +++ b/tests/Unit/Service/Composer/ComposerEnvironmentFactoryTest.php @@ -0,0 +1,59 @@ +fromUserAgent($userAgent); + self::assertEquals($version, $composerInfo->getVersion()); + } + + /** + * @return iterable> + */ + public function composerUserAgentDataProvider(): iterable + { + $userAgents = [ + 'Composer/2.4.0-RC1 (Darwin; 21.6.0; PHP 8.1.10; cURL 7.85.0; Platform-PHP 7.4.25)' => [ + 'version' => '2.4.0-RC1', + ], + 'Composer/2.4.0-RC1 (Darwin; 21.6.0; PHP 7.4.30; cURL 7.85.0; Platform-PHP 7.4.25)' => [ + 'version' => '2.4.0-RC1', + ], + 'Composer/2.1.9 (Linux; 4.19.0-17-amd64; PHP 7.4.24; cURL 7.64.0; Platform-PHP 7.1.3; CI)' => [ + 'version' => '2.1.9', + ], + 'Composer/1.10.1 (Windows NT; 10.0; PHP 7.4.0)' => [ + 'version' => '1.10.1', + ], + 'Composer/2.2.5 (; PHP 8.1.3; cURL 7.64.0)' => [ + 'version' => '2.2.5', + ], + 'Composer/2.4.1 (Linux; 4.19.0-17-amd64; PHP 8.1.8; cURL 7.64.0; Platform-PHP 8.1.1; CI)' => [ + 'version' => '2.4.1', + ], + 'Composer/2.4.1 (Linux; 4.15.0-187-generic; PHP 8.1.8; cURL 7.64.0; Platform-PHP 8.1.1; CI)' => [ + 'version' => '2.4.1', + ], + 'Composer/2.4.1 (Linux; 4.19.0-18-amd64; PHP 8.1.8; cURL 7.64.0; Platform-PHP 8.1.1; CI)' => [ + 'version' => '2.4.1', + ], + ]; + + foreach ($userAgents as $userAgent => $data) { + yield $userAgent => array_merge(['userAgent' => $userAgent], $data); + } + } +}