diff --git a/src/Controller/WatchListController.php b/src/Controller/WatchListController.php index 95fa000..1c8e82a 100644 --- a/src/Controller/WatchListController.php +++ b/src/Controller/WatchListController.php @@ -9,6 +9,7 @@ use App\Entity\User; use App\Entity\WatchList; use App\Repository\DomainEventRepository; use App\Repository\WatchListRepository; +use App\Service\RDAPService; use Doctrine\Common\Collections\Collection; use Eluceo\iCal\Domain\Entity\Calendar; use Eluceo\iCal\Presentation\Component\Property; @@ -29,6 +30,7 @@ class WatchListController extends AbstractController public function __construct( private readonly WatchListRepository $watchListRepository, private readonly DomainEventRepository $domainEventRepository, + private readonly RDAPService $RDAPService, ) { } @@ -110,9 +112,10 @@ class WatchListController extends AbstractController /** @var Domain $domain */ foreach ($watchList->getDomains()->getIterator() as $domain) { /** @var DomainEvent|null $exp */ - $exp = $this->domainEventRepository->findLastExpirationDomainEvent($domain); + $exp = $this->domainEventRepository->findLastDomainEvent($domain, 'expiration'); if (!$domain->getDeleted() && null !== $exp && !in_array($domain, $domains)) { + $domain->setExpiresInDays($this->RDAPService->getExpiresInDays($domain)); $domains[] = $domain; } } diff --git a/src/Entity/Domain.php b/src/Entity/Domain.php index 2c5d028..70bb835 100644 --- a/src/Entity/Domain.php +++ b/src/Entity/Domain.php @@ -148,6 +148,8 @@ class Domain #[Groups(['domain:item'])] private Collection $dnsKey; + private ?int $expiresInDays; + private const IMPORTANT_EVENTS = [EventAction::Deletion->value, EventAction::Expiration->value]; private const IMPORTANT_STATUS = [ 'redemption period', @@ -507,122 +509,6 @@ class Domain return in_array('pending delete', $this->getStatus()) && !in_array('redemption period', $this->getStatus()); } - /** - * @throws \DateMalformedIntervalStringException - */ - private function calculateDaysFromStatus(\DateTimeImmutable $now): ?int - { - $lastStatus = $this->getDomainStatuses()->first(); - if (false === $lastStatus) { - return null; - } - - if ($this->isPendingDelete() && ( - in_array('pending delete', $lastStatus->getAddStatus()) - || in_array('redemption period', $lastStatus->getDeleteStatus())) - ) { - return self::daysBetween($now, $lastStatus->getCreatedAt()->add(new \DateInterval('P'. 5 .'D'))); - } - - if ($this->isRedemptionPeriod() - && in_array('redemption period', $lastStatus->getAddStatus()) - ) { - return self::daysBetween($now, $lastStatus->getCreatedAt()->add(new \DateInterval('P'.(30 + 5).'D'))); - } - - return null; - } - - /* - private function calculateDaysFromEvents(\DateTimeImmutable $now): ?int - { - $lastChangedEvent = $this->getEvents()->findFirst(fn (int $i, DomainEvent $e) => !$e->getDeleted() && EventAction::LastChanged->value === $e->getAction()); - if (null === $lastChangedEvent) { - return null; - } - - if ($this->isRedemptionPeriod()) { - return self::daysBetween($now, $lastChangedEvent->getDate()->add(new \DateInterval('P'.(30 + 5).'D'))); - } - if ($this->isPendingDelete()) { - return self::daysBetween($now, $lastChangedEvent->getDate()->add(new \DateInterval('P'. 5 .'D'))); - } - - return null; - } - */ - - private static function daysBetween(\DateTimeImmutable $start, \DateTimeImmutable $end): int - { - $interval = $start->setTime(0, 0)->diff($end->setTime(0, 0)); - - return $interval->invert ? -$interval->days : $interval->days; - } - - private static function returnExpiresIn(array $guesses): ?int - { - $filteredGuesses = array_filter($guesses, function ($value) { - return null !== $value; - }); - - if (empty($filteredGuesses)) { - return null; - } - - return max(min($filteredGuesses), 0); - } - - /** - * @throws \Exception - */ - private function getRelevantDates(): array - { - $expiredAt = $deletedAt = null; - foreach ($this->getEvents()->getIterator() as $event) { - if (!$event->getDeleted()) { - if ('expiration' === $event->getAction()) { - $expiredAt = $event->getDate(); - } elseif ('deletion' === $event->getAction()) { - $deletedAt = $event->getDate(); - } - } - } - - return [$expiredAt, $deletedAt]; - } - - /** - * @throws \Exception - */ - #[Groups(['domain:item', 'domain:list'])] - public function getExpiresInDays(): ?int - { - if ($this->getDeleted()) { - return null; - } - - $now = new \DateTimeImmutable(); - [$expiredAt, $deletedAt] = $this->getRelevantDates(); - - if ($expiredAt) { - $guess = self::daysBetween($now, $expiredAt->add(new \DateInterval('P'.(45 + 30 + 5).'D'))); - } - - if ($deletedAt) { - // It has been observed that AFNIC, on the last day, adds a "deleted" event and removes the redemption period status. - if (0 === self::daysBetween($now, $deletedAt) && $this->isPendingDelete()) { - return 0; - } - - $guess = self::daysBetween($now, $deletedAt->add(new \DateInterval('P'. 30 .'D'))); - } - - return self::returnExpiresIn([ - $guess ?? null, - $this->calculateDaysFromStatus($now), - ]); - } - /** * @return Collection */ @@ -718,4 +604,17 @@ class Domain return $events; } + + #[Groups(['domain:item', 'domain:list'])] + public function getExpiresInDays(): ?int + { + return $this->expiresInDays; + } + + public function setExpiresInDays(?int $expiresInDays): static + { + $this->expiresInDays = $expiresInDays; + + return $this; + } } diff --git a/src/Repository/DomainEventRepository.php b/src/Repository/DomainEventRepository.php index 601f93e..4e07683 100644 --- a/src/Repository/DomainEventRepository.php +++ b/src/Repository/DomainEventRepository.php @@ -17,17 +17,18 @@ class DomainEventRepository extends ServiceEntityRepository parent::__construct($registry, DomainEvent::class); } - public function findLastExpirationDomainEvent(Domain $domain) + public function findLastDomainEvent(Domain $domain, string $action) { return $this->createQueryBuilder('de') ->select() ->where('de.domain = :domain') - ->andWhere('de.action = \'expiration\'') + ->andWhere('de.action = :action') ->andWhere('de.deleted = FALSE') ->orderBy('de.date', 'DESC') ->setMaxResults(1) ->getQuery() ->setParameter('domain', $domain) + ->setParameter('action', $action) ->getOneOrNullResult(); } diff --git a/src/Service/RDAPService.php b/src/Service/RDAPService.php index 6068c12..83ed7ab 100644 --- a/src/Service/RDAPService.php +++ b/src/Service/RDAPService.php @@ -23,6 +23,7 @@ use App\Exception\UnknownRdapServerException; use App\Repository\DomainEntityRepository; use App\Repository\DomainEventRepository; use App\Repository\DomainRepository; +use App\Repository\DomainStatusRepository; use App\Repository\EntityEventRepository; use App\Repository\EntityRepository; use App\Repository\IcannAccreditationRepository; @@ -79,7 +80,7 @@ class RDAPService private readonly StatService $statService, private readonly InfluxdbService $influxService, #[Autowire(param: 'influxdb_enabled')] - private readonly bool $influxdbEnabled, + private readonly bool $influxdbEnabled, private readonly DomainStatusRepository $domainStatusRepository, ) { } @@ -716,4 +717,113 @@ class RDAPService ]); } } + + private function calculateDaysFromStatus(Domain $domain, \DateTimeImmutable $now): ?int + { + /** @var ?DomainStatus $lastStatus */ + $lastStatus = $this->domainStatusRepository->createQueryBuilder('ds') + ->select() + ->where('ds.domain = :domain') + ->setParameter('domain', $domain) + ->orderBy('ds.createdAt', 'DESC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + + if (null === $lastStatus) { + return null; + } + + if ($domain->isPendingDelete() && ( + in_array('pending delete', $lastStatus->getAddStatus()) + || in_array('redemption period', $lastStatus->getDeleteStatus())) + ) { + return self::daysBetween($now, $lastStatus->getCreatedAt()->add(new \DateInterval('P'. 5 .'D'))); + } + + if ($domain->isRedemptionPeriod() + && in_array('redemption period', $lastStatus->getAddStatus()) + ) { + return self::daysBetween($now, $lastStatus->getCreatedAt()->add(new \DateInterval('P'.(30 + 5).'D'))); + } + + return null; + } + + private function getRelevantDates(Domain $domain): array + { + /** @var ?DomainEvent $expirationEvent */ + $expirationEvent = $this->domainEventRepository->findLastDomainEvent($domain, 'expiration'); + /** @var ?DomainEvent $deletionEvent */ + $deletionEvent = $this->domainEventRepository->findLastDomainEvent($domain, 'deletion'); + + return [$expirationEvent?->getDate(), $deletionEvent?->getDate()]; + } + + public function getExpiresInDays(Domain $domain): ?int + { + if ($domain->getDeleted()) { + return null; + } + + $now = new \DateTimeImmutable(); + [$expiredAt, $deletedAt] = $this->getRelevantDates($domain); + + if ($expiredAt) { + $guess = self::daysBetween($now, $expiredAt->add(new \DateInterval('P'.(45 + 30 + 5).'D'))); + } + + if ($deletedAt) { + // It has been observed that AFNIC, on the last day, adds a "deleted" event and removes the redemption period status. + if (0 === self::daysBetween($now, $deletedAt) && $domain->isPendingDelete()) { + return 0; + } + + $guess = self::daysBetween($now, $deletedAt->add(new \DateInterval('P'. 30 .'D'))); + } + + return self::returnExpiresIn([ + $guess ?? null, + $this->calculateDaysFromStatus($domain, $now), + ]); + } + + /* + private function calculateDaysFromEvents(\DateTimeImmutable $now): ?int + { + $lastChangedEvent = $this->getEvents()->findFirst(fn (int $i, DomainEvent $e) => !$e->getDeleted() && EventAction::LastChanged->value === $e->getAction()); + if (null === $lastChangedEvent) { + return null; + } + + if ($this->isRedemptionPeriod()) { + return self::daysBetween($now, $lastChangedEvent->getDate()->add(new \DateInterval('P'.(30 + 5).'D'))); + } + if ($this->isPendingDelete()) { + return self::daysBetween($now, $lastChangedEvent->getDate()->add(new \DateInterval('P'. 5 .'D'))); + } + + return null; + } + */ + + private static function daysBetween(\DateTimeImmutable $start, \DateTimeImmutable $end): int + { + $interval = $start->setTime(0, 0)->diff($end->setTime(0, 0)); + + return $interval->invert ? -$interval->days : $interval->days; + } + + private static function returnExpiresIn(array $guesses): ?int + { + $filteredGuesses = array_filter($guesses, function ($value) { + return null !== $value; + }); + + if (empty($filteredGuesses)) { + return null; + } + + return max(min($filteredGuesses), 0); + } }