diff --git a/src/Audit/AuditRecordType.php b/src/Audit/AuditRecordType.php index 111758705..d19429f23 100644 --- a/src/Audit/AuditRecordType.php +++ b/src/Audit/AuditRecordType.php @@ -23,6 +23,7 @@ enum AuditRecordType: string case PackageCreated = 'package_created'; case PackageDeleted = 'package_deleted'; case CanonicalUrlChanged = 'canonical_url_changed'; + case VersionCreated = 'version_created'; case VersionDeleted = 'version_deleted'; case VersionReferenceChanged = 'version_reference_changed'; diff --git a/src/Audit/Display/AuditLogDisplayFactory.php b/src/Audit/Display/AuditLogDisplayFactory.php index 52c591952..2ebb90fe7 100644 --- a/src/Audit/Display/AuditLogDisplayFactory.php +++ b/src/Audit/Display/AuditLogDisplayFactory.php @@ -72,6 +72,14 @@ public function buildSingle(AuditRecord $record): AuditLogDisplayInterface $record->attributes['repository_to'], $this->buildActor($record->attributes['actor']), ), + AuditRecordType::VersionCreated => new VersionCreatedDisplay( + $record->datetime, + $record->attributes['name'], + $record->attributes['version'], + $record->attributes['metadata']['source']['reference'] ?? null, + $record->attributes['metadata']['dist']['reference'] ?? null, + $this->buildActor($record->attributes['actor']), + ), AuditRecordType::VersionDeleted => new VersionDeletedDisplay( $record->datetime, $record->attributes['name'], diff --git a/src/Audit/Display/VersionCreatedDisplay.php b/src/Audit/Display/VersionCreatedDisplay.php new file mode 100644 index 000000000..003c2e229 --- /dev/null +++ b/src/Audit/Display/VersionCreatedDisplay.php @@ -0,0 +1,39 @@ + + * Nils Adermann + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Audit\Display; + +use App\Audit\AuditRecordType; + +readonly class VersionCreatedDisplay extends AbstractAuditLogDisplay +{ + public function __construct( + \DateTimeImmutable $datetime, + public string $packageName, + public string $version, + public ?string $sourceReference, + public ?string $distReference, + ActorDisplay $actor, + ) { + parent::__construct($datetime, $actor); + } + + public function getType(): AuditRecordType + { + return AuditRecordType::VersionCreated; + } + + public function getTemplateName(): string + { + return 'audit_log/display/version_created.html.twig'; + } +} diff --git a/src/Entity/AuditRecord.php b/src/Entity/AuditRecord.php index ae8aeb598..ff0b596cb 100644 --- a/src/Entity/AuditRecord.php +++ b/src/Entity/AuditRecord.php @@ -17,6 +17,9 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Uid\Ulid; +/** + * @phpstan-import-type VersionArray from Version + */ #[ORM\Entity(repositoryClass: AuditRecordRepository::class)] #[ORM\Table(name: 'audit_log')] #[ORM\Index(name: 'type_idx', columns: ['type'])] @@ -80,6 +83,16 @@ public static function packageTransferred(Package $package, ?User $actor, array return new self(AuditRecordType::PackageTransferred, ['name' => $package->getName(), 'actor' => self::getUserData($actor, 'admin'), 'previous_maintainers' => $previous, 'current_maintainers' => $current], $actor?->getId(), $package->getVendor(), $package->getId()); } + /** + * @param VersionArray $metadata + */ + public static function versionCreated(Version $version, array $metadata, ?User $actor): self + { + $package = $version->getPackage(); + + return new self(AuditRecordType::VersionCreated, ['name' => $package->getName(), 'version' => $version->getVersion(), 'actor' => self::getUserData($actor, 'automation'), 'metadata' => $metadata], $actor?->getId(), $package->getVendor(), $package->getId()); + } + public static function versionDeleted(Version $version, ?User $actor): self { $package = $version->getPackage(); diff --git a/src/EventListener/VersionListener.php b/src/EventListener/VersionListener.php index 527e96ca1..de7b80fce 100644 --- a/src/EventListener/VersionListener.php +++ b/src/EventListener/VersionListener.php @@ -25,6 +25,7 @@ #[AsEntityListener(event: 'preRemove', entity: Version::class)] #[AsEntityListener(event: 'preUpdate', entity: Version::class)] +#[AsEntityListener(event: 'postPersist', entity: Version::class)] #[AsEntityListener(event: 'postUpdate', entity: Version::class)] class VersionListener { @@ -39,6 +40,16 @@ public function __construct( ) { } + /** + * @param LifecycleEventArgs $event + */ + public function postPersist(Version $version, LifecycleEventArgs $event): void + { + $data = $version->toV2Array([]); + $record = AuditRecord::versionCreated($version, $data, $this->getUser()); + $this->getEM()->getRepository(AuditRecord::class)->insert($record); + } + /** * @param LifecycleEventArgs $event */ diff --git a/templates/audit_log/display/version_created.html.twig b/templates/audit_log/display/version_created.html.twig new file mode 100644 index 000000000..cc1d40795 --- /dev/null +++ b/templates/audit_log/display/version_created.html.twig @@ -0,0 +1,10 @@ +{% import 'audit_log/macros.html.twig' as auditLog %} + +{{ auditLog.packageLink(display.packageName) }} {{ display.version }}
+{% if display.sourceReference %} + Source: {{ display.sourceReference }}
+{% endif %} +{% if display.distReference %} + Dist: {{ display.distReference }}
+{% endif %} +Created by: {{ display.actor.username }} diff --git a/tests/Audit/VersionAuditRecordTest.php b/tests/Audit/VersionAuditRecordTest.php index 4e2f37410..0fb01c81f 100644 --- a/tests/Audit/VersionAuditRecordTest.php +++ b/tests/Audit/VersionAuditRecordTest.php @@ -13,7 +13,9 @@ namespace App\Tests\Controller; use App\Audit\AuditRecordType; +use App\Entity\AuditRecord; use App\Entity\Package; +use App\Entity\RequireLink; use App\Entity\Version; use Doctrine\DBAL\Connection; use Doctrine\Persistence\ManagerRegistry; @@ -36,27 +38,35 @@ protected function tearDown(): void parent::tearDown(); } - public function testVersionChangesGetRecorded(): void + public function testVersionCreationGetsRecorded(): void { $container = static::getContainer(); $em = $container->get(ManagerRegistry::class)->getManager(); - $package = new Package(); - $package->setRepository('https://github.com/composer/composer'); + $version = $this->createPackageAndVersion(); + + $log = $em->getRepository(AuditRecord::class)->findOneBy([ + 'type' => AuditRecordType::VersionCreated, + 'packageId' => $version->getPackage()->getId(), + ]); + + self::assertNotNull($log, 'No audit record created for new version'); + self::assertSame('composer', $log->vendor); + $attributes = $log->attributes; + self::assertSame('composer/composer', $attributes['name']); + self::assertSame('automation', $attributes['actor']); + self::assertSame('1.0.0', $attributes['version']); + self::assertSame('dist-ref', $attributes['metadata']['dist']['reference']); + self::assertSame('source-ref', $attributes['metadata']['source']['reference']); + self::assertSame('^1.5.0', $attributes['metadata']['require']['composer/ca-bundle']); + } - $version = new Version(); - $version->setPackage($package); - $version->setName($package->getName()); - $version->setVersion('1.0.0'); - $version->setNormalizedVersion('1.0.0.0'); - $version->setDevelopment(false); - $version->setLicense([]); - $version->setAutoload([]); - $version->setDist(['reference' => 'old-dist-ref', 'type' => 'zip', 'url' => 'https://example.org/dist.zip']); + public function testVersionChangesGetRecorded(): void + { + $container = static::getContainer(); + $em = $container->get(ManagerRegistry::class)->getManager(); - $em->persist($package); - $em->persist($version); - $em->flush(); + $version = $this->createPackageAndVersion(); $version->setDist(['reference' => 'new-dist-ref', 'type' => 'zip', 'url' => 'https://example.org/dist.zip']); $version->setSource(['reference' => 'new-source-ref', 'type' => 'git', 'url' => 'git://example.org/dist.zip']); @@ -64,9 +74,9 @@ public function testVersionChangesGetRecorded(): void $em->flush(); $logs = $container->get(Connection::class)->fetchAllAssociative('SELECT * FROM audit_log ORDER BY id DESC'); - self::assertCount(2, $logs); // package creation + version reference change + self::assertCount(3, $logs); // package creation + version creation + version reference change self::assertSame(AuditRecordType::VersionReferenceChanged->value, $logs[0]['type']); - self::assertSame('{"name": "composer/composer", "dist_to": "new-dist-ref", "version": "1.0.0", "dist_from": "old-dist-ref", "source_to": "new-source-ref", "source_from": null}', $logs[0]['attributes']); + self::assertSame('{"name": "composer/composer", "dist_to": "new-dist-ref", "version": "1.0.0", "dist_from": "dist-ref", "source_to": "new-source-ref", "source_from": "source-ref"}', $logs[0]['attributes']); // verify that unrelated changes do not create new audit logs $version->setLicense(['MIT']); @@ -74,7 +84,7 @@ public function testVersionChangesGetRecorded(): void $em->flush(); $logs = $container->get(Connection::class)->fetchAllAssociative('SELECT * FROM audit_log ORDER BY id DESC'); - self::assertCount(2, $logs); // package creation + version reference change + self::assertCount(3, $logs); // package creation + version creation + version reference change // verify that changing dist only without ref change does not create new audit log and does not crash $version->setDist(['reference' => 'new-dist-ref', 'type' => 'zip2', 'url' => 'https://example.org/dist.zip2']); @@ -82,7 +92,7 @@ public function testVersionChangesGetRecorded(): void $em->flush(); $logs = $container->get(Connection::class)->fetchAllAssociative('SELECT * FROM audit_log ORDER BY id DESC'); - self::assertCount(2, $logs); // package creation + version reference change + self::assertCount(3, $logs); // package creation + version creation + version reference change // verify that only reference changes triggers a new audit log $version->setDist(['reference' => 'new-dist-ref', 'type' => 'zip3', 'url' => 'https://example.org/dist.zip2']); @@ -91,13 +101,47 @@ public function testVersionChangesGetRecorded(): void $em->flush(); $logs = $container->get(Connection::class)->fetchAllAssociative('SELECT * FROM audit_log ORDER BY id DESC'); - self::assertCount(2, $logs); + self::assertCount(3, $logs); - $em->remove($version); + $em->getRepository(Version::class)->remove($version); $em->flush(); $logs = $container->get(Connection::class)->fetchAllAssociative('SELECT * FROM audit_log ORDER BY id DESC'); - self::assertCount(3, $logs); + self::assertCount(4, $logs); self::assertSame(AuditRecordType::VersionDeleted->value, $logs[0]['type']); } + + private function createPackageAndVersion(): Version + { + $container = static::getContainer(); + $em = $container->get(ManagerRegistry::class)->getManager(); + + $package = new Package(); + $package->setName('composer/composer'); + $package->setRepository('https://github.com/composer/composer'); + + $version = new Version(); + $version->setPackage($package); + $version->setName($package->getName()); + $version->setVersion('1.0.0'); + $version->setNormalizedVersion('1.0.0.0'); + $version->setDevelopment(false); + $version->setLicense([]); + $version->setAutoload([]); + $version->setDist(['reference' => 'dist-ref', 'type' => 'zip', 'url' => 'https://example.org/dist.zip']); + $version->setSource(['reference' => 'source-ref', 'type' => 'git', 'url' => 'https://example.org/dist.git']); + + $link = new RequireLink(); + $link->setVersion($version); + $link->setPackageVersion('^1.5.0'); + $link->setPackageName('composer/ca-bundle'); + $version->addRequireLink($link); + + $em->persist($link); + $em->persist($package); + $em->persist($version); + $em->flush(); + + return $version; + } } diff --git a/translations/messages.en.yml b/translations/messages.en.yml index d01189971..0b0db6598 100644 --- a/translations/messages.en.yml +++ b/translations/messages.en.yml @@ -190,5 +190,6 @@ audit_log: user_created: User created user_deleted: User deleted username_changed: Username changed + version_created: Version created version_deleted: Version deleted version_reference_changed: Version reference changed