diff --git a/src/Command/RegisterDomainCommand.php b/src/Command/RegisterDomainCommand.php index a59b26d..98e5a88 100644 --- a/src/Command/RegisterDomainCommand.php +++ b/src/Command/RegisterDomainCommand.php @@ -48,7 +48,7 @@ class RegisterDomainCommand extends Command try { if (null !== $domain && !$force) { - if (!$domain->isToBeUpdated(true, true)) { + if (!$this->rdapService->isToBeUpdated($domain, true, true)) { $io->warning('The domain name is already present in the database and does not need to be updated at this time.'); return Command::SUCCESS; diff --git a/src/Controller/WatchListController.php b/src/Controller/WatchListController.php index 1c8e82a..0224aaf 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\CalendarService; use App\Service\RDAPService; use Doctrine\Common\Collections\Collection; use Eluceo\iCal\Domain\Entity\Calendar; @@ -30,7 +31,7 @@ class WatchListController extends AbstractController public function __construct( private readonly WatchListRepository $watchListRepository, private readonly DomainEventRepository $domainEventRepository, - private readonly RDAPService $RDAPService, + private readonly RDAPService $RDAPService, private readonly CalendarService $calendarService, ) { } @@ -74,7 +75,7 @@ class WatchListController extends AbstractController /** @var Domain $domain */ foreach ($watchList->getDomains()->getIterator() as $domain) { - foreach ($domain->getDomainCalendarEvents() as $event) { + foreach ($this->calendarService->getDomainCalendarEvents($domain) as $event) { $calendar->addEvent($event); } } diff --git a/src/Entity/Domain.php b/src/Entity/Domain.php index 70bb835..7049513 100644 --- a/src/Entity/Domain.php +++ b/src/Entity/Domain.php @@ -14,18 +14,6 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; -use Eluceo\iCal\Domain\Entity\Attendee; -use Eluceo\iCal\Domain\Entity\Event; -use Eluceo\iCal\Domain\Enum\EventStatus; -use Eluceo\iCal\Domain\ValueObject\Category; -use Eluceo\iCal\Domain\ValueObject\Date; -use Eluceo\iCal\Domain\ValueObject\EmailAddress; -use Eluceo\iCal\Domain\ValueObject\SingleDay; -use Eluceo\iCal\Domain\ValueObject\Timestamp; -use Sabre\VObject\EofException; -use Sabre\VObject\InvalidDataException; -use Sabre\VObject\ParseException; -use Sabre\VObject\Reader; use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Serializer\Attribute\SerializedName; @@ -387,7 +375,7 @@ class Domain * * @throws \Exception */ - protected function isToBeWatchClosely(): bool + public function isToBeWatchClosely(): bool { $status = $this->getStatus(); if ((!empty($status) && count(array_intersect($status, self::IMPORTANT_STATUS))) || $this->getDeleted()) { @@ -404,47 +392,6 @@ class Domain return !empty($events) && in_array($events[0]->getAction(), self::IMPORTANT_EVENTS); } - /** - * Returns true if one or more of these conditions are met: - * - It has been more than 7 days since the domain name was last updated - * - It has been more than 12 minutes and the domain name has statuses that suggest it is not stable - * - It has been more than 1 day and the domain name is blocked in DNS - * - * @throws \Exception - */ - public function isToBeUpdated(bool $fromUser = true, bool $intensifyLastDay = false): bool - { - $updatedAtDiff = $this->getUpdatedAt()->diff(new \DateTimeImmutable()); - - if ($updatedAtDiff->days >= 7) { - return true; - } - - if ($this->getDeleted()) { - return $fromUser; - } - - $expiresIn = $this->getExpiresInDays(); - - if ($intensifyLastDay && (0 === $expiresIn || 1 === $expiresIn)) { - return true; - } - - $minutesDiff = $updatedAtDiff->h * 60 + $updatedAtDiff->i; - if (($minutesDiff >= 12 || $fromUser) && $this->isToBeWatchClosely()) { - return true; - } - - if ( - count(array_intersect($this->getStatus(), ['auto renew period', 'client hold', 'server hold'])) > 0 - && $updatedAtDiff->days >= 1 - ) { - return true; - } - - return false; - } - /** * @return Collection */ @@ -539,72 +486,6 @@ class Domain return $this; } - /** - * @return Event[] - * - * @throws ParseException - * @throws EofException - * @throws InvalidDataException - * @throws \Exception - */ - public function getDomainCalendarEvents(): array - { - $events = []; - $attendees = []; - - /* @var DomainEntity $entity */ - foreach ($this->getDomainEntities()->filter(fn (DomainEntity $domainEntity) => !$domainEntity->getDeletedAt())->getIterator() as $domainEntity) { - $jCard = $domainEntity->getEntity()->getJCard(); - - if (empty($jCard)) { - continue; - } - - $vCardData = Reader::readJson($jCard); - - if (empty($vCardData->EMAIL) || empty($vCardData->FN)) { - continue; - } - - $email = (string) $vCardData->EMAIL; - - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - continue; - } - - $attendees[] = (new Attendee(new EmailAddress($email)))->setDisplayName((string) $vCardData->FN); - } - - /** @var DomainEvent $event */ - foreach ($this->getEvents()->filter(fn (DomainEvent $e) => $e->getDate()->diff(new \DateTimeImmutable('now'))->y <= 10)->getIterator() as $event) { - $events[] = (new Event()) - ->setLastModified(new Timestamp($this->getUpdatedAt())) - ->setStatus(EventStatus::CONFIRMED()) - ->setSummary($this->getLdhName().': '.$event->getAction()) - ->addCategory(new Category($event->getAction())) - ->setAttendees($attendees) - ->setOccurrence(new SingleDay(new Date($event->getDate())) - ); - } - - $expiresInDays = $this->getExpiresInDays(); - - if (null !== $expiresInDays) { - $events[] = (new Event()) - ->setLastModified(new Timestamp($this->getUpdatedAt())) - ->setStatus(EventStatus::CONFIRMED()) - ->setSummary($this->getLdhName().': estimated WHOIS release date') - ->addCategory(new Category('release')) - ->setAttendees($attendees) - ->setOccurrence(new SingleDay(new Date( - (new \DateTimeImmutable())->setTime(0, 0)->add(new \DateInterval('P'.$expiresInDays.'D')) - )) - ); - } - - return $events; - } - #[Groups(['domain:item', 'domain:list'])] public function getExpiresInDays(): ?int { diff --git a/src/MessageHandler/UpdateDomainsFromWatchlistHandler.php b/src/MessageHandler/UpdateDomainsFromWatchlistHandler.php index e4a02fd..e6bdf1f 100644 --- a/src/MessageHandler/UpdateDomainsFromWatchlistHandler.php +++ b/src/MessageHandler/UpdateDomainsFromWatchlistHandler.php @@ -96,7 +96,7 @@ final readonly class UpdateDomainsFromWatchlistHandler */ /** @var Domain $domain */ - foreach ($watchList->getDomains()->filter(fn ($domain) => $domain->isToBeUpdated(false, null !== $watchList->getConnector())) as $domain + foreach ($watchList->getDomains()->filter(fn ($domain) => $this->RDAPService->isToBeUpdated($domain, false, null !== $watchList->getConnector())) as $domain ) { $updatedAt = $domain->getUpdatedAt(); $deleted = $domain->getDeleted(); diff --git a/src/Service/CalendarService.php b/src/Service/CalendarService.php new file mode 100644 index 0000000..e58eb7d --- /dev/null +++ b/src/Service/CalendarService.php @@ -0,0 +1,93 @@ +getDomainEntities()->filter(fn (DomainEntity $domainEntity) => !$domainEntity->getDeletedAt())->getIterator() as $domainEntity) { + $jCard = $domainEntity->getEntity()->getJCard(); + + if (empty($jCard)) { + continue; + } + + $vCardData = Reader::readJson($jCard); + + if (empty($vCardData->EMAIL) || empty($vCardData->FN)) { + continue; + } + + $email = (string) $vCardData->EMAIL; + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + continue; + } + + $attendees[] = (new Attendee(new EmailAddress($email)))->setDisplayName((string) $vCardData->FN); + } + + /** @var DomainEvent $event */ + foreach ($domain->getEvents()->filter(fn (DomainEvent $e) => $e->getDate()->diff(new \DateTimeImmutable('now'))->y <= 10)->getIterator() as $event) { + $events[] = (new Event()) + ->setLastModified(new Timestamp($domain->getUpdatedAt())) + ->setStatus(EventStatus::CONFIRMED()) + ->setSummary($domain->getLdhName().': '.$event->getAction()) + ->addCategory(new Category($event->getAction())) + ->setAttendees($attendees) + ->setOccurrence(new SingleDay(new Date($event->getDate())) + ); + } + + $expiresInDays = $this->RDAPService->getExpiresInDays($domain); + + if (null !== $expiresInDays) { + $events[] = (new Event()) + ->setLastModified(new Timestamp($domain->getUpdatedAt())) + ->setStatus(EventStatus::CONFIRMED()) + ->setSummary($domain->getLdhName().': estimated WHOIS release date') + ->addCategory(new Category('release')) + ->setAttendees($attendees) + ->setOccurrence(new SingleDay(new Date( + (new \DateTimeImmutable())->setTime(0, 0)->add(new \DateInterval('P'.$expiresInDays.'D')) + )) + ); + } + + return $events; + } +} diff --git a/src/Service/RDAPService.php b/src/Service/RDAPService.php index 83ed7ab..9604cf5 100644 --- a/src/Service/RDAPService.php +++ b/src/Service/RDAPService.php @@ -788,6 +788,47 @@ class RDAPService ]); } + /** + * Returns true if one or more of these conditions are met: + * - It has been more than 7 days since the domain name was last updated + * - It has been more than 12 minutes and the domain name has statuses that suggest it is not stable + * - It has been more than 1 day and the domain name is blocked in DNS + * + * @throws \Exception + */ + public function isToBeUpdated(Domain $domain, bool $fromUser = true, bool $intensifyLastDay = false): bool + { + $updatedAtDiff = $domain->getUpdatedAt()->diff(new \DateTimeImmutable()); + + if ($updatedAtDiff->days >= 7) { + return true; + } + + if ($domain->getDeleted()) { + return $fromUser; + } + + $expiresIn = $this->getExpiresInDays($domain); + + if ($intensifyLastDay && (0 === $expiresIn || 1 === $expiresIn)) { + return true; + } + + $minutesDiff = $updatedAtDiff->h * 60 + $updatedAtDiff->i; + if (($minutesDiff >= 12 || $fromUser) && $domain->isToBeWatchClosely()) { + return true; + } + + if ( + count(array_intersect($domain->getStatus(), ['auto renew period', 'client hold', 'server hold'])) > 0 + && $updatedAtDiff->days >= 1 + ) { + return true; + } + + return false; + } + /* private function calculateDaysFromEvents(\DateTimeImmutable $now): ?int { diff --git a/src/State/AutoRegisterDomainProvider.php b/src/State/AutoRegisterDomainProvider.php index 80337e5..5810dba 100644 --- a/src/State/AutoRegisterDomainProvider.php +++ b/src/State/AutoRegisterDomainProvider.php @@ -6,9 +6,14 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\Entity\Domain; use App\Entity\WatchList; +use App\Exception\DomainNotFoundException; +use App\Exception\MalformedDomainException; +use App\Exception\TldNotSupportedException; +use App\Exception\UnknownRdapServerException; use App\Message\SendDomainEventNotif; use App\Repository\DomainRepository; use App\Service\RDAPService; +use Doctrine\ORM\OptimisticLockException; use Psr\Log\LoggerInterface; use Random\Randomizer; use Symfony\Bundle\SecurityBundle\Security; @@ -16,8 +21,14 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\Messenger\Exception\ExceptionInterface; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\RateLimiter\RateLimiterFactory; +use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; readonly class AutoRegisterDomainProvider implements ProviderInterface { @@ -34,6 +45,20 @@ readonly class AutoRegisterDomainProvider implements ProviderInterface ) { } + /** + * @throws RedirectionExceptionInterface + * @throws DomainNotFoundException + * @throws TldNotSupportedException + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + * @throws OptimisticLockException + * @throws TransportExceptionInterface + * @throws MalformedDomainException + * @throws ServerExceptionInterface + * @throws UnknownRdapServerException + * @throws ExceptionInterface + * @throws \Exception + */ public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { $userId = $this->security->getUser()->getUserIdentifier(); @@ -51,7 +76,7 @@ readonly class AutoRegisterDomainProvider implements ProviderInterface // If the domain name exists in the database, recently updated and not important, we return the stored Domain if (null !== $domain && !$domain->getDeleted() - && !$domain->isToBeUpdated(true, true) + && !$this->RDAPService->isToBeUpdated($domain, true, true) && ($request && !filter_var($request->get('forced', false), FILTER_VALIDATE_BOOLEAN)) ) { $this->logger->debug('It is not necessary to update the domain name', [ diff --git a/tests/Entity/DomainTest.php b/tests/Entity/DomainTest.php index 3d5fa06..6cc659f 100644 --- a/tests/Entity/DomainTest.php +++ b/tests/Entity/DomainTest.php @@ -8,6 +8,7 @@ use App\Entity\Domain; use App\Entity\DomainEvent; use App\Entity\DomainStatus; use App\Exception\MalformedDomainException; +use App\Service\RDAPService; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -49,9 +50,6 @@ final class DomainTest extends TestCase ); } - /** - * @throws \Exception - */ public function testGetExpiresInDays(): void { $this->assertNull( @@ -235,17 +233,21 @@ final class DomainTest extends TestCase array $status, bool $expected, ): void { - $mock = $this->getMockBuilder(Domain::class) - ->onlyMethods(['getUpdatedAt', 'getDeleted', 'getExpiresInDays', 'isToBeWatchClosely', 'getStatus']) + $rdapServiceMock = $this->getMockBuilder(RDAPService::class) + ->onlyMethods(['getExpiresInDays']) ->getMock(); - $mock->method('getUpdatedAt')->willReturn($updatedAt); - $mock->method('getDeleted')->willReturn($deleted); - $mock->method('getExpiresInDays')->willReturn($expiresIn); - $mock->method('isToBeWatchClosely')->willReturn($watchClosely); - $mock->method('getStatus')->willReturn($status); + $domainMock = $this->getMockBuilder(Domain::class) + ->onlyMethods(['getUpdatedAt', 'getDeleted', 'isToBeWatchClosely', 'getStatus']) + ->getMock(); - $result = $mock->isToBeUpdated($fromUser, $intensifyLastDay); + $domainMock->method('getUpdatedAt')->willReturn($updatedAt); + $domainMock->method('getDeleted')->willReturn($deleted); + $rdapServiceMock->method('getExpiresInDays')->willReturn($expiresIn); + $domainMock->method('isToBeWatchClosely')->willReturn($watchClosely); + $domainMock->method('getStatus')->willReturn($status); + + $result = $rdapServiceMock->isToBeUpdated($domainMock, $fromUser, $intensifyLastDay); $this->assertEquals($expected, $result); } diff --git a/tests/State/AutoRegisterDomainProviderTest.php b/tests/State/AutoRegisterDomainProviderTest.php index b3d0e53..2a4f390 100644 --- a/tests/State/AutoRegisterDomainProviderTest.php +++ b/tests/State/AutoRegisterDomainProviderTest.php @@ -33,12 +33,12 @@ final class AutoRegisterDomainProviderTest extends ApiTestCase $client = AutoRegisterDomainProviderTest::createClientWithCredentials(AutoRegisterDomainProviderTest::getToken(UserFactory::createOne())); $mockedDomain = $this->getMockBuilder(Domain::class)->getMock(); - $mockedDomain->method('isToBeUpdated')->willReturn(false); $mockedDomainRepository = $this->createMock(DomainRepository::class); $mockedDomainRepository->method('findOneBy')->willReturn($mockedDomain); $rdapServiceMocked = $this->createMock(RDAPService::class); + $rdapServiceMocked->method('isToBeUpdated')->willReturn(false); $rdapServiceMocked->expects(self::never())->method('registerDomain'); $container = static::getContainer();