mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-29 16:15:04 +00:00
chore: merge develop
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Api\Extension;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
|
||||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use App\Entity\Entity;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class NotNullAccreditationIcannExtension implements QueryCollectionExtensionInterface
|
||||
{
|
||||
public function applyToCollection(
|
||||
QueryBuilder $queryBuilder,
|
||||
QueryNameGeneratorInterface $queryNameGenerator,
|
||||
string $resourceClass,
|
||||
?Operation $operation = null,
|
||||
array $context = [],
|
||||
): void {
|
||||
if (Entity::class !== $resourceClass) {
|
||||
return;
|
||||
}
|
||||
if ($operation && 'icann_accreditations_collection' === $operation->getName()) {
|
||||
$rootAlias = $queryBuilder->getRootAliases()[0];
|
||||
$queryBuilder->andWhere(sprintf('%s.icannAccreditation.status IS NOT NULL', $rootAlias));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Message\ProcessWatchListsTrigger;
|
||||
use App\Message\ProcessWatchlistTrigger;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
@@ -12,10 +12,10 @@ use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:process-watchlists',
|
||||
description: 'Process watchlists and send emails if necessary',
|
||||
name: 'app:process-watchlist',
|
||||
description: 'Process watchlist and send emails if necessary',
|
||||
)]
|
||||
class ProcessWatchlistsCommand extends Command
|
||||
class ProcessWatchlistCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly MessageBusInterface $bus)
|
||||
{
|
||||
@@ -33,7 +33,7 @@ class ProcessWatchlistsCommand extends Command
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$this->bus->dispatch(new ProcessWatchListsTrigger());
|
||||
$this->bus->dispatch(new ProcessWatchlistTrigger());
|
||||
|
||||
$io->success('Watchlist processing triggered!');
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Service\RDAPService;
|
||||
@@ -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;
|
||||
@@ -60,11 +60,11 @@ class RegisterDomainCommand extends Command
|
||||
|
||||
if ($notif) {
|
||||
$randomizer = new Randomizer();
|
||||
$watchLists = $randomizer->shuffleArray($domain->getWatchLists()->toArray());
|
||||
$watchlists = $randomizer->shuffleArray($domain->getWatchlists()->toArray());
|
||||
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($watchLists as $watchList) {
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
/** @var Watchlist $watchlist */
|
||||
foreach ($watchlists as $watchlist) {
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchlist->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
use App\Service\Connector\AutodnsProvider;
|
||||
use App\Service\Connector\EppClientProvider;
|
||||
use App\Service\Connector\GandiProvider;
|
||||
use App\Service\Connector\NamecheapProvider;
|
||||
use App\Service\Connector\NameComProvider;
|
||||
use App\Service\Connector\OvhProvider;
|
||||
use App\Service\Provider\AutodnsProvider;
|
||||
use App\Service\Provider\EppClientProvider;
|
||||
use App\Service\Provider\GandiProvider;
|
||||
use App\Service\Provider\NamecheapProvider;
|
||||
use App\Service\Provider\NameComProvider;
|
||||
use App\Service\Provider\OvhProvider;
|
||||
|
||||
enum ConnectorProvider: string
|
||||
{
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
enum TriggerAction: string
|
||||
{
|
||||
case SendEmail = 'email';
|
||||
case SendChat = 'chat';
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\WatchList;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Service\RDAPService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Random\Randomizer;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
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\DecodingExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
|
||||
class DomainRefreshController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly DomainRepository $domainRepository,
|
||||
private readonly RDAPService $RDAPService,
|
||||
private readonly RateLimiterFactory $rdapRequestsLimiter,
|
||||
private readonly MessageBusInterface $bus,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly KernelInterface $kernel,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws DecodingExceptionInterface
|
||||
* @throws ExceptionInterface
|
||||
* @throws \Exception
|
||||
* @throws HttpExceptionInterface
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function __invoke(string $ldhName, Request $request): Domain
|
||||
{
|
||||
$idnDomain = RDAPService::convertToIdn($ldhName);
|
||||
$userId = $this->getUser()->getUserIdentifier();
|
||||
|
||||
$this->logger->info('User {username} wants to update the domain name {idnDomain}.', [
|
||||
'username' => $userId,
|
||||
'idnDomain' => $idnDomain,
|
||||
]);
|
||||
|
||||
/** @var ?Domain $domain */
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $idnDomain]);
|
||||
// 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->kernel->isDebug()
|
||||
&& true !== filter_var($request->get('forced', false), FILTER_VALIDATE_BOOLEAN)
|
||||
) {
|
||||
$this->logger->info('It is not necessary to update the information of the domain name {idnDomain} with the RDAP protocol.', [
|
||||
'idnDomain' => $idnDomain,
|
||||
]);
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
if (false === $this->kernel->isDebug() && true === $this->getParameter('limited_features')) {
|
||||
$limiter = $this->rdapRequestsLimiter->create($userId);
|
||||
$limit = $limiter->consume();
|
||||
|
||||
if (!$limit->isAccepted()) {
|
||||
throw new TooManyRequestsHttpException($limit->getRetryAfter()->getTimestamp() - time());
|
||||
}
|
||||
}
|
||||
|
||||
$updatedAt = null === $domain ? new \DateTimeImmutable('now') : $domain->getUpdatedAt();
|
||||
$domain = $this->RDAPService->registerDomain($idnDomain);
|
||||
|
||||
$randomizer = new Randomizer();
|
||||
$watchLists = $randomizer->shuffleArray($domain->getWatchLists()->toArray());
|
||||
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($watchLists as $watchList) {
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,19 @@ namespace App\Controller;
|
||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class HomeController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly RouterInterface $router)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RouterInterface $router,
|
||||
private readonly ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/', name: 'index')]
|
||||
@@ -25,7 +29,10 @@ class HomeController extends AbstractController
|
||||
#[Route(path: '/login/oauth', name: 'oauth_connect')]
|
||||
public function connectAction(ClientRegistry $clientRegistry): Response
|
||||
{
|
||||
return $clientRegistry->getClient('oauth')->redirect([], []);
|
||||
if ($this->parameterBag->get('oauth_enabled')) {
|
||||
return $clientRegistry->getClient('oauth')->redirect([], []);
|
||||
}
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
#[Route(path: '/logout', name: 'logout')]
|
||||
|
||||
@@ -21,6 +21,7 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class RegistrationController extends AbstractController
|
||||
{
|
||||
@@ -33,6 +34,7 @@ class RegistrationController extends AbstractController
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly KernelInterface $kernel,
|
||||
private readonly ValidatorInterface $validator,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -44,7 +46,7 @@ class RegistrationController extends AbstractController
|
||||
name: 'user_register',
|
||||
defaults: [
|
||||
'_api_resource_class' => User::class,
|
||||
'_api_operation_name' => 'register',
|
||||
'_api_operation_name' => 'user_register',
|
||||
],
|
||||
methods: ['POST']
|
||||
)]
|
||||
@@ -64,22 +66,21 @@ class RegistrationController extends AbstractController
|
||||
}
|
||||
|
||||
$user = $this->serializer->deserialize($request->getContent(), User::class, 'json', ['groups' => 'user:register']);
|
||||
if (null === $user->getEmail() || null === $user->getPassword()) {
|
||||
throw new BadRequestHttpException('Bad request');
|
||||
$violations = $this->validator->validate($user);
|
||||
|
||||
if ($violations->count() > 0) {
|
||||
throw new BadRequestHttpException($violations->get(0));
|
||||
}
|
||||
|
||||
$user->setPassword(
|
||||
$userPasswordHasher->hashPassword(
|
||||
$user,
|
||||
$user->getPassword()
|
||||
$user->getPlainPassword()
|
||||
)
|
||||
);
|
||||
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
)->setCreatedAt(new \DateTimeImmutable());
|
||||
|
||||
if (false === (bool) $this->getParameter('registration_verify_email')) {
|
||||
$user->setVerified(true);
|
||||
$user->setVerifiedAt($user->getCreatedAt());
|
||||
} else {
|
||||
$email = $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
|
||||
(new TemplatedEmail())
|
||||
@@ -91,13 +92,16 @@ class RegistrationController extends AbstractController
|
||||
);
|
||||
|
||||
$signedUrl = (string) $email->getContext()['signedUrl'];
|
||||
$this->logger->notice('The validation link for user {username} is {signedUrl}', [
|
||||
$this->logger->notice('The validation link for this user is generated', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'signedUrl' => $signedUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->logger->info('A new user has registered ({username}).', [
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
$this->logger->info('New user has registered', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
@@ -121,7 +125,7 @@ class RegistrationController extends AbstractController
|
||||
|
||||
$this->emailVerifier->handleEmailConfirmation($request, $user);
|
||||
|
||||
$this->logger->info('User {username} has validated his email address.', [
|
||||
$this->logger->info('User has validated his email address', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Statistics;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -15,7 +15,7 @@ class StatisticsController extends AbstractController
|
||||
public function __construct(
|
||||
private readonly CacheItemPoolInterface $pool,
|
||||
private readonly DomainRepository $domainRepository,
|
||||
private readonly WatchListRepository $watchListRepository,
|
||||
private readonly WatchlistRepository $watchlistRepository,
|
||||
private readonly KernelInterface $kernel,
|
||||
) {
|
||||
}
|
||||
@@ -34,22 +34,10 @@ class StatisticsController extends AbstractController
|
||||
->setAlertSent($this->pool->getItem('stats.alert.sent')->get() ?? 0)
|
||||
|
||||
->setDomainTracked(
|
||||
$this->getCachedItem('stats.domain.tracked', fn () => $this->watchListRepository->createQueryBuilder('w')
|
||||
->join('w.domains', 'd')
|
||||
->select('COUNT(DISTINCT d.ldhName)')
|
||||
->where('d.deleted = FALSE')
|
||||
->getQuery()->getSingleColumnResult()[0])
|
||||
$this->getCachedItem('stats.domain.tracked', fn () => $this->watchlistRepository->getTrackedDomainCount())
|
||||
)
|
||||
->setDomainCount(
|
||||
$this->getCachedItem('stats.domain.count', fn () => $this->domainRepository->createQueryBuilder('d')
|
||||
->join('d.tld', 't')
|
||||
->select('t.tld tld')
|
||||
->addSelect('COUNT(d.ldhName) AS domain')
|
||||
->addGroupBy('t.tld')
|
||||
->where('d.deleted = FALSE')
|
||||
->orderBy('domain', 'DESC')
|
||||
->setMaxResults(5)
|
||||
->getQuery()->getArrayResult())
|
||||
$this->getCachedItem('stats.domain.count', fn () => $this->domainRepository->getActiveDomainCountByTld())
|
||||
)
|
||||
->setDomainCountTotal(
|
||||
$this->getCachedItem('stats.domain.total', fn () => $this->domainRepository->count(['deleted' => false])
|
||||
|
||||
@@ -6,8 +6,11 @@ use App\Entity\Domain;
|
||||
use App\Entity\DomainEvent;
|
||||
use App\Entity\DomainStatus;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WatchList;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\Service\CalendarService;
|
||||
use App\Service\RDAPService;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Eluceo\iCal\Domain\Entity\Calendar;
|
||||
use Eluceo\iCal\Presentation\Component\Property;
|
||||
@@ -23,10 +26,13 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class WatchListController extends AbstractController
|
||||
class WatchlistController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly WatchListRepository $watchListRepository,
|
||||
private readonly WatchlistRepository $watchlistRepository,
|
||||
private readonly RDAPService $RDAPService,
|
||||
private readonly CalendarService $calendarService,
|
||||
private readonly DomainRepository $domainRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -34,17 +40,17 @@ class WatchListController extends AbstractController
|
||||
path: '/api/watchlists',
|
||||
name: 'watchlist_get_all_mine',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_resource_class' => Watchlist::class,
|
||||
'_api_operation_name' => 'get_all_mine',
|
||||
],
|
||||
methods: ['GET']
|
||||
)]
|
||||
public function getWatchLists(): Collection
|
||||
public function getWatchlists(): Collection
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
return $user->getWatchLists();
|
||||
return $user->getWatchlists();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,26 +63,26 @@ class WatchListController extends AbstractController
|
||||
path: '/api/watchlists/{token}/calendar',
|
||||
name: 'watchlist_calendar',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_resource_class' => Watchlist::class,
|
||||
'_api_operation_name' => 'calendar',
|
||||
]
|
||||
)]
|
||||
public function getWatchlistCalendar(string $token): Response
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $token]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $token]);
|
||||
|
||||
$calendar = new Calendar();
|
||||
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->getIterator() as $domain) {
|
||||
foreach ($domain->getDomainCalendarEvents() as $event) {
|
||||
foreach ($watchlist->getDomains()->getIterator() as $domain) {
|
||||
foreach ($this->calendarService->getDomainCalendarEvents($domain) as $event) {
|
||||
$calendar->addEvent($event);
|
||||
}
|
||||
}
|
||||
|
||||
$calendarResponse = (new CalendarFactory())->createCalendar($calendar);
|
||||
$calendarName = $watchList->getName();
|
||||
$calendarName = $watchlist->getName();
|
||||
if (null !== $calendarName) {
|
||||
$calendarResponse->withProperty(new Property('X-WR-CALNAME', new TextValue($calendarName)));
|
||||
}
|
||||
@@ -93,7 +99,7 @@ class WatchListController extends AbstractController
|
||||
path: '/api/tracked',
|
||||
name: 'watchlist_get_tracked_domains',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_resource_class' => Watchlist::class,
|
||||
'_api_operation_name' => 'get_tracked_domains',
|
||||
]
|
||||
)]
|
||||
@@ -102,18 +108,9 @@ class WatchListController extends AbstractController
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$domains = [];
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($user->getWatchLists()->getIterator() as $watchList) {
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->getIterator() as $domain) {
|
||||
/** @var DomainEvent|null $exp */
|
||||
$exp = $domain->getEvents()->findFirst(fn (int $key, DomainEvent $e) => !$e->getDeleted() && 'expiration' === $e->getAction());
|
||||
|
||||
if (!$domain->getDeleted() && null !== $exp && !in_array($domain, $domains)) {
|
||||
$domains[] = $domain;
|
||||
}
|
||||
}
|
||||
$domains = $this->domainRepository->getMyTrackedDomains($user);
|
||||
foreach ($domains as $domain) {
|
||||
$domain->setExpiresInDays($this->RDAPService->getExpiresInDays($domain));
|
||||
}
|
||||
|
||||
usort($domains, fn (Domain $d1, Domain $d2) => $d1->getExpiresInDays() - $d2->getExpiresInDays());
|
||||
@@ -128,14 +125,14 @@ class WatchListController extends AbstractController
|
||||
path: '/api/watchlists/{token}/rss/events',
|
||||
name: 'watchlist_rss_events',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_resource_class' => Watchlist::class,
|
||||
'_api_operation_name' => 'rss_events',
|
||||
]
|
||||
)]
|
||||
public function getWatchlistRssEventsFeed(string $token, Request $request): Response
|
||||
{
|
||||
/** @var WatchList $watchlist */
|
||||
$watchlist = $this->watchListRepository->findOneBy(['token' => $token]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $token]);
|
||||
|
||||
$feed = (new Feed())
|
||||
->setLanguage('en')
|
||||
@@ -166,14 +163,14 @@ class WatchListController extends AbstractController
|
||||
path: '/api/watchlists/{token}/rss/status',
|
||||
name: 'watchlist_rss_status',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_resource_class' => Watchlist::class,
|
||||
'_api_operation_name' => 'rss_status',
|
||||
]
|
||||
)]
|
||||
public function getWatchlistRssStatusFeed(string $token, Request $request): Response
|
||||
{
|
||||
/** @var WatchList $watchlist */
|
||||
$watchlist = $this->watchListRepository->findOneBy(['token' => $token]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $token]);
|
||||
|
||||
$feed = (new Feed())
|
||||
->setLanguage('en')
|
||||
15
src/DataFixtures/AppFixtures.php
Normal file
15
src/DataFixtures/AppFixtures.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\DataFixtures;
|
||||
|
||||
use App\Story\DefaultUsersStory;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
class AppFixtures extends Fixture
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
DefaultUsersStory::load();
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ use Symfony\Component\Uid\Uuid;
|
||||
),
|
||||
new Get(
|
||||
normalizationContext: ['groups' => 'connector:list'],
|
||||
security: 'object.user == user'
|
||||
security: 'object.getUser() == user'
|
||||
),
|
||||
new Post(
|
||||
normalizationContext: ['groups' => ['connector:create', 'connector:list']],
|
||||
@@ -35,7 +35,7 @@ use Symfony\Component\Uid\Uuid;
|
||||
processor: ConnectorCreateProcessor::class
|
||||
),
|
||||
new Delete(
|
||||
security: 'object.user == user',
|
||||
security: 'object.getUser() == user',
|
||||
processor: ConnectorDeleteProcessor::class
|
||||
),
|
||||
]
|
||||
@@ -61,10 +61,10 @@ class Connector
|
||||
private array $authData = [];
|
||||
|
||||
/**
|
||||
* @var Collection<int, WatchList>
|
||||
* @var Collection<int, Watchlist>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: WatchList::class, mappedBy: 'connector')]
|
||||
private Collection $watchLists;
|
||||
#[ORM\OneToMany(targetEntity: Watchlist::class, mappedBy: 'connector')]
|
||||
private Collection $watchlists;
|
||||
|
||||
#[Groups(['connector:list', 'watchlist:list'])]
|
||||
#[ORM\Column]
|
||||
@@ -76,7 +76,7 @@ class Connector
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = Uuid::v4();
|
||||
$this->watchLists = new ArrayCollection();
|
||||
$this->watchlists = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
@@ -121,29 +121,29 @@ class Connector
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WatchList>
|
||||
* @return Collection<int, Watchlist>
|
||||
*/
|
||||
public function getWatchLists(): Collection
|
||||
public function getWatchlists(): Collection
|
||||
{
|
||||
return $this->watchLists;
|
||||
return $this->watchlists;
|
||||
}
|
||||
|
||||
public function addWatchList(WatchList $watchList): static
|
||||
public function addWatchlist(Watchlist $watchlist): static
|
||||
{
|
||||
if (!$this->watchLists->contains($watchList)) {
|
||||
$this->watchLists->add($watchList);
|
||||
$watchList->setConnector($this);
|
||||
if (!$this->watchlists->contains($watchlist)) {
|
||||
$this->watchlists->add($watchlist);
|
||||
$watchlist->setConnector($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeWatchList(WatchList $watchList): static
|
||||
public function removeWatchlist(Watchlist $watchlist): static
|
||||
{
|
||||
if ($this->watchLists->removeElement($watchList)) {
|
||||
if ($this->watchlists->removeElement($watchlist)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($watchList->getConnector() === $this) {
|
||||
$watchList->setConnector(null);
|
||||
if ($watchlist->getConnector() === $this) {
|
||||
$watchlist->setConnector(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,6 @@ class Connector
|
||||
|
||||
public function getWatchlistCount(): ?int
|
||||
{
|
||||
return $this->watchLists->count();
|
||||
return $this->watchlists->count();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,21 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\QueryParameter;
|
||||
use App\Config\EventAction;
|
||||
use App\Controller\DomainRefreshController;
|
||||
use App\Exception\MalformedDomainException;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Service\RDAPService;
|
||||
use App\State\AutoRegisterDomainProvider;
|
||||
use App\State\FindDomainCollectionFromEntityProvider;
|
||||
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;
|
||||
|
||||
@@ -40,9 +32,23 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
]
|
||||
),
|
||||
*/
|
||||
new GetCollection(
|
||||
uriTemplate: '/domains',
|
||||
normalizationContext: [
|
||||
'groups' => [
|
||||
'domain:list',
|
||||
'tld:list',
|
||||
'event:list',
|
||||
'event:list',
|
||||
],
|
||||
],
|
||||
provider: FindDomainCollectionFromEntityProvider::class,
|
||||
parameters: [
|
||||
'registrant' => new QueryParameter(description: 'The exact name of the registrant (case insensitive)', required: true),
|
||||
]
|
||||
),
|
||||
new Get(
|
||||
uriTemplate: '/domains/{ldhName}', // Do not delete this line, otherwise Symfony interprets the TLD of the domain name as a return type
|
||||
controller: DomainRefreshController::class,
|
||||
normalizationContext: [
|
||||
'groups' => [
|
||||
'domain:item',
|
||||
@@ -54,7 +60,6 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
'ds:list',
|
||||
],
|
||||
],
|
||||
read: false
|
||||
),
|
||||
],
|
||||
provider: AutoRegisterDomainProvider::class
|
||||
@@ -75,25 +80,30 @@ class Domain
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: DomainEvent::class, mappedBy: 'domain', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[Groups(['domain:item', 'domain:list', 'watchlist:list'])]
|
||||
#[ApiProperty(
|
||||
openapiContext: [
|
||||
'type' => 'array',
|
||||
]
|
||||
)]
|
||||
private Collection $events;
|
||||
|
||||
/**
|
||||
* @var Collection<int, DomainEntity>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: DomainEntity::class, mappedBy: 'domain', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[Groups(['domain:item'])]
|
||||
#[Groups(['domain:item', 'watchlist:item'])]
|
||||
#[SerializedName('entities')]
|
||||
private Collection $domainEntities;
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['domain:item', 'domain:list', 'watchlist:item', 'watchlist:list'])]
|
||||
private array $status = [];
|
||||
|
||||
/**
|
||||
* @var Collection<int, WatchList>
|
||||
* @var Collection<int, Watchlist>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: WatchList::class, mappedBy: 'domains', cascade: ['persist'])]
|
||||
private Collection $watchLists;
|
||||
#[ORM\ManyToMany(targetEntity: Watchlist::class, mappedBy: 'domains', cascade: ['persist'])]
|
||||
private Collection $watchlists;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Nameserver>
|
||||
@@ -103,7 +113,7 @@ class Domain
|
||||
joinColumns: [new ORM\JoinColumn(name: 'domain_ldh_name', referencedColumnName: 'ldh_name')],
|
||||
inverseJoinColumns: [new ORM\JoinColumn(name: 'nameserver_ldh_name', referencedColumnName: 'ldh_name')]
|
||||
)]
|
||||
#[Groups(['domain:item'])]
|
||||
#[Groups(['domain:item', 'watchlist:item'])]
|
||||
private Collection $nameservers;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
@@ -135,7 +145,7 @@ class Domain
|
||||
|
||||
#[ORM\Column(nullable: false, options: ['default' => false])]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private ?bool $delegationSigned = null;
|
||||
private bool $delegationSigned = false;
|
||||
|
||||
/**
|
||||
* @var Collection<int, DnsKey>
|
||||
@@ -144,6 +154,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',
|
||||
@@ -160,7 +172,7 @@ class Domain
|
||||
{
|
||||
$this->events = new ArrayCollection();
|
||||
$this->domainEntities = new ArrayCollection();
|
||||
$this->watchLists = new ArrayCollection();
|
||||
$this->watchlists = new ArrayCollection();
|
||||
$this->nameservers = new ArrayCollection();
|
||||
$this->updatedAt = new \DateTimeImmutable('now');
|
||||
$this->createdAt = $this->updatedAt;
|
||||
@@ -174,6 +186,9 @@ class Domain
|
||||
return $this->ldhName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MalformedDomainException
|
||||
*/
|
||||
public function setLdhName(string $ldhName): static
|
||||
{
|
||||
$this->ldhName = RDAPService::convertToIdn($ldhName);
|
||||
@@ -266,27 +281,27 @@ class Domain
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WatchList>
|
||||
* @return Collection<int, Watchlist>
|
||||
*/
|
||||
public function getWatchLists(): Collection
|
||||
public function getWatchlists(): Collection
|
||||
{
|
||||
return $this->watchLists;
|
||||
return $this->watchlists;
|
||||
}
|
||||
|
||||
public function addWatchList(WatchList $watchList): static
|
||||
public function addWatchlists(Watchlist $watchlist): static
|
||||
{
|
||||
if (!$this->watchLists->contains($watchList)) {
|
||||
$this->watchLists->add($watchList);
|
||||
$watchList->addDomain($this);
|
||||
if (!$this->watchlists->contains($watchlist)) {
|
||||
$this->watchlists->add($watchlist);
|
||||
$watchlist->addDomain($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeWatchList(WatchList $watchList): static
|
||||
public function removeWatchlists(Watchlist $watchlist): static
|
||||
{
|
||||
if ($this->watchLists->removeElement($watchList)) {
|
||||
$watchList->removeDomain($this);
|
||||
if ($this->watchlists->removeElement($watchlist)) {
|
||||
$watchlist->removeDomain($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -333,7 +348,7 @@ class Domain
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function setUpdatedAt(?\DateTimeImmutable $updatedAt): void
|
||||
public function setUpdatedAt(?\DateTimeImmutable $updatedAt): void
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
}
|
||||
@@ -378,7 +393,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()) {
|
||||
@@ -395,47 +410,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<int, DomainStatus>
|
||||
*/
|
||||
@@ -500,122 +474,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()->last();
|
||||
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<int, DnsKey>
|
||||
*/
|
||||
@@ -646,69 +504,16 @@ class Domain
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Event[]
|
||||
*
|
||||
* @throws ParseException
|
||||
* @throws EofException
|
||||
* @throws InvalidDataException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getDomainCalendarEvents(): array
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
public function getExpiresInDays(): ?int
|
||||
{
|
||||
$events = [];
|
||||
$attendees = [];
|
||||
return $this->expiresInDays;
|
||||
}
|
||||
|
||||
/* @var DomainEntity $entity */
|
||||
foreach ($this->getDomainEntities()->filter(fn (DomainEntity $domainEntity) => !$domainEntity->getDeleted())->getIterator() as $domainEntity) {
|
||||
$jCard = $domainEntity->getEntity()->getJCard();
|
||||
public function setExpiresInDays(?int $expiresInDays): static
|
||||
{
|
||||
$this->expiresInDays = $expiresInDays;
|
||||
|
||||
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;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,18 +23,13 @@ class DomainEntity
|
||||
#[Groups(['domain-entity:entity'])]
|
||||
private ?Entity $entity = null;
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
#[Groups(['domain-entity:entity', 'domain-entity:domain'])]
|
||||
private array $roles = [];
|
||||
|
||||
#[ORM\Column]
|
||||
#[ORM\Column(nullable: true)]
|
||||
#[Groups(['domain-entity:entity', 'domain-entity:domain'])]
|
||||
private ?bool $deleted;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->deleted = false;
|
||||
}
|
||||
private ?\DateTimeImmutable $deletedAt = null;
|
||||
|
||||
public function getDomain(): ?Domain
|
||||
{
|
||||
@@ -75,14 +70,14 @@ class DomainEntity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeleted(): ?bool
|
||||
public function getDeletedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->deleted;
|
||||
return $this->deletedAt;
|
||||
}
|
||||
|
||||
public function setDeleted(?bool $deleted): static
|
||||
public function setDeletedAt(?\DateTimeImmutable $deletedAt): static
|
||||
{
|
||||
$this->deleted = $deleted;
|
||||
$this->deletedAt = $deletedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ class DomainStatus
|
||||
#[Groups(['domain:item'])]
|
||||
private \DateTimeImmutable $date;
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['domain:item'])]
|
||||
private array $addStatus = [];
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['domain:item'])]
|
||||
private array $deleteStatus = [];
|
||||
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use App\Repository\EntityRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\ORM\Mapping\Embedded;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
|
||||
@@ -20,31 +17,6 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
)]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/entities/icann-accreditations',
|
||||
openapiContext: [
|
||||
'parameters' => [
|
||||
[
|
||||
'name' => 'icannAccreditation.status',
|
||||
'in' => 'query',
|
||||
'required' => true,
|
||||
'schema' => [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['Accredited', 'Terminated', 'Reserved'],
|
||||
],
|
||||
],
|
||||
'style' => 'form',
|
||||
'explode' => true,
|
||||
'description' => 'Filter by ICANN accreditation status',
|
||||
],
|
||||
],
|
||||
],
|
||||
description: 'ICANN Registrar IDs list',
|
||||
normalizationContext: ['groups' => ['entity:list']],
|
||||
name: 'icann_accreditations_collection'
|
||||
),
|
||||
/*
|
||||
new GetCollection(
|
||||
uriTemplate: '/entities',
|
||||
@@ -67,12 +39,6 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
*/
|
||||
]
|
||||
)]
|
||||
#[ApiFilter(
|
||||
SearchFilter::class,
|
||||
properties: [
|
||||
'icannAccreditation.status' => 'exact',
|
||||
]
|
||||
)]
|
||||
class Entity
|
||||
{
|
||||
#[ORM\Id]
|
||||
@@ -81,12 +47,12 @@ class Entity
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Tld::class, inversedBy: 'entities')]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'tld', nullable: true)]
|
||||
#[Groups(['entity:list', 'entity:item', 'domain:item'])]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'tld', nullable: false)]
|
||||
#[Groups(['entity:list', 'entity:item', 'domain:item', 'watchlist:item'])]
|
||||
private ?Tld $tld = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['entity:list', 'entity:item', 'domain:item'])]
|
||||
#[Groups(['entity:list', 'entity:item', 'domain:item', 'watchlist:item'])]
|
||||
private ?string $handle = null;
|
||||
|
||||
/**
|
||||
@@ -112,15 +78,21 @@ class Entity
|
||||
#[Groups(['entity:item', 'domain:item'])]
|
||||
private Collection $events;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['entity:item', 'domain:item'])]
|
||||
#[ORM\Column(type: 'json')]
|
||||
#[ApiProperty(
|
||||
openapiContext: [
|
||||
'type' => 'array',
|
||||
'items' => ['type' => 'array'],
|
||||
]
|
||||
)]
|
||||
#[Groups(['entity:item', 'domain:item', 'watchlist:item'])]
|
||||
private array $jCard = [];
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
#[Groups(['entity:item', 'domain:item'])]
|
||||
#[Groups(['entity:item', 'domain:item', 'watchlist:item'])]
|
||||
private ?array $remarks = null;
|
||||
|
||||
#[Embedded(class: IcannAccreditation::class, columnPrefix: 'icann_')]
|
||||
#[ORM\ManyToOne(inversedBy: 'entities')]
|
||||
#[Groups(['entity:list', 'entity:item', 'domain:item'])]
|
||||
private ?IcannAccreditation $icannAccreditation = null;
|
||||
|
||||
@@ -129,7 +101,6 @@ class Entity
|
||||
$this->domainEntities = new ArrayCollection();
|
||||
$this->nameserverEntities = new ArrayCollection();
|
||||
$this->events = new ArrayCollection();
|
||||
$this->icannAccreditation = new IcannAccreditation();
|
||||
}
|
||||
|
||||
public function getHandle(): ?string
|
||||
@@ -284,11 +255,13 @@ class Entity
|
||||
|
||||
public function getIcannAccreditation(): ?IcannAccreditation
|
||||
{
|
||||
return null === $this->icannAccreditation->getStatus() ? null : $this->icannAccreditation;
|
||||
return $this->icannAccreditation;
|
||||
}
|
||||
|
||||
public function setIcannAccreditation(?IcannAccreditation $icannAccreditation): void
|
||||
public function setIcannAccreditation(?IcannAccreditation $icannAccreditation): static
|
||||
{
|
||||
$this->icannAccreditation = $icannAccreditation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,35 +2,92 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use App\Config\RegistrarStatus;
|
||||
use App\Repository\IcannAccreditationRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\ORM\Mapping\Embeddable;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[Embeddable]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/icann-accreditations',
|
||||
openapiContext: [
|
||||
'parameters' => [
|
||||
[
|
||||
'name' => 'status',
|
||||
'in' => 'query',
|
||||
'required' => true,
|
||||
'schema' => [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['Accredited', 'Terminated', 'Reserved'],
|
||||
],
|
||||
],
|
||||
'style' => 'form',
|
||||
'explode' => true,
|
||||
'description' => 'Filter by ICANN accreditation status',
|
||||
],
|
||||
],
|
||||
],
|
||||
shortName: 'ICANN Accreditation',
|
||||
description: 'ICANN Registrar IDs list',
|
||||
normalizationContext: ['groups' => ['icann:list']]
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[ApiFilter(
|
||||
SearchFilter::class,
|
||||
properties: [
|
||||
'status' => 'exact',
|
||||
]
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: IcannAccreditationRepository::class)]
|
||||
class IcannAccreditation
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column]
|
||||
#[Groups(['icann:item', 'icann:list', 'domain:item'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['entity:item', 'entity:list', 'domain:item'])]
|
||||
#[Groups(['icann:item', 'icann:list', 'domain:item'])]
|
||||
private ?string $registrarName = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['entity:item', 'domain:item'])]
|
||||
#[Groups(['icann:item'])]
|
||||
private ?string $rdapBaseUrl = null;
|
||||
|
||||
#[ORM\Column(nullable: true, enumType: RegistrarStatus::class)]
|
||||
#[Groups(['entity:item', 'entity:list', 'domain:item'])]
|
||||
#[Groups(['icann:item', 'icann:list', 'domain:item'])]
|
||||
private ?RegistrarStatus $status = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
#[Groups(['entity:item', 'entity:list', 'domain:item'])]
|
||||
#[Groups(['icann:item', 'icann:list', 'domain:item'])]
|
||||
private ?\DateTimeImmutable $updated = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
#[Groups(['entity:item', 'entity:list', 'domain:item'])]
|
||||
#[Groups(['icann:item', 'icann:list', 'domain:item'])]
|
||||
private ?\DateTimeImmutable $date = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Entity>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Entity::class, mappedBy: 'icannAccreditation')]
|
||||
private Collection $entities;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->entities = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getRegistrarName(): ?string
|
||||
{
|
||||
return $this->registrarName;
|
||||
@@ -90,4 +147,46 @@ class IcannAccreditation
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(?int $id): static
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Entity>
|
||||
*/
|
||||
public function getEntities(): Collection
|
||||
{
|
||||
return $this->entities;
|
||||
}
|
||||
|
||||
public function addEntity(Entity $entity): static
|
||||
{
|
||||
if (!$this->entities->contains($entity)) {
|
||||
$this->entities->add($entity);
|
||||
$entity->setIcannAccreditation($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeEntity(Entity $entity): static
|
||||
{
|
||||
if ($this->entities->removeElement($entity)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($entity->getIcannAccreditation() === $this) {
|
||||
$entity->setIcannAccreditation(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ class Nameserver
|
||||
* @var Collection<int, Domain>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Domain::class, mappedBy: 'nameservers')]
|
||||
#[Groups(['nameserver:item'])]
|
||||
private Collection $domains;
|
||||
|
||||
public function __construct()
|
||||
|
||||
@@ -23,11 +23,11 @@ class NameserverEntity
|
||||
#[Groups(['nameserver-entity:entity'])]
|
||||
private ?Entity $entity = null;
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
#[Groups(['nameserver-entity:entity', 'nameserver-entity:nameserver'])]
|
||||
private array $roles = [];
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
#[Groups(['nameserver-entity:entity', 'nameserver-entity:nameserver'])]
|
||||
private array $status = [];
|
||||
|
||||
|
||||
@@ -77,6 +77,9 @@ class Tld
|
||||
#[ORM\OneToMany(targetEntity: Entity::class, mappedBy: 'tld')]
|
||||
private Collection $entities;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $deletedAt = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->rdapServers = new ArrayCollection();
|
||||
@@ -241,4 +244,16 @@ class Tld
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeletedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->deletedAt;
|
||||
}
|
||||
|
||||
public function setDeletedAt(?\DateTimeImmutable $deletedAt): static
|
||||
{
|
||||
$this->deletedAt = $deletedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
|
||||
@@ -46,6 +48,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
|
||||
#[ORM\Column(length: 180)]
|
||||
#[Groups(['user:list', 'user:register'])]
|
||||
#[Assert\Email]
|
||||
#[Assert\NotBlank]
|
||||
private ?string $email = null;
|
||||
|
||||
/**
|
||||
@@ -59,14 +63,13 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
* @var string|null The hashed password
|
||||
*/
|
||||
#[ORM\Column(nullable: true)]
|
||||
#[Groups(['user:register'])]
|
||||
private ?string $password = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, WatchList>
|
||||
* @var Collection<int, Watchlist>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: WatchList::class, mappedBy: 'user', orphanRemoval: true)]
|
||||
private Collection $watchLists;
|
||||
#[ORM\OneToMany(targetEntity: Watchlist::class, mappedBy: 'user', orphanRemoval: true)]
|
||||
private Collection $watchlists;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Connector>
|
||||
@@ -75,11 +78,20 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
private Collection $connectors;
|
||||
|
||||
#[ORM\Column]
|
||||
private bool $isVerified = false;
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $verifiedAt = null;
|
||||
|
||||
#[Assert\PasswordStrength]
|
||||
#[Assert\NotBlank]
|
||||
#[SerializedName('password')]
|
||||
#[Groups(['user:register'])]
|
||||
private ?string $plainPassword = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->watchLists = new ArrayCollection();
|
||||
$this->watchlists = new ArrayCollection();
|
||||
$this->connectors = new ArrayCollection();
|
||||
}
|
||||
|
||||
@@ -152,33 +164,33 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// If you store any temporary, sensitive data on the user, clear it here
|
||||
// $this->plainPassword = null;
|
||||
$this->plainPassword = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WatchList>
|
||||
* @return Collection<int, Watchlist>
|
||||
*/
|
||||
public function getWatchLists(): Collection
|
||||
public function getWatchlists(): Collection
|
||||
{
|
||||
return $this->watchLists;
|
||||
return $this->watchlists;
|
||||
}
|
||||
|
||||
public function addWatchList(WatchList $watchList): static
|
||||
public function addWatchlist(Watchlist $watchlist): static
|
||||
{
|
||||
if (!$this->watchLists->contains($watchList)) {
|
||||
$this->watchLists->add($watchList);
|
||||
$watchList->setUser($this);
|
||||
if (!$this->watchlists->contains($watchlist)) {
|
||||
$this->watchlists->add($watchlist);
|
||||
$watchlist->setUser($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeWatchList(WatchList $watchList): static
|
||||
public function removeWatchlist(Watchlist $watchlist): static
|
||||
{
|
||||
if ($this->watchLists->removeElement($watchList)) {
|
||||
if ($this->watchlists->removeElement($watchlist)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($watchList->getUser() === $this) {
|
||||
$watchList->setUser(null);
|
||||
if ($watchlist->getUser() === $this) {
|
||||
$watchlist->setUser(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,14 +227,38 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isVerified(): bool
|
||||
public function getCreatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->isVerified;
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setVerified(bool $isVerified): static
|
||||
public function setCreatedAt(\DateTimeImmutable $createdAt): static
|
||||
{
|
||||
$this->isVerified = $isVerified;
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVerifiedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->verifiedAt;
|
||||
}
|
||||
|
||||
public function setVerifiedAt(\DateTimeImmutable $verifiedAt): static
|
||||
{
|
||||
$this->verifiedAt = $verifiedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPlainPassword(): ?string
|
||||
{
|
||||
return $this->plainPassword;
|
||||
}
|
||||
|
||||
public function setPlainPassword(?string $plainPassword): self
|
||||
{
|
||||
$this->plainPassword = $plainPassword;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Link;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Config\TriggerAction;
|
||||
use App\Repository\EventTriggerRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ORM\Entity(repositoryClass: EventTriggerRepository::class)]
|
||||
#[ApiResource(
|
||||
uriTemplate: '/watchlists/{watchListId}/triggers/{action}/{event}',
|
||||
shortName: 'Watchlist Trigger',
|
||||
operations: [
|
||||
new Get(),
|
||||
new GetCollection(
|
||||
uriTemplate: '/watchlists/{watchListId}/triggers',
|
||||
uriVariables: [
|
||||
'watchListId' => new Link(fromProperty: 'token', toProperty: 'watchList', fromClass: WatchList::class),
|
||||
],
|
||||
),
|
||||
new Post(
|
||||
uriTemplate: '/watchlist-triggers',
|
||||
uriVariables: [],
|
||||
security: 'true'
|
||||
),
|
||||
new Delete(),
|
||||
],
|
||||
uriVariables: [
|
||||
'watchListId' => new Link(fromProperty: 'token', toProperty: 'watchList', fromClass: WatchList::class),
|
||||
'action' => 'action',
|
||||
'event' => 'event',
|
||||
],
|
||||
security: 'object.getWatchList().user == user',
|
||||
)]
|
||||
class WatchListTrigger
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(length: 255, nullable: false)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?string $event;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\ManyToOne(targetEntity: WatchList::class, inversedBy: 'watchListTriggers')]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'token', nullable: false, onDelete: 'CASCADE')]
|
||||
private ?WatchList $watchList;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(nullable: false, enumType: TriggerAction::class)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?TriggerAction $action;
|
||||
|
||||
public function getEvent(): ?string
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
public function setEvent(string $event): static
|
||||
{
|
||||
$this->event = $event;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWatchList(): ?WatchList
|
||||
{
|
||||
return $this->watchList;
|
||||
}
|
||||
|
||||
public function setWatchList(?WatchList $watchList): static
|
||||
{
|
||||
$this->watchList = $watchList;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAction(): ?TriggerAction
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function setAction(TriggerAction $action): static
|
||||
{
|
||||
$this->action = $action;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@ use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\State\WatchListUpdateProcessor;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\State\WatchlistUpdateProcessor;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
@@ -17,8 +18,9 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: WatchListRepository::class)]
|
||||
#[ORM\Entity(repositoryClass: WatchlistRepository::class)]
|
||||
#[ApiResource(
|
||||
shortName: 'Watchlist',
|
||||
operations: [
|
||||
@@ -41,7 +43,6 @@ use Symfony\Component\Uid\Uuid;
|
||||
'domain:list',
|
||||
'tld:list',
|
||||
'event:list',
|
||||
'domain:list',
|
||||
'event:list',
|
||||
],
|
||||
],
|
||||
@@ -51,7 +52,7 @@ use Symfony\Component\Uid\Uuid;
|
||||
normalizationContext: [
|
||||
'groups' => [
|
||||
'watchlist:item',
|
||||
'domain:item',
|
||||
'domain:list',
|
||||
'event:list',
|
||||
'domain-entity:entity',
|
||||
'nameserver-entity:nameserver',
|
||||
@@ -59,7 +60,7 @@ use Symfony\Component\Uid\Uuid;
|
||||
'tld:item',
|
||||
],
|
||||
],
|
||||
security: 'object.user == user'
|
||||
security: 'object.getUser() == user'
|
||||
),
|
||||
new Get(
|
||||
routeName: 'watchlist_calendar',
|
||||
@@ -86,16 +87,22 @@ use Symfony\Component\Uid\Uuid;
|
||||
new Post(
|
||||
normalizationContext: ['groups' => 'watchlist:list'],
|
||||
denormalizationContext: ['groups' => 'watchlist:create'],
|
||||
processor: WatchListUpdateProcessor::class,
|
||||
processor: WatchlistUpdateProcessor::class,
|
||||
),
|
||||
new Put(
|
||||
normalizationContext: ['groups' => 'watchlist:item'],
|
||||
denormalizationContext: ['groups' => ['watchlist:create', 'watchlist:token']],
|
||||
security: 'object.user == user',
|
||||
processor: WatchListUpdateProcessor::class,
|
||||
normalizationContext: ['groups' => 'watchlist:list'],
|
||||
denormalizationContext: ['groups' => ['watchlist:update']],
|
||||
security: 'object.getUser() == user',
|
||||
processor: WatchlistUpdateProcessor::class,
|
||||
),
|
||||
new Patch(
|
||||
normalizationContext: ['groups' => 'watchlist:list'],
|
||||
denormalizationContext: ['groups' => ['watchlist:update']],
|
||||
security: 'object.getUser() == user',
|
||||
processor: WatchlistUpdateProcessor::class,
|
||||
),
|
||||
new Delete(
|
||||
security: 'object.user == user'
|
||||
security: 'object.getUser() == user'
|
||||
),
|
||||
new Get(
|
||||
routeName: 'watchlist_rss_status',
|
||||
@@ -145,9 +152,9 @@ use Symfony\Component\Uid\Uuid;
|
||||
),
|
||||
],
|
||||
)]
|
||||
class WatchList
|
||||
class Watchlist
|
||||
{
|
||||
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'watchLists')]
|
||||
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'watchlists')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
public ?User $user = null;
|
||||
|
||||
@@ -159,27 +166,19 @@ class WatchList
|
||||
/**
|
||||
* @var Collection<int, Domain>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Domain::class, inversedBy: 'watchLists')]
|
||||
#[ORM\JoinTable(name: 'watch_lists_domains',
|
||||
joinColumns: [new ORM\JoinColumn(name: 'watch_list_token', referencedColumnName: 'token', onDelete: 'CASCADE')],
|
||||
#[ORM\ManyToMany(targetEntity: Domain::class, inversedBy: 'watchlists')]
|
||||
#[ORM\JoinTable(name: 'watchlist_domains',
|
||||
joinColumns: [new ORM\JoinColumn(name: 'watchlist_token', referencedColumnName: 'token', onDelete: 'CASCADE')],
|
||||
inverseJoinColumns: [new ORM\JoinColumn(name: 'domain_ldh_name', referencedColumnName: 'ldh_name', onDelete: 'CASCADE')])]
|
||||
#[Groups(['watchlist:create', 'watchlist:list', 'watchlist:item'])]
|
||||
#[Groups(['watchlist:create', 'watchlist:list', 'watchlist:item', 'watchlist:update'])]
|
||||
private Collection $domains;
|
||||
|
||||
/**
|
||||
* @var Collection<int, WatchListTrigger>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: WatchListTrigger::class, mappedBy: 'watchList', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
#[SerializedName('triggers')]
|
||||
private Collection $watchListTriggers;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'watchLists')]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
#[ORM\ManyToOne(inversedBy: 'watchlists')]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
private ?Connector $connector = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column]
|
||||
@@ -187,15 +186,42 @@ class WatchList
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[SerializedName('dsn')]
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
#[ORM\Column(type: Types::JSON, nullable: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Assert\Unique]
|
||||
#[Assert\All([
|
||||
new Assert\Type('string'),
|
||||
new Assert\NotBlank(),
|
||||
])]
|
||||
private ?array $webhookDsn = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Assert\Unique]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\All([
|
||||
new Assert\Type('string'),
|
||||
new Assert\NotBlank(),
|
||||
])]
|
||||
private array $trackedEvents = [];
|
||||
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Assert\Unique]
|
||||
#[Assert\All([
|
||||
new Assert\Type('string'),
|
||||
new Assert\NotBlank(),
|
||||
])]
|
||||
private array $trackedEppStatus = [];
|
||||
|
||||
#[ORM\Column(type: Types::BOOLEAN)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
private ?bool $enabled = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->token = Uuid::v4();
|
||||
$this->domains = new ArrayCollection();
|
||||
$this->watchListTriggers = new ArrayCollection();
|
||||
$this->createdAt = new \DateTimeImmutable('now');
|
||||
}
|
||||
|
||||
@@ -245,36 +271,6 @@ class WatchList
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, WatchListTrigger>
|
||||
*/
|
||||
public function getWatchListTriggers(): Collection
|
||||
{
|
||||
return $this->watchListTriggers;
|
||||
}
|
||||
|
||||
public function addWatchListTrigger(WatchListTrigger $watchListTrigger): static
|
||||
{
|
||||
if (!$this->watchListTriggers->contains($watchListTrigger)) {
|
||||
$this->watchListTriggers->add($watchListTrigger);
|
||||
$watchListTrigger->setWatchList($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeWatchListTrigger(WatchListTrigger $watchListTrigger): static
|
||||
{
|
||||
if ($this->watchListTriggers->removeElement($watchListTrigger)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($watchListTrigger->getWatchList() === $this) {
|
||||
$watchListTrigger->setWatchList(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getConnector(): ?Connector
|
||||
{
|
||||
return $this->connector;
|
||||
@@ -322,4 +318,40 @@ class WatchList
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTrackedEvents(): array
|
||||
{
|
||||
return $this->trackedEvents;
|
||||
}
|
||||
|
||||
public function setTrackedEvents(array $trackedEvents): static
|
||||
{
|
||||
$this->trackedEvents = $trackedEvents;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTrackedEppStatus(): array
|
||||
{
|
||||
return $this->trackedEppStatus;
|
||||
}
|
||||
|
||||
public function setTrackedEppStatus(array $trackedEppStatus): static
|
||||
{
|
||||
$this->trackedEppStatus = $trackedEppStatus;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isEnabled(): ?bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function setEnabled(bool $enabled): static
|
||||
{
|
||||
$this->enabled = $enabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
11
src/Exception/DomainNotFoundException.php
Normal file
11
src/Exception/DomainNotFoundException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class DomainNotFoundException extends \Exception
|
||||
{
|
||||
public static function fromDomain(string $ldhName): self
|
||||
{
|
||||
return new self("The domain name $ldhName is not present in the WHOIS database");
|
||||
}
|
||||
}
|
||||
11
src/Exception/MalformedDomainException.php
Normal file
11
src/Exception/MalformedDomainException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class MalformedDomainException extends \Exception
|
||||
{
|
||||
public static function fromDomain(string $ldhName): self
|
||||
{
|
||||
return new self("Malformed domain name ($ldhName)");
|
||||
}
|
||||
}
|
||||
7
src/Exception/Provider/AbstractProviderException.php
Normal file
7
src/Exception/Provider/AbstractProviderException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
abstract class AbstractProviderException extends \Exception
|
||||
{
|
||||
}
|
||||
7
src/Exception/Provider/DomainOrderFailedExeption.php
Normal file
7
src/Exception/Provider/DomainOrderFailedExeption.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class DomainOrderFailedExeption extends AbstractProviderException
|
||||
{
|
||||
}
|
||||
11
src/Exception/Provider/EppContactIsAvailableException.php
Normal file
11
src/Exception/Provider/EppContactIsAvailableException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class EppContactIsAvailableException extends AbstractProviderException
|
||||
{
|
||||
public static function fromContact(string $handle): self
|
||||
{
|
||||
return new self("At least one of the entered contacts cannot be used because it is indicated as available ($handle)");
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/ExpiredLoginException.php
Normal file
11
src/Exception/Provider/ExpiredLoginException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class ExpiredLoginException extends AbstractProviderException
|
||||
{
|
||||
public static function fromIdentifier(string $identifier): self
|
||||
{
|
||||
return new self("Expired login for identifier $identifier");
|
||||
}
|
||||
}
|
||||
16
src/Exception/Provider/InvalidLoginException.php
Normal file
16
src/Exception/Provider/InvalidLoginException.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class InvalidLoginException extends AbstractProviderException
|
||||
{
|
||||
public function __construct(string $message = '')
|
||||
{
|
||||
parent::__construct('' === $message ? 'The status of these credentials is not valid' : $message);
|
||||
}
|
||||
|
||||
public static function fromIdentifier(string $identifier): self
|
||||
{
|
||||
return new self("Invalid login for identifier $identifier");
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/InvalidLoginStatusException.php
Normal file
11
src/Exception/Provider/InvalidLoginStatusException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class InvalidLoginStatusException extends AbstractProviderException
|
||||
{
|
||||
public static function fromStatus(string $status): self
|
||||
{
|
||||
return new self("The status of these credentials is not valid ($status)");
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/NamecheapRequiresAddressException.php
Normal file
11
src/Exception/Provider/NamecheapRequiresAddressException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class NamecheapRequiresAddressException extends AbstractProviderException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Namecheap account requires at least one address to purchase a domain');
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/PermissionErrorException.php
Normal file
11
src/Exception/Provider/PermissionErrorException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class PermissionErrorException extends AbstractProviderException
|
||||
{
|
||||
public static function fromIdentifier(string $identifier): self
|
||||
{
|
||||
return new self("Not enough permissions for identifier $identifier");
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/ProviderGenericErrorException.php
Normal file
11
src/Exception/Provider/ProviderGenericErrorException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class ProviderGenericErrorException extends AbstractProviderException
|
||||
{
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
||||
11
src/Exception/Provider/UserNoExplicitConsentException.php
Normal file
11
src/Exception/Provider/UserNoExplicitConsentException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception\Provider;
|
||||
|
||||
class UserNoExplicitConsentException extends AbstractProviderException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('The user has not given explicit consent');
|
||||
}
|
||||
}
|
||||
11
src/Exception/TldNotSupportedException.php
Normal file
11
src/Exception/TldNotSupportedException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class TldNotSupportedException extends \Exception
|
||||
{
|
||||
public static function fromTld(string $tld): self
|
||||
{
|
||||
return new self("The requested TLD $tld is not yet supported, please try again with another one");
|
||||
}
|
||||
}
|
||||
11
src/Exception/UnknownRdapServerException.php
Normal file
11
src/Exception/UnknownRdapServerException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class UnknownRdapServerException extends \Exception
|
||||
{
|
||||
public static function fromTld(string $tld): self
|
||||
{
|
||||
return new self("TLD $tld: Unable to determine which RDAP server to contact");
|
||||
}
|
||||
}
|
||||
11
src/Exception/UnsupportedDsnSchemeException.php
Normal file
11
src/Exception/UnsupportedDsnSchemeException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class UnsupportedDsnSchemeException extends \Exception
|
||||
{
|
||||
public static function fromScheme(string $scheme): UnsupportedDsnSchemeException
|
||||
{
|
||||
return new UnsupportedDsnSchemeException("The DSN scheme ($scheme) is not supported");
|
||||
}
|
||||
}
|
||||
64
src/Factory/UserFactory.php
Normal file
64
src/Factory/UserFactory.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Factory;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
|
||||
|
||||
/**
|
||||
* @extends PersistentObjectFactory<User>
|
||||
*/
|
||||
final class UserFactory extends PersistentObjectFactory
|
||||
{
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
|
||||
*
|
||||
* @todo inject services if required
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function class(): string
|
||||
{
|
||||
return User::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
|
||||
*
|
||||
* @todo add your default values here
|
||||
*/
|
||||
#[\Override]
|
||||
protected function defaults(): array|callable
|
||||
{
|
||||
$createdAt = \DateTimeImmutable::createFromMutable(self::faker()->dateTime());
|
||||
$plainPassword = self::faker()->password(16, 20);
|
||||
|
||||
return [
|
||||
'createdAt' => $createdAt,
|
||||
'verifiedAt' => $createdAt,
|
||||
'email' => self::faker()->unique()->safeEmail(),
|
||||
'plainPassword' => $plainPassword,
|
||||
'roles' => ['ROLE_USER'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
|
||||
*/
|
||||
#[\Override]
|
||||
protected function initialize(): static
|
||||
{
|
||||
return $this->afterInstantiate(function (User $user): void {
|
||||
if ($user->getPlainPassword()) {
|
||||
$user->setPassword(
|
||||
$this->passwordHasher->hashPassword($user, $user->getPlainPassword())
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace App\Message;
|
||||
final class OrderDomain
|
||||
{
|
||||
public function __construct(
|
||||
public string $watchListToken,
|
||||
public string $watchlistToken,
|
||||
public string $ldhName,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
final class ProcessWatchListsTrigger
|
||||
final class ProcessWatchlistTrigger
|
||||
{
|
||||
/*
|
||||
* Add whatever properties and methods you need
|
||||
@@ -5,7 +5,7 @@ namespace App\Message;
|
||||
final class SendDomainEventNotif
|
||||
{
|
||||
public function __construct(
|
||||
public string $watchListToken,
|
||||
public string $watchlistToken,
|
||||
public string $ldhName,
|
||||
public \DateTimeImmutable $updatedAt,
|
||||
) {
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Message;
|
||||
final readonly class UpdateDomainsFromWatchlist
|
||||
{
|
||||
public function __construct(
|
||||
public string $watchListToken,
|
||||
public string $watchlistToken,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Message\OrderDomain;
|
||||
use App\Notifier\DomainOrderErrorNotification;
|
||||
use App\Notifier\DomainOrderNotification;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\Service\ChatNotificationService;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use App\Service\InfluxdbService;
|
||||
use App\Service\Provider\AbstractProvider;
|
||||
use App\Service\StatService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
@@ -31,7 +31,7 @@ final readonly class OrderDomainHandler
|
||||
public function __construct(
|
||||
string $mailerSenderEmail,
|
||||
string $mailerSenderName,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private WatchlistRepository $watchlistRepository,
|
||||
private DomainRepository $domainRepository,
|
||||
private KernelInterface $kernel,
|
||||
private MailerInterface $mailer,
|
||||
@@ -54,12 +54,12 @@ final readonly class OrderDomainHandler
|
||||
*/
|
||||
public function __invoke(OrderDomain $message): void
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $message->watchListToken]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $message->watchlistToken]);
|
||||
/** @var Domain $domain */
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $message->ldhName]);
|
||||
|
||||
$connector = $watchList->getConnector();
|
||||
$connector = $watchlist->getConnector();
|
||||
|
||||
/*
|
||||
* We make sure that the domain name is marked absent from WHOIS in the database before continuing.
|
||||
@@ -71,8 +71,8 @@ final readonly class OrderDomainHandler
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->notice('Watchlist {watchlist} is linked to connector {connector}. A purchase attempt will be made for domain name {ldhName} with provider {provider}.', [
|
||||
'watchlist' => $message->watchListToken,
|
||||
$this->logger->notice('Watchlist is linked to a connector : a purchase attempt will be made for this domain name', [
|
||||
'watchlist' => $message->watchlistToken,
|
||||
'connector' => $connector->getId(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'provider' => $connector->getProvider()->value,
|
||||
@@ -93,14 +93,15 @@ final readonly class OrderDomainHandler
|
||||
* The user is authenticated to ensure that the credentials are still valid.
|
||||
* If no errors occur, the purchase is attempted.
|
||||
*/
|
||||
$connectorProvider->authenticate($connector->getAuthData());
|
||||
|
||||
$connectorProvider->orderDomain($domain, $this->kernel->isDebug());
|
||||
|
||||
/*
|
||||
* If the purchase was successful, the statistics are updated and a success message is sent to the user.
|
||||
*/
|
||||
$this->logger->notice('Watchlist {watchlist} is linked to connector {connector}. A purchase was successfully made for domain {ldhName} with provider {provider}.', [
|
||||
'watchlist' => $message->watchListToken,
|
||||
$this->logger->notice('Watchlist is linked to connector : a purchase was successfully made for this domain name', [
|
||||
'watchlist' => $message->watchlistToken,
|
||||
'connector' => $connector->getId(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'provider' => $connector->getProvider()->value,
|
||||
@@ -111,15 +112,18 @@ final readonly class OrderDomainHandler
|
||||
$this->influxdbService->addDomainOrderPoint($connector, $domain, true);
|
||||
}
|
||||
$notification = (new DomainOrderNotification($this->sender, $domain, $connector));
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchList->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchList, $notification);
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchlist, $notification);
|
||||
} catch (\Throwable $exception) {
|
||||
/*
|
||||
* The purchase was not successful (for several possible reasons that we have not determined).
|
||||
* The user is informed and the exception is raised, which may allow you to try again.
|
||||
*/
|
||||
$this->logger->warning('Unable to complete purchase. An error message is sent to user {username}.', [
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
$this->logger->warning('Unable to complete purchase : an error message is sent to the user', [
|
||||
'watchlist' => $message->watchlistToken,
|
||||
'connector' => $connector->getId(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'provider' => $connector->getProvider()->value,
|
||||
]);
|
||||
|
||||
$this->statService->incrementStat('stats.domain.purchase.failed');
|
||||
@@ -127,8 +131,8 @@ final readonly class OrderDomainHandler
|
||||
$this->influxdbService->addDomainOrderPoint($connector, $domain, false);
|
||||
}
|
||||
$notification = (new DomainOrderErrorNotification($this->sender, $domain));
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchList->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchList, $notification);
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchlist, $notification);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Entity\WatchList;
|
||||
use App\Message\ProcessWatchListsTrigger;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Message\ProcessWatchlistTrigger;
|
||||
use App\Message\UpdateDomainsFromWatchlist;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use Random\Randomizer;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final readonly class ProcessWatchListsTriggerHandler
|
||||
final readonly class ProcessWatchlistTriggerHandler
|
||||
{
|
||||
public function __construct(
|
||||
private WatchListRepository $watchListRepository,
|
||||
private WatchlistRepository $watchlistRepository,
|
||||
private MessageBusInterface $bus,
|
||||
) {
|
||||
}
|
||||
@@ -23,7 +23,7 @@ final readonly class ProcessWatchListsTriggerHandler
|
||||
/**
|
||||
* @throws ExceptionInterface
|
||||
*/
|
||||
public function __invoke(ProcessWatchListsTrigger $message): void
|
||||
public function __invoke(ProcessWatchlistTrigger $message): void
|
||||
{
|
||||
/*
|
||||
* We shuffle the watch lists to process them in an order that we consider random.
|
||||
@@ -31,11 +31,11 @@ final readonly class ProcessWatchListsTriggerHandler
|
||||
*/
|
||||
|
||||
$randomizer = new Randomizer();
|
||||
$watchLists = $randomizer->shuffleArray($this->watchListRepository->findAll());
|
||||
$watchlists = $randomizer->shuffleArray($this->watchlistRepository->getEnabledWatchlist());
|
||||
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($watchLists as $watchList) {
|
||||
$this->bus->dispatch(new UpdateDomainsFromWatchlist($watchList->getToken()));
|
||||
/** @var Watchlist $watchlist */
|
||||
foreach ($watchlists as $watchlist) {
|
||||
$this->bus->dispatch(new UpdateDomainsFromWatchlist($watchlist->getToken()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Config\TriggerAction;
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainEvent;
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\WatchListTrigger;
|
||||
use App\Entity\DomainStatus;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Notifier\DomainStatusUpdateNotification;
|
||||
use App\Notifier\DomainUpdateNotification;
|
||||
use App\Repository\DomainEventRepository;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Repository\DomainStatusRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\Service\ChatNotificationService;
|
||||
use App\Service\InfluxdbService;
|
||||
use App\Service\StatService;
|
||||
@@ -19,7 +21,6 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Notifier\Recipient\Recipient;
|
||||
|
||||
@@ -35,11 +36,13 @@ final readonly class SendDomainEventNotifHandler
|
||||
private MailerInterface $mailer,
|
||||
private StatService $statService,
|
||||
private DomainRepository $domainRepository,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private WatchlistRepository $watchlistRepository,
|
||||
private ChatNotificationService $chatNotificationService,
|
||||
#[Autowire(param: 'influxdb_enabled')]
|
||||
private bool $influxdbEnabled,
|
||||
private InfluxdbService $influxdbService,
|
||||
private DomainEventRepository $domainEventRepository,
|
||||
private DomainStatusRepository $domainStatusRepository,
|
||||
) {
|
||||
$this->sender = new Address($mailerSenderEmail, $mailerSenderName);
|
||||
}
|
||||
@@ -47,56 +50,99 @@ final readonly class SendDomainEventNotifHandler
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws \Exception
|
||||
* @throws ExceptionInterface
|
||||
*/
|
||||
public function __invoke(SendDomainEventNotif $message): void
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $message->watchListToken]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $message->watchlistToken]);
|
||||
/** @var Domain $domain */
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $message->ldhName]);
|
||||
$recipient = new Recipient($watchlist->getUser()->getEmail());
|
||||
|
||||
/*
|
||||
* For each new event whose date is after the domain name update date (before the current domain name update)
|
||||
*/
|
||||
|
||||
/** @var DomainEvent $event */
|
||||
foreach ($domain->getEvents()->filter(
|
||||
fn ($event) => $message->updatedAt < $event->getDate() && $event->getDate() < new \DateTimeImmutable()) as $event
|
||||
) {
|
||||
$watchListTriggers = $watchList->getWatchListTriggers()
|
||||
->filter(fn ($trigger) => $trigger->getEvent() === $event->getAction());
|
||||
/** @var DomainEvent[] $newEvents */
|
||||
$newEvents = $this->domainEventRepository->findNewDomainEvents($domain, $message->updatedAt);
|
||||
|
||||
/*
|
||||
* For each trigger, we perform the appropriate action: send email or send push notification (for now)
|
||||
*/
|
||||
foreach ($newEvents as $event) {
|
||||
if (!in_array($event->getAction(), $watchlist->getTrackedEvents())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var WatchListTrigger $watchListTrigger */
|
||||
foreach ($watchListTriggers->getIterator() as $watchListTrigger) {
|
||||
$this->logger->info('Action {event} has been detected on the domain name {ldhName}. A notification is sent to user {username}.', [
|
||||
$notification = new DomainUpdateNotification($this->sender, $event);
|
||||
|
||||
$this->logger->info('New action has been detected on this domain name : an email is sent to user', [
|
||||
'event' => $event->getAction(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'username' => $watchlist->getUser()->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$this->mailer->send($notification->asEmailMessage($recipient)->getMessage());
|
||||
|
||||
if ($this->influxdbEnabled) {
|
||||
$this->influxdbService->addDomainNotificationPoint($domain, 'email', true);
|
||||
}
|
||||
|
||||
$webhookDsn = $watchlist->getWebhookDsn();
|
||||
if (null !== $webhookDsn && 0 !== count($webhookDsn)) {
|
||||
$this->logger->info('New action has been detected on this domain name : a notification is sent to user', [
|
||||
'event' => $event->getAction(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
'username' => $watchlist->getUser()->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$recipient = new Recipient($watchList->getUser()->getEmail());
|
||||
$notification = new DomainUpdateNotification($this->sender, $event);
|
||||
|
||||
if (TriggerAction::SendEmail == $watchListTrigger->getAction()) {
|
||||
$this->mailer->send($notification->asEmailMessage($recipient)->getMessage());
|
||||
} elseif (TriggerAction::SendChat == $watchListTrigger->getAction()) {
|
||||
$webhookDsn = $watchList->getWebhookDsn();
|
||||
if (null === $webhookDsn || 0 === count($webhookDsn)) {
|
||||
continue;
|
||||
}
|
||||
$this->chatNotificationService->sendChatNotification($watchList, $notification);
|
||||
$this->chatNotificationService->sendChatNotification($watchlist, $notification);
|
||||
if ($this->influxdbEnabled) {
|
||||
$this->influxdbService->addDomainNotificationPoint($domain, 'chat', true);
|
||||
}
|
||||
}
|
||||
|
||||
$this->statService->incrementStat('stats.alert.sent');
|
||||
}
|
||||
|
||||
/** @var DomainStatus $domainStatus */
|
||||
$domainStatus = $this->domainStatusRepository->findNewDomainStatus($domain, $message->updatedAt);
|
||||
|
||||
if (null !== $domainStatus && count(array_intersect(
|
||||
$watchlist->getTrackedEppStatus(),
|
||||
[...$domainStatus->getAddStatus(), ...$domainStatus->getDeleteStatus()]
|
||||
))) {
|
||||
$notification = new DomainStatusUpdateNotification($this->sender, $domain, $domainStatus);
|
||||
|
||||
$this->logger->info('New domain status has been detected on this domain name : an email is sent to user', [
|
||||
'addStatus' => $domainStatus->getAddStatus(),
|
||||
'deleteStatus' => $domainStatus->getDeleteStatus(),
|
||||
'status' => $domain->getStatus(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'username' => $watchlist->getUser()->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$this->mailer->send($notification->asEmailMessage($recipient)->getMessage());
|
||||
|
||||
if ($this->influxdbEnabled) {
|
||||
$this->influxdbService->addDomainNotificationPoint($domain, 'email', true);
|
||||
}
|
||||
|
||||
$webhookDsn = $watchlist->getWebhookDsn();
|
||||
if (null !== $webhookDsn && 0 !== count($webhookDsn)) {
|
||||
$this->logger->info('New domain status has been detected on this domain name : a notification is sent to user', [
|
||||
'addStatus' => $domainStatus->getAddStatus(),
|
||||
'deleteStatus' => $domainStatus->getDeleteStatus(),
|
||||
'status' => $domain->getStatus(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'username' => $watchlist->getUser()->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$this->chatNotificationService->sendChatNotification($watchlist, $notification);
|
||||
|
||||
if ($this->influxdbEnabled) {
|
||||
$this->influxdbService->addDomainNotificationPoint($domain, TriggerAction::SendChat, true);
|
||||
$this->influxdbService->addDomainNotificationPoint($domain, 'chat', true);
|
||||
}
|
||||
$this->statService->incrementStat('stats.alert.sent');
|
||||
}
|
||||
|
||||
$this->statService->incrementStat('stats.alert.sent');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,23 @@
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Exception\DomainNotFoundException;
|
||||
use App\Exception\TldNotSupportedException;
|
||||
use App\Exception\UnknownRdapServerException;
|
||||
use App\Message\OrderDomain;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Message\UpdateDomainsFromWatchlist;
|
||||
use App\Notifier\DomainDeletedNotification;
|
||||
use App\Notifier\DomainUpdateErrorNotification;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchlistRepository;
|
||||
use App\Service\ChatNotificationService;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use App\Service\Connector\CheckDomainProviderInterface;
|
||||
use App\Service\Provider\AbstractProvider;
|
||||
use App\Service\Provider\CheckDomainProviderInterface;
|
||||
use App\Service\RDAPService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
@@ -36,10 +38,11 @@ final readonly class UpdateDomainsFromWatchlistHandler
|
||||
string $mailerSenderEmail,
|
||||
string $mailerSenderName,
|
||||
private MessageBusInterface $bus,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private WatchlistRepository $watchlistRepository,
|
||||
private LoggerInterface $logger,
|
||||
#[Autowire(service: 'service_container')]
|
||||
private ContainerInterface $locator,
|
||||
private DomainRepository $domainRepository,
|
||||
) {
|
||||
$this->sender = new Address($mailerSenderEmail, $mailerSenderName);
|
||||
}
|
||||
@@ -49,36 +52,37 @@ final readonly class UpdateDomainsFromWatchlistHandler
|
||||
*/
|
||||
public function __invoke(UpdateDomainsFromWatchlist $message): void
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $message->watchListToken]);
|
||||
/** @var Watchlist $watchlist */
|
||||
$watchlist = $this->watchlistRepository->findOneBy(['token' => $message->watchlistToken]);
|
||||
|
||||
$this->logger->info('Domain names from Watchlist {token} will be processed.', [
|
||||
'token' => $message->watchListToken,
|
||||
$this->logger->debug('Domain names listed in the Watchlist will be updated', [
|
||||
'watchlist' => $message->watchlistToken,
|
||||
]);
|
||||
|
||||
/** @var AbstractProvider $connectorProvider */
|
||||
$connectorProvider = $this->getConnectorProvider($watchList);
|
||||
$connectorProvider = $this->getConnectorProvider($watchlist);
|
||||
|
||||
if ($connectorProvider instanceof CheckDomainProviderInterface) {
|
||||
$this->logger->notice('Watchlist {watchlist} linked to connector {connector}.', [
|
||||
'watchlist' => $watchList->getToken(),
|
||||
'connector' => $watchList->getConnector()->getId(),
|
||||
$this->logger->debug('Watchlist is linked to a connector', [
|
||||
'watchlist' => $watchlist->getToken(),
|
||||
'connector' => $watchlist->getConnector()->getId(),
|
||||
]);
|
||||
|
||||
$domainList = array_unique(array_map(fn (Domain $d) => $d->getLdhName(), $watchlist->getDomains()->toArray()));
|
||||
|
||||
try {
|
||||
$checkedDomains = $connectorProvider->checkDomains(
|
||||
...array_unique(array_map(fn (Domain $d) => $d->getLdhName(), $watchList->getDomains()->toArray()))
|
||||
);
|
||||
$checkedDomains = $connectorProvider->checkDomains(...$domainList);
|
||||
} catch (\Throwable $exception) {
|
||||
$this->logger->warning('Unable to check domain names availability with connector {connector}.', [
|
||||
'connector' => $watchList->getConnector()->getId(),
|
||||
$this->logger->warning('Unable to check domain names availability with this connector', [
|
||||
'connector' => $watchlist->getConnector()->getId(),
|
||||
'ldhName' => $domainList,
|
||||
]);
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
foreach ($checkedDomains as $domain) {
|
||||
$this->bus->dispatch(new OrderDomain($watchList->getToken(), $domain));
|
||||
$this->bus->dispatch(new OrderDomain($watchlist->getToken(), $domain));
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -92,9 +96,10 @@ 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();
|
||||
|
||||
try {
|
||||
/*
|
||||
@@ -102,42 +107,34 @@ final readonly class UpdateDomainsFromWatchlistHandler
|
||||
* We send messages that correspond to the sending of notifications that will not be processed here.
|
||||
*/
|
||||
$this->RDAPService->registerDomain($domain->getLdhName());
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
} catch (NotFoundHttpException) {
|
||||
if (!$domain->getDeleted()) {
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchlist->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
} catch (DomainNotFoundException) {
|
||||
$newDomain = $this->domainRepository->findOneBy(['ldhName' => $domain->getLdhName()]);
|
||||
|
||||
if (!$deleted && null !== $newDomain && $newDomain->getDeleted()) {
|
||||
$notification = new DomainDeletedNotification($this->sender, $domain);
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchList->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchList, $notification);
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchlist, $notification);
|
||||
}
|
||||
|
||||
if ($watchList->getConnector()) {
|
||||
if ($watchlist->getConnector()) {
|
||||
/*
|
||||
* If the domain name no longer appears in the WHOIS AND a connector is associated with this Watchlist,
|
||||
* this connector is used to purchase the domain name.
|
||||
*/
|
||||
$this->bus->dispatch(new OrderDomain($watchList->getToken(), $domain->getLdhName()));
|
||||
$this->bus->dispatch(new OrderDomain($watchlist->getToken(), $domain->getLdhName()));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
} catch (TldNotSupportedException|UnknownRdapServerException) {
|
||||
/*
|
||||
* In case of another unknown error,
|
||||
* the owner of the Watchlist is informed that an error occurred in updating the domain name.
|
||||
* In this case, the domain name can no longer be updated. Unfortunately, there is nothing more that can be done.
|
||||
*/
|
||||
$this->logger->error('An update error email is sent to user {username}.', [
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
'error' => $e,
|
||||
]);
|
||||
$email = (new DomainUpdateErrorNotification($this->sender, $domain))
|
||||
->asEmailMessage(new Recipient($watchList->getUser()->getEmail()));
|
||||
$this->mailer->send($email->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getConnectorProvider(WatchList $watchList): ?object
|
||||
private function getConnectorProvider(Watchlist $watchlist): ?object
|
||||
{
|
||||
$connector = $watchList->getConnector();
|
||||
$connector = $watchlist->getConnector();
|
||||
if (null === $connector || null === $connector->getProvider()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Message\UpdateRdapServers;
|
||||
use App\Service\RDAPService;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Service\OfficialDataService;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
@@ -16,8 +17,8 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
final readonly class UpdateRdapServersHandler
|
||||
{
|
||||
public function __construct(
|
||||
private RDAPService $RDAPService,
|
||||
private ParameterBagInterface $bag,
|
||||
private OfficialDataService $officialDataService,
|
||||
private ParameterBagInterface $bag, private DomainRepository $domainRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -39,8 +40,9 @@ final readonly class UpdateRdapServersHandler
|
||||
*/
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateTldListIANA();
|
||||
$this->RDAPService->updateGTldListICANN();
|
||||
$this->officialDataService->updateTldListIANA();
|
||||
$this->officialDataService->updateGTldListICANN();
|
||||
$this->domainRepository->setDomainDeletedIfTldIsDeleted();
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
@@ -50,7 +52,7 @@ final readonly class UpdateRdapServersHandler
|
||||
*/
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateRDAPServersFromIANA();
|
||||
$this->officialDataService->updateRDAPServersFromIANA();
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
@@ -60,13 +62,13 @@ final readonly class UpdateRdapServersHandler
|
||||
*/
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateRDAPServersFromFile($this->bag->get('custom_rdap_servers_file'));
|
||||
$this->officialDataService->updateRDAPServersFromFile($this->bag->get('custom_rdap_servers_file'));
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateRegistrarListIANA();
|
||||
$this->officialDataService->updateRegistrarListIANA();
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\MessageHandler;
|
||||
use App\Message\ValidateConnectorCredentials;
|
||||
use App\Notifier\ValidateConnectorCredentialsErrorNotification;
|
||||
use App\Repository\ConnectorRepository;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use App\Service\Provider\AbstractProvider;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
|
||||
69
src/Notifier/DomainStatusUpdateNotification.php
Normal file
69
src/Notifier/DomainStatusUpdateNotification.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifier;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainStatus;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Notifier\Message\ChatMessage;
|
||||
use Symfony\Component\Notifier\Message\EmailMessage;
|
||||
use Symfony\Component\Notifier\Message\PushMessage;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
|
||||
use Symfony\Component\Notifier\Recipient\RecipientInterface;
|
||||
|
||||
class DomainStatusUpdateNotification extends DomainWatchdogNotification
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Address $sender,
|
||||
private readonly Domain $domain,
|
||||
private readonly DomainStatus $domainStatus,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
|
||||
{
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
$this->subject("Domain EPP status changed $ldhName")
|
||||
->content("Domain name $ldhName EPP status has been updated.")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return ChatMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asPushMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?PushMessage
|
||||
{
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
$this->subject("Domain EPP status changed $ldhName")
|
||||
->content("Domain name $ldhName EPP status has been updated.")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return PushMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asEmailMessage(EmailRecipientInterface $recipient): EmailMessage
|
||||
{
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
|
||||
$email = (new TemplatedEmail())
|
||||
->from($this->sender)
|
||||
->to($recipient->getEmail())
|
||||
->priority(Email::PRIORITY_HIGH)
|
||||
->subject("Domain EPP status changed $ldhName")
|
||||
->htmlTemplate('emails/success/domain_status_updated.html.twig')
|
||||
->locale('en')
|
||||
->context([
|
||||
'domain' => $this->domain,
|
||||
'domainStatus' => $this->domainStatus,
|
||||
]);
|
||||
|
||||
$email->getHeaders()
|
||||
->addTextHeader('In-Reply-To', "<$ldhName+updated@domain-watchdog>")
|
||||
->addTextHeader('References', "<$ldhName+updated@domain-watchdog>");
|
||||
|
||||
return new EmailMessage($email);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class DomainUpdateNotification extends DomainWatchdogNotification
|
||||
$ldhName = $this->domainEvent->getDomain()->getLdhName();
|
||||
$action = $this->domainEvent->getAction();
|
||||
$this->subject("Domain changed $ldhName ($action)")
|
||||
->content("Domain name $ldhName information has been updated ($action).")
|
||||
->content("Domain name $ldhName events has been updated ($action).")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return ChatMessage::fromNotification($this);
|
||||
@@ -38,7 +38,7 @@ class DomainUpdateNotification extends DomainWatchdogNotification
|
||||
$ldhName = $this->domainEvent->getDomain()->getLdhName();
|
||||
$action = $this->domainEvent->getAction();
|
||||
$this->subject("Domain changed $ldhName ($action)")
|
||||
->content("Domain name $ldhName information has been updated ($action).")
|
||||
->content("Domain name $ldhName events has been updated ($action).")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return PushMessage::fromNotification($this);
|
||||
@@ -52,8 +52,8 @@ class DomainUpdateNotification extends DomainWatchdogNotification
|
||||
->from($this->sender)
|
||||
->to($recipient->getEmail())
|
||||
->priority(Email::PRIORITY_HIGH)
|
||||
->subject("Domain name $ldhName information has been updated")
|
||||
->htmlTemplate('emails/success/domain_updated.html.twig')
|
||||
->subject("Domain name $ldhName events has been updated")
|
||||
->htmlTemplate('emails/success/domain_event_updated.html.twig')
|
||||
->locale('en')
|
||||
->context([
|
||||
'event' => $this->domainEvent,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainEntity;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
@@ -16,6 +17,18 @@ class DomainEntityRepository extends ServiceEntityRepository
|
||||
parent::__construct($registry, DomainEntity::class);
|
||||
}
|
||||
|
||||
public function setDomainEntityAsDeleted(Domain $domain)
|
||||
{
|
||||
return $this->createQueryBuilder('de')
|
||||
->update()
|
||||
->set('de.deletedAt', ':now')
|
||||
->where('de.domain = :domain')
|
||||
->andWhere('de.deletedAt IS NULL')
|
||||
->setParameter('now', new \DateTimeImmutable())
|
||||
->setParameter('domain', $domain)
|
||||
->getQuery()->execute();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return DomainEntity[] Returns an array of DomainEntity objects
|
||||
// */
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainEvent;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
@@ -16,6 +17,46 @@ class DomainEventRepository extends ServiceEntityRepository
|
||||
parent::__construct($registry, DomainEvent::class);
|
||||
}
|
||||
|
||||
public function findLastDomainEvent(Domain $domain, string $action)
|
||||
{
|
||||
return $this->createQueryBuilder('de')
|
||||
->select()
|
||||
->where('de.domain = :domain')
|
||||
->andWhere('de.action = :action')
|
||||
->andWhere('de.deleted = FALSE')
|
||||
->orderBy('de.date', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->setParameter('domain', $domain)
|
||||
->setParameter('action', $action)
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
public function findNewDomainEvents(Domain $domain, \DateTimeImmutable $updatedAt)
|
||||
{
|
||||
return $this->createQueryBuilder('de')
|
||||
->select()
|
||||
->where('de.domain = :domain')
|
||||
->andWhere('de.date > :updatedAt')
|
||||
->andWhere('de.date < :now')
|
||||
->setParameter('domain', $domain)
|
||||
->setParameter('updatedAt', $updatedAt)
|
||||
->setParameter('now', new \DateTimeImmutable())
|
||||
->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function setDomainEventAsDeleted(Domain $domain)
|
||||
{
|
||||
return $this->createQueryBuilder('de')
|
||||
->update()
|
||||
->set('de.deleted', ':deleted')
|
||||
->where('de.domain = :domain')
|
||||
->setParameter('deleted', true)
|
||||
->setParameter('domain', $domain)
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return DomainEvent[] Returns an array of DomainEvent objects
|
||||
// */
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\Tld;
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
@@ -16,6 +18,51 @@ class DomainRepository extends ServiceEntityRepository
|
||||
parent::__construct($registry, Domain::class);
|
||||
}
|
||||
|
||||
public function findByTld(string $tld): array
|
||||
{
|
||||
return $this->createQueryBuilder('d')
|
||||
->addSelect('events')
|
||||
->leftJoin('d.events', 'events')
|
||||
->where('d.tld = :dot')
|
||||
->setParameter('dot', $tld)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function getActiveDomainCountByTld(): array
|
||||
{
|
||||
return $this->createQueryBuilder('d')
|
||||
->select('t.tld tld')
|
||||
->join('d.tld', 't')
|
||||
->addSelect('COUNT(d.ldhName) AS domain')
|
||||
->addGroupBy('t.tld')
|
||||
->where('d.deleted = FALSE')
|
||||
->orderBy('domain', 'DESC')
|
||||
->setMaxResults(5)
|
||||
->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
public function setDomainDeletedIfTldIsDeleted()
|
||||
{
|
||||
return $this->createQueryBuilder('d')
|
||||
->update()
|
||||
->set('d.deleted', ':deleted')
|
||||
->where('d.tld IN (SELECT t FROM '.Tld::class.' t WHERE t.deletedAt IS NOT NULL)')
|
||||
->setParameter('deleted', true)
|
||||
->getQuery()->execute();
|
||||
}
|
||||
|
||||
public function getMyTrackedDomains(User $user): array
|
||||
{
|
||||
return $this->createQueryBuilder('d')
|
||||
->join('d.watchlists', 'w')
|
||||
->where('w.user = :user')
|
||||
->andWhere('d.deleted = false')
|
||||
->setParameter('user', $user)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Domain[] Returns an array of Domain objects
|
||||
// */
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainStatus;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
@@ -16,6 +17,31 @@ class DomainStatusRepository extends ServiceEntityRepository
|
||||
parent::__construct($registry, DomainStatus::class);
|
||||
}
|
||||
|
||||
public function findNewDomainStatus(Domain $domain, \DateTimeImmutable $updatedAt): ?DomainStatus
|
||||
{
|
||||
return $this->createQueryBuilder('ds')
|
||||
->select()
|
||||
->where('ds.domain = :domain')
|
||||
->andWhere('ds.date = :date')
|
||||
->orderBy('ds.createdAt', 'DESC')
|
||||
->setParameter('domain', $domain)
|
||||
->setParameter('date', $updatedAt)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
public function findLastDomainStatus(Domain $domain): ?DomainStatus
|
||||
{
|
||||
return $this->createQueryBuilder('ds')
|
||||
->select()
|
||||
->where('ds.domain = :domain')
|
||||
->setParameter('domain', $domain)
|
||||
->orderBy('ds.createdAt', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return DomainStatus[] Returns an array of DomainStatus objects
|
||||
// */
|
||||
|
||||
@@ -2,39 +2,39 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\WatchListTrigger;
|
||||
use App\Entity\IcannAccreditation;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<WatchListTrigger>
|
||||
* @extends ServiceEntityRepository<IcannAccreditation>
|
||||
*/
|
||||
class EventTriggerRepository extends ServiceEntityRepository
|
||||
class IcannAccreditationRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, WatchListTrigger::class);
|
||||
parent::__construct($registry, IcannAccreditation::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return WatchListTrigger[] Returns an array of WatchListTrigger objects
|
||||
// * @return Connector[] Returns an array of Connector objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('e')
|
||||
// ->andWhere('e.exampleField = :val')
|
||||
// return $this->createQueryBuilder('c')
|
||||
// ->andWhere('c.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('e.id', 'ASC')
|
||||
// ->orderBy('c.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?WatchListTrigger
|
||||
// public function findOneBySomeField($value): ?Connector
|
||||
// {
|
||||
// return $this->createQueryBuilder('e')
|
||||
// ->andWhere('e.exampleField = :val')
|
||||
// return $this->createQueryBuilder('c')
|
||||
// ->andWhere('c.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
@@ -16,6 +16,28 @@ class TldRepository extends ServiceEntityRepository
|
||||
parent::__construct($registry, Tld::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tld[] Returns an array of deleted Tld
|
||||
*/
|
||||
public function findDeleted(): array
|
||||
{
|
||||
return $this->createQueryBuilder('t')
|
||||
->andWhere('t.deletedAt IS NOT NULL')
|
||||
->orderBy('t.deletedAt', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function setAllTldAsDeleted()
|
||||
{
|
||||
return $this->createQueryBuilder('t')
|
||||
->update()
|
||||
->set('t.deletedAt', 'COALESCE(t.removalDate, CURRENT_TIMESTAMP())')
|
||||
->where('t.tld != :dot')
|
||||
->setParameter('dot', '.')
|
||||
->getQuery()->execute();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Tld[] Returns an array of Tld objects
|
||||
// */
|
||||
|
||||
@@ -2,22 +2,39 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\Watchlist;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<WatchList>
|
||||
* @extends ServiceEntityRepository<Watchlist>
|
||||
*/
|
||||
class WatchListRepository extends ServiceEntityRepository
|
||||
class WatchlistRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, WatchList::class);
|
||||
parent::__construct($registry, Watchlist::class);
|
||||
}
|
||||
|
||||
public function getTrackedDomainCount()
|
||||
{
|
||||
return $this->createQueryBuilder('w')
|
||||
->select('COUNT(DISTINCT d.ldhName)')
|
||||
->join('w.domains', 'd')
|
||||
->where('d.deleted = FALSE')
|
||||
->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function getEnabledWatchlist()
|
||||
{
|
||||
return $this->createQueryBuilder('w')
|
||||
->select()
|
||||
->where('w.enabled = true')
|
||||
->getQuery()->execute();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return WatchList[] Returns an array of WatchList objects
|
||||
// * @return Watchlist[] Returns an array of Watchlist objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
@@ -31,7 +48,7 @@ class WatchListRepository extends ServiceEntityRepository
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?WatchList
|
||||
// public function findOneBySomeField($value): ?Watchlist
|
||||
// {
|
||||
// return $this->createQueryBuilder('b')
|
||||
// ->andWhere('b.exampleField = :val')
|
||||
28
src/Schedule.php
Normal file
28
src/Schedule.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Component\Scheduler\Attribute\AsSchedule;
|
||||
use Symfony\Component\Scheduler\Schedule as SymfonySchedule;
|
||||
use Symfony\Component\Scheduler\ScheduleProviderInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
#[AsSchedule]
|
||||
class Schedule implements ScheduleProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CacheInterface $cache,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSchedule(): SymfonySchedule
|
||||
{
|
||||
return (new SymfonySchedule())
|
||||
->stateful($this->cache) // ensure missed tasks are executed
|
||||
->processOnlyLastMissedRun(true) // ensure only last missed task is run
|
||||
|
||||
// add your own tasks here
|
||||
// see https://symfony.com/doc/current/scheduler.html#attaching-recurring-messages-to-a-schedule
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Scheduler;
|
||||
|
||||
use App\Message\ProcessWatchListsTrigger;
|
||||
use App\Message\ProcessWatchlistTrigger;
|
||||
use Symfony\Component\Scheduler\Attribute\AsSchedule;
|
||||
use Symfony\Component\Scheduler\RecurringMessage;
|
||||
use Symfony\Component\Scheduler\Schedule;
|
||||
@@ -10,7 +10,7 @@ use Symfony\Component\Scheduler\ScheduleProviderInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
#[AsSchedule('notif_watchlist')]
|
||||
final readonly class SendNotifWatchListTriggerSchedule implements ScheduleProviderInterface
|
||||
final readonly class SendNotifWatchlistTriggerSchedule implements ScheduleProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CacheInterface $cache,
|
||||
@@ -21,7 +21,7 @@ final readonly class SendNotifWatchListTriggerSchedule implements ScheduleProvid
|
||||
{
|
||||
return (new Schedule())
|
||||
->add(
|
||||
RecurringMessage::every('5 minutes', new ProcessWatchListsTrigger()),
|
||||
RecurringMessage::every('5 minutes', new ProcessWatchlistTrigger()),
|
||||
)
|
||||
->stateful($this->cache);
|
||||
}
|
||||
@@ -47,7 +47,7 @@ readonly class EmailVerifier
|
||||
{
|
||||
$this->verifyEmailHelper->validateEmailConfirmationFromRequest($request, (string) $user->getId(), $user->getEmail());
|
||||
|
||||
$user->setVerified(true);
|
||||
$user->setVerifiedAt(new \DateTimeImmutable());
|
||||
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
|
||||
@@ -33,7 +33,7 @@ class JWTAuthenticator implements AuthenticationSuccessHandlerInterface
|
||||
|
||||
public function handleAuthenticationSuccess(UserInterface $user, $jwt = null): Response
|
||||
{
|
||||
if (($user instanceof User) && !$user->isVerified()) {
|
||||
if (($user instanceof User) && null === $user->getVerifiedAt()) {
|
||||
throw new AccessDeniedHttpException('You have not yet validated your email address.');
|
||||
}
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ class OAuthAuthenticator extends OAuth2Authenticator implements AuthenticationEn
|
||||
return $existingUser;
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($userFromToken->getEmail())->setVerified(true);
|
||||
$user = (new User())->setCreatedAt(new \DateTimeImmutable());
|
||||
$user->setEmail($userFromToken->getEmail())->setVerifiedAt($user->getCreatedAt());
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
|
||||
93
src/Service/CalendarService.php
Normal file
93
src/Service/CalendarService.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainEntity;
|
||||
use App\Entity\DomainEvent;
|
||||
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;
|
||||
|
||||
readonly class CalendarService
|
||||
{
|
||||
public function __construct(
|
||||
private RDAPService $RDAPService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Event[]
|
||||
*
|
||||
* @throws ParseException
|
||||
* @throws EofException
|
||||
* @throws InvalidDataException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getDomainCalendarEvents(Domain $domain): array
|
||||
{
|
||||
$events = [];
|
||||
$attendees = [];
|
||||
|
||||
/* @var DomainEntity $entity */
|
||||
foreach ($domain->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;
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,10 @@
|
||||
namespace App\Service;
|
||||
|
||||
use App\Config\WebhookScheme;
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Exception\UnsupportedDsnSchemeException;
|
||||
use App\Notifier\DomainWatchdogNotification;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Notifier\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Notifier\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Notifier\Recipient\NoRecipient;
|
||||
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
|
||||
use Symfony\Component\Notifier\Transport\Dsn;
|
||||
@@ -15,30 +14,29 @@ use Symfony\Component\Notifier\Transport\Dsn;
|
||||
readonly class ChatNotificationService
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function sendChatNotification(WatchList $watchList, DomainWatchdogNotification $notification): void
|
||||
/**
|
||||
* @throws UnsupportedDsnSchemeException
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function sendChatNotification(Watchlist $watchlist, DomainWatchdogNotification $notification): void
|
||||
{
|
||||
$webhookDsn = $watchList->getWebhookDsn();
|
||||
$webhookDsn = $watchlist->getWebhookDsn();
|
||||
|
||||
if (empty($webhookDsn)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($webhookDsn as $dsnString) {
|
||||
try {
|
||||
$dsn = new Dsn($dsnString);
|
||||
} catch (InvalidArgumentException $exception) {
|
||||
throw new BadRequestHttpException($exception->getMessage());
|
||||
}
|
||||
$dsn = new Dsn($dsnString);
|
||||
|
||||
$scheme = $dsn->getScheme();
|
||||
$webhookScheme = WebhookScheme::tryFrom($scheme);
|
||||
|
||||
if (null === $webhookScheme) {
|
||||
throw new BadRequestHttpException("The DSN scheme ($scheme) is not supported");
|
||||
throw new UnsupportedDsnSchemeException($scheme);
|
||||
}
|
||||
|
||||
$transportFactoryClass = $webhookScheme->getChatTransportFactory();
|
||||
@@ -48,29 +46,14 @@ readonly class ChatNotificationService
|
||||
$push = $notification->asPushMessage(new NoRecipient());
|
||||
$chat = $notification->asChatMessage(new NoRecipient(), $webhookScheme->value);
|
||||
|
||||
try {
|
||||
$factory = $transportFactory->create($dsn);
|
||||
$factory = $transportFactory->create($dsn);
|
||||
|
||||
if ($factory->supports($push)) {
|
||||
$factory->send($push);
|
||||
} elseif ($factory->supports($chat)) {
|
||||
$factory->send($chat);
|
||||
} else {
|
||||
throw new BadRequestHttpException('Unsupported message type');
|
||||
}
|
||||
|
||||
$this->logger->info('Chat message sent with {schema} for Watchlist {token}',
|
||||
[
|
||||
'scheme' => $webhookScheme->name,
|
||||
'token' => $watchList->getToken(),
|
||||
]);
|
||||
} catch (\Throwable $exception) {
|
||||
$this->logger->error('Unable to send a chat message to {scheme} for Watchlist {token}',
|
||||
[
|
||||
'scheme' => $webhookScheme->name,
|
||||
'token' => $watchList->getToken(),
|
||||
]);
|
||||
throw new BadRequestHttpException($exception->getMessage());
|
||||
if ($factory->supports($push)) {
|
||||
$factory->send($push);
|
||||
} elseif ($factory->supports($chat)) {
|
||||
$factory->send($chat);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Unsupported message type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Config\TriggerAction;
|
||||
use App\Entity\Connector;
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\RdapServer;
|
||||
@@ -53,6 +52,7 @@ readonly class InfluxdbService
|
||||
], (int) floor($info['start_time'] * 1e3),
|
||||
WritePrecision::MS)
|
||||
);
|
||||
$this->client->close();
|
||||
}
|
||||
|
||||
public function addDomainOrderPoint(Connector $connector, Domain $domain, bool $success): void
|
||||
@@ -64,17 +64,19 @@ readonly class InfluxdbService
|
||||
], [
|
||||
'success' => $success,
|
||||
]));
|
||||
$this->client->close();
|
||||
}
|
||||
|
||||
public function addDomainNotificationPoint(Domain $domain, TriggerAction $triggerAction, bool $success): void
|
||||
public function addDomainNotificationPoint(Domain $domain, string $triggerAction, bool $success): void
|
||||
{
|
||||
$this->writePoints(new Point('domain_notification', [
|
||||
'domain' => $domain->getLdhName(),
|
||||
'tld' => $domain->getTld()->getTld(),
|
||||
'medium' => $triggerAction->value,
|
||||
'medium' => $triggerAction,
|
||||
], [
|
||||
'success' => $success,
|
||||
]));
|
||||
$this->client->close();
|
||||
}
|
||||
|
||||
private function writePoints(Point ...$points): void
|
||||
@@ -89,5 +91,6 @@ readonly class InfluxdbService
|
||||
} catch (\Throwable) {
|
||||
// TODO: Add a retry mechanism if writing fails
|
||||
}
|
||||
$this->client->close();
|
||||
}
|
||||
}
|
||||
|
||||
302
src/Service/OfficialDataService.php
Normal file
302
src/Service/OfficialDataService.php
Normal file
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Config\RegistrarStatus;
|
||||
use App\Config\TldType;
|
||||
use App\Entity\IcannAccreditation;
|
||||
use App\Entity\RdapServer;
|
||||
use App\Entity\Tld;
|
||||
use App\Repository\IcannAccreditationRepository;
|
||||
use App\Repository\RdapServerRepository;
|
||||
use App\Repository\TldRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
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;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class OfficialDataService
|
||||
{
|
||||
/* @see https://www.iana.org/domains/root/db */
|
||||
private const ISO_TLD_EXCEPTION = ['ac', 'eu', 'uk', 'su', 'tp'];
|
||||
private const INFRA_TLD = ['arpa'];
|
||||
private const SPONSORED_TLD = [
|
||||
'aero',
|
||||
'asia',
|
||||
'cat',
|
||||
'coop',
|
||||
'edu',
|
||||
'gov',
|
||||
'int',
|
||||
'jobs',
|
||||
'mil',
|
||||
'museum',
|
||||
'post',
|
||||
'tel',
|
||||
'travel',
|
||||
'xxx',
|
||||
];
|
||||
private const TEST_TLD = [
|
||||
'xn--kgbechtv',
|
||||
'xn--hgbk6aj7f53bba',
|
||||
'xn--0zwm56d',
|
||||
'xn--g6w251d',
|
||||
'xn--80akhbyknj4f',
|
||||
'xn--11b5bs3a9aj6g',
|
||||
'xn--jxalpdlp',
|
||||
'xn--9t4b11yi5a',
|
||||
'xn--deba0ad',
|
||||
'xn--zckzah',
|
||||
'xn--hlcj6aya9esc7a',
|
||||
];
|
||||
|
||||
private const IANA_REGISTRAR_IDS_URL = 'https://www.iana.org/assignments/registrar-ids/registrar-ids.xml';
|
||||
private const IANA_RDAP_SERVER_LIST_URL = 'https://data.iana.org/rdap/dns.json';
|
||||
private const IANA_TLD_LIST_URL = 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt';
|
||||
private const ICANN_GTLD_LIST_URL = 'https://www.icann.org/resources/registries/gtlds/v2/gtlds.json';
|
||||
|
||||
public const DOMAIN_DOT = '.';
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $client,
|
||||
private readonly RdapServerRepository $rdapServerRepository,
|
||||
private readonly TldRepository $tldRepository,
|
||||
private readonly IcannAccreditationRepository $icannAccreditationRepository,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws DecodingExceptionInterface
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateRDAPServersFromIANA(): void
|
||||
{
|
||||
$this->logger->info('Start of update the RDAP server list from IANA');
|
||||
|
||||
$dnsRoot = $this->client->request(
|
||||
'GET', self::IANA_RDAP_SERVER_LIST_URL
|
||||
)->toArray();
|
||||
|
||||
$this->updateRDAPServers($dnsRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function updateRDAPServers(array $dnsRoot): void
|
||||
{
|
||||
foreach ($dnsRoot['services'] as $service) {
|
||||
foreach ($service[0] as $tld) {
|
||||
if (self::DOMAIN_DOT === $tld && null === $this->tldRepository->findOneBy(['tld' => $tld])) {
|
||||
$this->em->persist((new Tld())->setTld(self::DOMAIN_DOT)->setType(TldType::root));
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
$tldEntity = $this->tldRepository->findOneBy(['tld' => $tld]);
|
||||
if (null === $tldEntity) {
|
||||
$tldEntity = (new Tld())->setTld($tld)->setType(TldType::gTLD);
|
||||
$this->em->persist($tldEntity);
|
||||
}
|
||||
|
||||
foreach ($service[1] as $rdapServerUrl) {
|
||||
$server = $this->rdapServerRepository->findOneBy(['tld' => $tldEntity->getTld(), 'url' => $rdapServerUrl]);
|
||||
|
||||
if (null === $server) {
|
||||
$server = new RdapServer();
|
||||
}
|
||||
|
||||
$server
|
||||
->setTld($tldEntity)
|
||||
->setUrl($rdapServerUrl)
|
||||
->setUpdatedAt(new \DateTimeImmutable($dnsRoot['publication'] ?? 'now'));
|
||||
|
||||
$this->em->persist($server);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateRDAPServersFromFile(string $fileName): void
|
||||
{
|
||||
if (!file_exists($fileName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->info('Start of update the RDAP server list from custom config file');
|
||||
$this->updateRDAPServers(Yaml::parseFile($fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws ClientExceptionInterface
|
||||
*/
|
||||
public function updateTldListIANA(): void
|
||||
{
|
||||
$this->logger->info('Start of retrieval of the list of TLDs according to IANA');
|
||||
$tldList = array_map(
|
||||
fn ($tld) => strtolower($tld),
|
||||
explode(PHP_EOL,
|
||||
$this->client->request(
|
||||
'GET', self::IANA_TLD_LIST_URL
|
||||
)->getContent()
|
||||
));
|
||||
array_shift($tldList);
|
||||
|
||||
foreach ($tldList as $tld) {
|
||||
if ('' === $tld) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->tldRepository->setAllTldAsDeleted();
|
||||
|
||||
$tldEntity = $this->tldRepository->findOneBy(['tld' => $tld]);
|
||||
|
||||
if (null === $tldEntity) {
|
||||
$tldEntity = new Tld();
|
||||
$tldEntity->setTld($tld);
|
||||
|
||||
$this->logger->notice('New TLD detected according to IANA', [
|
||||
'tld' => $tld,
|
||||
]);
|
||||
} else {
|
||||
$this->em->refresh($tldEntity);
|
||||
}
|
||||
|
||||
$type = $this->getTldType($tld);
|
||||
|
||||
if (null !== $type) {
|
||||
$tldEntity->setType($type);
|
||||
} elseif (null === $tldEntity->isContractTerminated()) { // ICANN managed, must be a ccTLD
|
||||
$tldEntity->setType(TldType::ccTLD);
|
||||
} else {
|
||||
$tldEntity->setType(TldType::gTLD);
|
||||
}
|
||||
|
||||
$tldEntity->setDeletedAt(null);
|
||||
$this->em->persist($tldEntity);
|
||||
}
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateRegistrarListIANA(): void
|
||||
{
|
||||
$this->logger->info('Start of retrieval of the list of Registrar IDs according to IANA');
|
||||
$registrarList = $this->client->request(
|
||||
'GET', self::IANA_REGISTRAR_IDS_URL
|
||||
);
|
||||
|
||||
$data = new \SimpleXMLElement($registrarList->getContent());
|
||||
|
||||
foreach ($data->registry->record as $registrar) {
|
||||
$icannAcreditation = $this->icannAccreditationRepository->findOneBy(['id' => (int) $registrar->value]);
|
||||
if (null === $icannAcreditation) {
|
||||
$icannAcreditation = new IcannAccreditation();
|
||||
}
|
||||
|
||||
$icannAcreditation
|
||||
->setId((int) $registrar->value)
|
||||
->setRegistrarName($registrar->name)
|
||||
->setStatus(RegistrarStatus::from($registrar->status))
|
||||
->setRdapBaseUrl($registrar->rdapurl->count() ? ($registrar->rdapurl->server) : null)
|
||||
->setUpdated(null !== $registrar->attributes()->updated ? new \DateTimeImmutable($registrar->attributes()->updated) : null)
|
||||
->setDate(null !== $registrar->attributes()->date ? new \DateTimeImmutable($registrar->attributes()->date) : null);
|
||||
|
||||
$this->em->persist($icannAcreditation);
|
||||
}
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function getTldType(string $tld): ?TldType
|
||||
{
|
||||
if (in_array(strtolower($tld), self::ISO_TLD_EXCEPTION)) {
|
||||
return TldType::ccTLD;
|
||||
}
|
||||
if (in_array(strtolower($tld), self::INFRA_TLD)) {
|
||||
return TldType::iTLD;
|
||||
}
|
||||
if (in_array(strtolower($tld), self::SPONSORED_TLD)) {
|
||||
return TldType::sTLD;
|
||||
}
|
||||
if (in_array(strtolower($tld), self::TEST_TLD)) {
|
||||
return TldType::tTLD;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws DecodingExceptionInterface
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateGTldListICANN(): void
|
||||
{
|
||||
$this->logger->info('Start of retrieval of the list of gTLDs according to ICANN');
|
||||
|
||||
$gTldList = $this->client->request(
|
||||
'GET', self::ICANN_GTLD_LIST_URL
|
||||
)->toArray()['gTLDs'];
|
||||
|
||||
foreach ($gTldList as $gTld) {
|
||||
if ('' === $gTld['gTLD']) {
|
||||
continue;
|
||||
}
|
||||
/** @var Tld|null $gtTldEntity */
|
||||
$gtTldEntity = $this->tldRepository->findOneBy(['tld' => $gTld['gTLD']]);
|
||||
|
||||
if (null === $gtTldEntity) {
|
||||
$gtTldEntity = new Tld();
|
||||
$gtTldEntity->setTld($gTld['gTLD'])->setType(TldType::gTLD);
|
||||
$this->logger->notice('New gTLD detected according to ICANN', [
|
||||
'tld' => $gTld['gTLD'],
|
||||
]);
|
||||
}
|
||||
|
||||
$gtTldEntity
|
||||
->setContractTerminated($gTld['contractTerminated'])
|
||||
->setRegistryOperator($gTld['registryOperator'])
|
||||
->setSpecification13($gTld['specification13']);
|
||||
// NOTICE: sTLDs are listed in ICANN's gTLD list
|
||||
|
||||
if (null !== $gTld['removalDate']) {
|
||||
$gtTldEntity->setRemovalDate(new \DateTimeImmutable($gTld['removalDate']));
|
||||
}
|
||||
if (null !== $gTld['delegationDate']) {
|
||||
$gtTldEntity->setDelegationDate(new \DateTimeImmutable($gTld['delegationDate']));
|
||||
}
|
||||
if (null !== $gTld['dateOfContractSignature']) {
|
||||
$gtTldEntity->setDateOfContractSignature(new \DateTimeImmutable($gTld['dateOfContractSignature']));
|
||||
}
|
||||
$this->em->persist($gtTldEntity);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
namespace App\Service\Provider;
|
||||
|
||||
use App\Dto\Connector\DefaultProviderDto;
|
||||
use App\Entity\Domain;
|
||||
use App\Exception\Provider\UserNoExplicitConsentException;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
@@ -65,7 +66,7 @@ abstract class AbstractProvider
|
||||
*
|
||||
* @return array raw authentication data as supplied by the user
|
||||
*
|
||||
* @throws HttpException when the user does not accept the necessary conditions
|
||||
* @throws UserNoExplicitConsentException when the user does not accept the necessary conditions
|
||||
*/
|
||||
private function verifyLegalAuthData(array $authData): array
|
||||
{
|
||||
@@ -76,7 +77,7 @@ abstract class AbstractProvider
|
||||
if (true !== $acceptConditions
|
||||
|| true !== $ownerLegalAge
|
||||
|| true !== $waiveRetractationPeriod) {
|
||||
throw new HttpException(451, 'The user has not given explicit consent');
|
||||
throw new UserNoExplicitConsentException();
|
||||
}
|
||||
|
||||
return $authData;
|
||||
@@ -1,17 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
namespace App\Service\Provider;
|
||||
|
||||
use App\Dto\Connector\AutodnsProviderDto;
|
||||
use App\Dto\Connector\DefaultProviderDto;
|
||||
use App\Entity\Domain;
|
||||
use App\Exception\Provider\InvalidLoginException;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\HttpClient\HttpOptions;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
@@ -201,26 +201,23 @@ class AutodnsProvider extends AbstractProvider
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws InvalidLoginException
|
||||
*/
|
||||
protected function assertAuthentication(): void
|
||||
{
|
||||
try {
|
||||
$response = $this->client->request(
|
||||
'GET',
|
||||
'/v1/hello',
|
||||
(new HttpOptions())
|
||||
->setAuthBasic($this->authData->username, $this->authData->password)
|
||||
->setHeader('Accept', 'application/json')
|
||||
->setHeader('X-Domainrobot-Context', (string) $this->authData->context)
|
||||
->setBaseUri(self::BASE_URL)
|
||||
->toArray()
|
||||
);
|
||||
} catch (\Exception) {
|
||||
throw new BadRequestHttpException('Invalid Login');
|
||||
}
|
||||
$response = $this->client->request(
|
||||
'GET',
|
||||
'/v1/hello',
|
||||
(new HttpOptions())
|
||||
->setAuthBasic($this->authData->username, $this->authData->password)
|
||||
->setHeader('Accept', 'application/json')
|
||||
->setHeader('X-Domainrobot-Context', (string) $this->authData->context)
|
||||
->setBaseUri(self::BASE_URL)
|
||||
->toArray()
|
||||
);
|
||||
|
||||
if (Response::HTTP_OK !== $response->getStatusCode()) {
|
||||
throw new BadRequestHttpException('The status of these credentials is not valid');
|
||||
throw InvalidLoginException::fromIdentifier($this->authData->username);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
namespace App\Service\Provider;
|
||||
|
||||
interface CheckDomainProviderInterface
|
||||
{
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
namespace App\Service\Provider;
|
||||
|
||||
use App\Dto\Connector\DefaultProviderDto;
|
||||
use App\Dto\Connector\EppClientProviderDto;
|
||||
use App\Entity\Domain;
|
||||
use App\Exception\Provider\EppContactIsAvailableException;
|
||||
use Metaregistrar\EPP\eppCheckContactRequest;
|
||||
use Metaregistrar\EPP\eppCheckContactResponse;
|
||||
use Metaregistrar\EPP\eppCheckDomainRequest;
|
||||
@@ -18,7 +19,6 @@ use Metaregistrar\EPP\eppHelloRequest;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Serializer\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
@@ -57,7 +57,7 @@ class EppClientProvider extends AbstractProvider implements CheckDomainProviderI
|
||||
$resp = $this->eppClient->request(new eppCheckContactRequest($contacts));
|
||||
foreach ($resp->getCheckedContacts() as $contact => $available) {
|
||||
if ($available) {
|
||||
throw new BadRequestHttpException("At least one of the entered contacts cannot be used because it is indicated as available ($contact).");
|
||||
throw EppContactIsAvailableException::fromContact($contact);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
namespace App\Service\Provider;
|
||||
|
||||
use App\Dto\Connector\DefaultProviderDto;
|
||||
use App\Dto\Connector\GandiProviderDto;
|
||||
use App\Entity\Domain;
|
||||
use App\Exception\Provider\DomainOrderFailedExeption;
|
||||
use App\Exception\Provider\InvalidLoginException;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\HttpClient\HttpOptions;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
@@ -31,12 +32,14 @@ class GandiProvider extends AbstractProvider
|
||||
protected DefaultProviderDto $authData;
|
||||
|
||||
private const BASE_URL = 'https://api.gandi.net';
|
||||
private const SANDBOX_BASE_URL = 'https://api.sandbox.gandi.net';
|
||||
|
||||
public function __construct(
|
||||
CacheItemPoolInterface $cacheItemPool,
|
||||
private readonly HttpClientInterface $client,
|
||||
DenormalizerInterface&NormalizerInterface $serializer,
|
||||
ValidatorInterface $validator,
|
||||
private readonly KernelInterface $kernel,
|
||||
) {
|
||||
parent::__construct($cacheItemPool, $serializer, $validator);
|
||||
}
|
||||
@@ -58,14 +61,14 @@ class GandiProvider extends AbstractProvider
|
||||
$user = $this->client->request('GET', '/v5/organization/user-info', (new HttpOptions())
|
||||
->setAuthBearer($this->authData->token)
|
||||
->setHeader('Accept', 'application/json')
|
||||
->setBaseUri(self::BASE_URL)
|
||||
->setBaseUri($this->kernel->isDebug() ? self::SANDBOX_BASE_URL : self::BASE_URL)
|
||||
->toArray()
|
||||
)->toArray();
|
||||
|
||||
$httpOptions = (new HttpOptions())
|
||||
->setAuthBearer($this->authData->token)
|
||||
->setHeader('Accept', 'application/json')
|
||||
->setBaseUri(self::BASE_URL)
|
||||
->setBaseUri($this->kernel->isDebug() ? self::SANDBOX_BASE_URL : self::BASE_URL)
|
||||
->setHeader('Dry-Run', $dryRun ? '1' : '0')
|
||||
->setJson([
|
||||
'fqdn' => $ldhName,
|
||||
@@ -90,28 +93,29 @@ class GandiProvider extends AbstractProvider
|
||||
]);
|
||||
}
|
||||
|
||||
$res = $this->client->request('POST', '/domain/domains', $httpOptions->toArray());
|
||||
$res = $this->client->request('POST', '/v5/domain/domains', $httpOptions->toArray());
|
||||
|
||||
if ((!$dryRun && Response::HTTP_ACCEPTED !== $res->getStatusCode())
|
||||
|| ($dryRun && Response::HTTP_OK !== $res->getStatusCode())) {
|
||||
throw new HttpException($res->toArray()['message']);
|
||||
throw new DomainOrderFailedExeption($res->toArray()['message']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws InvalidLoginException
|
||||
*/
|
||||
protected function assertAuthentication(): void
|
||||
{
|
||||
$response = $this->client->request('GET', '/v5/organization/user-info', (new HttpOptions())
|
||||
->setAuthBearer($this->authData->token)
|
||||
->setHeader('Accept', 'application/json')
|
||||
->setBaseUri(self::BASE_URL)
|
||||
->setBaseUri($this->kernel->isDebug() ? self::SANDBOX_BASE_URL : self::BASE_URL)
|
||||
->toArray()
|
||||
);
|
||||
|
||||
if (Response::HTTP_OK !== $response->getStatusCode()) {
|
||||
throw new BadRequestHttpException('The status of these credentials is not valid');
|
||||
throw new InvalidLoginException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +131,7 @@ class GandiProvider extends AbstractProvider
|
||||
$response = $this->client->request('GET', '/v5/domain/tlds', (new HttpOptions())
|
||||
->setAuthBearer($this->authData->token)
|
||||
->setHeader('Accept', 'application/json')
|
||||
->setBaseUri(self::BASE_URL)
|
||||
->setBaseUri($this->kernel->isDebug() ? self::SANDBOX_BASE_URL : self::BASE_URL)
|
||||
->toArray())->toArray();
|
||||
|
||||
return array_map(fn ($tld) => $tld['name'], $response);
|
||||
@@ -1,17 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
namespace App\Service\Provider;
|
||||
|
||||
use App\Dto\Connector\DefaultProviderDto;
|
||||
use App\Dto\Connector\NameComProviderDto;
|
||||
use App\Entity\Domain;
|
||||
use App\Exception\Provider\InvalidLoginException;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\HttpClient\HttpOptions;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
@@ -57,20 +57,17 @@ class NameComProvider extends AbstractProvider
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/v4/domains',
|
||||
'/core/v1/domains',
|
||||
(new HttpOptions())
|
||||
->setHeader('Accept', 'application/json')
|
||||
->setAuthBasic($this->authData->username, $this->authData->token)
|
||||
->setBaseUri($dryRun ? self::DEV_BASE_URL : self::BASE_URL)
|
||||
->setJson([
|
||||
'domain' => [
|
||||
[
|
||||
'domainName' => $domain->getLdhName(),
|
||||
'locked' => false,
|
||||
'autorenewEnabled' => false,
|
||||
],
|
||||
'domainName' => $domain->getLdhName(),
|
||||
'locked' => false,
|
||||
'autorenewEnabled' => false,
|
||||
'purchaseType' => 'registration',
|
||||
'years' => 1,
|
||||
// 'tldRequirements' => []
|
||||
],
|
||||
])
|
||||
@@ -98,25 +95,22 @@ class NameComProvider extends AbstractProvider
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws InvalidLoginException
|
||||
*/
|
||||
protected function assertAuthentication(): void
|
||||
{
|
||||
try {
|
||||
$response = $this->client->request(
|
||||
'GET',
|
||||
'/v4/hello',
|
||||
(new HttpOptions())
|
||||
->setHeader('Accept', 'application/json')
|
||||
->setAuthBasic($this->authData->username, $this->authData->token)
|
||||
->setBaseUri($this->kernel->isDebug() ? self::DEV_BASE_URL : self::BASE_URL)
|
||||
->toArray()
|
||||
);
|
||||
} catch (\Exception) {
|
||||
throw new BadRequestHttpException('Invalid Login');
|
||||
}
|
||||
$response = $this->client->request(
|
||||
'GET',
|
||||
'/core/v1/hello',
|
||||
(new HttpOptions())
|
||||
->setHeader('Accept', 'application/json')
|
||||
->setAuthBasic($this->authData->username, $this->authData->token)
|
||||
->setBaseUri($this->kernel->isDebug() ? self::DEV_BASE_URL : self::BASE_URL)
|
||||
->toArray()
|
||||
);
|
||||
|
||||
if (Response::HTTP_OK !== $response->getStatusCode()) {
|
||||
throw new BadRequestHttpException('The status of these credentials is not valid');
|
||||
throw InvalidLoginException::fromIdentifier($this->authData->username);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
namespace App\Service\Provider;
|
||||
|
||||
use App\Dto\Connector\DefaultProviderDto;
|
||||
use App\Dto\Connector\NamecheapProviderDto;
|
||||
use App\Entity\Domain;
|
||||
use App\Exception\Provider\NamecheapRequiresAddressException;
|
||||
use App\Exception\Provider\ProviderGenericErrorException;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
@@ -27,8 +29,8 @@ class NamecheapProvider extends AbstractProvider
|
||||
/** @var NamecheapProviderDto */
|
||||
protected DefaultProviderDto $authData;
|
||||
|
||||
public const BASE_URL = 'https://api.namecheap.com/xml.response';
|
||||
public const SANDBOX_BASE_URL = 'https://api.sandbox.namecheap.com/xml.response';
|
||||
private const BASE_URL = 'https://api.namecheap.com/xml.response';
|
||||
private const SANDBOX_BASE_URL = 'https://api.sandbox.namecheap.com/xml.response';
|
||||
|
||||
public function __construct(
|
||||
CacheItemPoolInterface $cacheItemPool,
|
||||
@@ -36,6 +38,7 @@ class NamecheapProvider extends AbstractProvider
|
||||
private readonly string $outgoingIp,
|
||||
DenormalizerInterface&NormalizerInterface $serializer,
|
||||
ValidatorInterface $validator,
|
||||
private readonly KernelInterface $kernel,
|
||||
) {
|
||||
parent::__construct($cacheItemPool, $serializer, $validator);
|
||||
}
|
||||
@@ -49,7 +52,7 @@ class NamecheapProvider extends AbstractProvider
|
||||
$addresses = $this->call('namecheap.users.address.getList', [], $dryRun)->AddressGetListResult->List;
|
||||
|
||||
if (count($addresses) < 1) {
|
||||
throw new BadRequestHttpException('Namecheap account requires at least one address to purchase a domain');
|
||||
throw new NamecheapRequiresAddressException();
|
||||
}
|
||||
|
||||
$addressId = (string) $addresses->attributes()['AddressId'];
|
||||
@@ -98,14 +101,14 @@ class NamecheapProvider extends AbstractProvider
|
||||
'ClientIp' => $this->outgoingIp,
|
||||
], $parameters);
|
||||
|
||||
$response = $this->client->request('POST', $dryRun ? self::SANDBOX_BASE_URL : self::BASE_URL, [
|
||||
$response = $this->client->request('POST', ($this->kernel->isDebug() || $dryRun) ? self::SANDBOX_BASE_URL : self::BASE_URL, [
|
||||
'query' => $actualParams,
|
||||
]);
|
||||
|
||||
$data = new \SimpleXMLElement($response->getContent());
|
||||
|
||||
if ($data->Errors->Error) {
|
||||
throw new BadRequestHttpException($data->Errors->Error);
|
||||
throw new ProviderGenericErrorException($data->Errors->Error);
|
||||
}
|
||||
|
||||
return $data->CommandResponse;
|
||||
@@ -116,13 +119,14 @@ class NamecheapProvider extends AbstractProvider
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws NamecheapRequiresAddressException
|
||||
*/
|
||||
protected function assertAuthentication(): void
|
||||
{
|
||||
$addresses = $this->call('namecheap.users.address.getList', [], false)->AddressGetListResult->List;
|
||||
|
||||
if (count($addresses) < 1) {
|
||||
throw new BadRequestHttpException('Namecheap account requires at least one address to purchase a domain');
|
||||
throw new NamecheapRequiresAddressException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
namespace App\Service\Provider;
|
||||
|
||||
use App\Dto\Connector\DefaultProviderDto;
|
||||
use App\Dto\Connector\OpenProviderProviderDto;
|
||||
use App\Entity\Domain;
|
||||
use App\Service\Provider\AbstractProvider;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
@@ -27,7 +28,7 @@ class OpenProviderProvider extends AbstractProvider
|
||||
/** @var OpenProviderProviderDto */
|
||||
protected DefaultProviderDto $authData;
|
||||
|
||||
private const BASE_URL = 'https://api.openprovider.eu/v1beta';
|
||||
private const string BASE_URL = 'https://api.openprovider.eu/v1beta';
|
||||
|
||||
public function __construct(
|
||||
CacheItemPoolInterface $cacheItemPool,
|
||||
@@ -1,10 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
namespace App\Service\Provider;
|
||||
|
||||
use App\Dto\Connector\DefaultProviderDto;
|
||||
use App\Dto\Connector\OvhProviderDto;
|
||||
use App\Entity\Domain;
|
||||
use App\Exception\Provider\DomainOrderFailedExeption;
|
||||
use App\Exception\Provider\ExpiredLoginException;
|
||||
use App\Exception\Provider\InvalidLoginStatusException;
|
||||
use App\Exception\Provider\PermissionErrorException;
|
||||
use App\Exception\Provider\ProviderGenericErrorException;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Ovh\Api;
|
||||
use Ovh\Exceptions\InvalidParameterException;
|
||||
@@ -12,7 +17,6 @@ use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
@@ -104,7 +108,7 @@ class OvhProvider extends AbstractProvider
|
||||
);
|
||||
if (empty($offer)) {
|
||||
$conn->delete("/order/cart/{$cartId}");
|
||||
throw new \InvalidArgumentException('Cannot buy this domain name');
|
||||
throw new DomainOrderFailedExeption();
|
||||
}
|
||||
|
||||
$item = $conn->post("/order/cart/{$cartId}/domain", [
|
||||
@@ -153,15 +157,15 @@ class OvhProvider extends AbstractProvider
|
||||
try {
|
||||
$res = $conn->get('/auth/currentCredential');
|
||||
if (null !== $res['expiration'] && new \DateTimeImmutable($res['expiration']) < new \DateTimeImmutable()) {
|
||||
throw new BadRequestHttpException('These credentials have expired');
|
||||
throw ExpiredLoginException::fromIdentifier($this->authData->appKey);
|
||||
}
|
||||
|
||||
$status = $res['status'];
|
||||
if ('validated' !== $status) {
|
||||
throw new BadRequestHttpException("The status of these credentials is not valid ($status)");
|
||||
throw InvalidLoginStatusException::fromStatus($status);
|
||||
}
|
||||
} catch (ClientException $exception) {
|
||||
throw new BadRequestHttpException($exception->getMessage());
|
||||
throw new ProviderGenericErrorException($exception->getMessage());
|
||||
}
|
||||
|
||||
foreach (self::REQUIRED_ROUTES as $requiredRoute) {
|
||||
@@ -177,7 +181,7 @@ class OvhProvider extends AbstractProvider
|
||||
}
|
||||
|
||||
if (!$ok) {
|
||||
throw new BadRequestHttpException('This Connector does not have enough permissions on the Provider API. Please recreate this Connector.');
|
||||
throw PermissionErrorException::fromIdentifier($this->authData->appKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ namespace App\Service;
|
||||
use App\Config\DnsKey\Algorithm;
|
||||
use App\Config\DnsKey\DigestType;
|
||||
use App\Config\EventAction;
|
||||
use App\Config\RegistrarStatus;
|
||||
use App\Config\TldType;
|
||||
use App\Entity\DnsKey;
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainEntity;
|
||||
@@ -18,28 +16,30 @@ use App\Entity\Nameserver;
|
||||
use App\Entity\NameserverEntity;
|
||||
use App\Entity\RdapServer;
|
||||
use App\Entity\Tld;
|
||||
use App\Exception\DomainNotFoundException;
|
||||
use App\Exception\MalformedDomainException;
|
||||
use App\Exception\TldNotSupportedException;
|
||||
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;
|
||||
use App\Repository\NameserverEntityRepository;
|
||||
use App\Repository\NameserverRepository;
|
||||
use App\Repository\RdapServerRepository;
|
||||
use App\Repository\TldRepository;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
@@ -48,39 +48,6 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
class RDAPService
|
||||
{
|
||||
/* @see https://www.iana.org/domains/root/db */
|
||||
public const ISO_TLD_EXCEPTION = ['ac', 'eu', 'uk', 'su', 'tp'];
|
||||
public const INFRA_TLD = ['arpa'];
|
||||
public const SPONSORED_TLD = [
|
||||
'aero',
|
||||
'asia',
|
||||
'cat',
|
||||
'coop',
|
||||
'edu',
|
||||
'gov',
|
||||
'int',
|
||||
'jobs',
|
||||
'mil',
|
||||
'museum',
|
||||
'post',
|
||||
'tel',
|
||||
'travel',
|
||||
'xxx',
|
||||
];
|
||||
public const TEST_TLD = [
|
||||
'xn--kgbechtv',
|
||||
'xn--hgbk6aj7f53bba',
|
||||
'xn--0zwm56d',
|
||||
'xn--g6w251d',
|
||||
'xn--80akhbyknj4f',
|
||||
'xn--11b5bs3a9aj6g',
|
||||
'xn--jxalpdlp',
|
||||
'xn--9t4b11yi5a',
|
||||
'xn--deba0ad',
|
||||
'xn--zckzah',
|
||||
'xn--hlcj6aya9esc7a',
|
||||
];
|
||||
|
||||
public const ENTITY_HANDLE_BLACKLIST = [
|
||||
'REDACTED_FOR_PRIVACY',
|
||||
'ANO00-FRNIC',
|
||||
@@ -96,29 +63,39 @@ class RDAPService
|
||||
'Private',
|
||||
];
|
||||
|
||||
public function __construct(private HttpClientInterface $client,
|
||||
private EntityRepository $entityRepository,
|
||||
private DomainRepository $domainRepository,
|
||||
private DomainEventRepository $domainEventRepository,
|
||||
private NameserverRepository $nameserverRepository,
|
||||
private NameserverEntityRepository $nameserverEntityRepository,
|
||||
private EntityEventRepository $entityEventRepository,
|
||||
private DomainEntityRepository $domainEntityRepository,
|
||||
private RdapServerRepository $rdapServerRepository,
|
||||
private TldRepository $tldRepository,
|
||||
private EntityManagerInterface $em,
|
||||
private LoggerInterface $logger,
|
||||
private StatService $statService,
|
||||
private InfluxdbService $influxService,
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $client,
|
||||
private readonly EntityRepository $entityRepository,
|
||||
private readonly DomainRepository $domainRepository,
|
||||
private readonly DomainEventRepository $domainEventRepository,
|
||||
private readonly NameserverRepository $nameserverRepository,
|
||||
private readonly NameserverEntityRepository $nameserverEntityRepository,
|
||||
private readonly EntityEventRepository $entityEventRepository,
|
||||
private readonly DomainEntityRepository $domainEntityRepository,
|
||||
private readonly RdapServerRepository $rdapServerRepository,
|
||||
private readonly TldRepository $tldRepository,
|
||||
private readonly IcannAccreditationRepository $icannAccreditationRepository,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly StatService $statService,
|
||||
private readonly InfluxdbService $influxService,
|
||||
#[Autowire(param: 'influxdb_enabled')]
|
||||
private bool $influxdbEnabled,
|
||||
private readonly bool $influxdbEnabled,
|
||||
private readonly DomainStatusRepository $domainStatusRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws HttpExceptionInterface
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws DomainNotFoundException
|
||||
* @throws DecodingExceptionInterface
|
||||
* @throws TldNotSupportedException
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws OptimisticLockException
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws MalformedDomainException
|
||||
* @throws UnknownRdapServerException
|
||||
*/
|
||||
public function registerDomains(array $domains): void
|
||||
{
|
||||
@@ -128,11 +105,16 @@ class RDAPService
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws DomainNotFoundException
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws DecodingExceptionInterface
|
||||
* @throws TldNotSupportedException
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws OptimisticLockException
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws MalformedDomainException
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws UnknownRdapServerException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function registerDomain(string $fqdn): Domain
|
||||
@@ -140,8 +122,8 @@ class RDAPService
|
||||
$idnDomain = RDAPService::convertToIdn($fqdn);
|
||||
$tld = $this->getTld($idnDomain);
|
||||
|
||||
$this->logger->info('An update request for domain name {idnDomain} is requested.', [
|
||||
'idnDomain' => $idnDomain,
|
||||
$this->logger->debug('Update request for a domain name is requested', [
|
||||
'ldhName' => $idnDomain,
|
||||
]);
|
||||
|
||||
$rdapServer = $this->fetchRdapServer($tld);
|
||||
@@ -160,7 +142,7 @@ class RDAPService
|
||||
$this->updateDomainStatus($domain, $rdapData);
|
||||
|
||||
if (in_array('free', $domain->getStatus())) {
|
||||
throw new NotFoundHttpException("The domain name $idnDomain is not present in the WHOIS database.");
|
||||
throw DomainNotFoundException::fromDomain($idnDomain);
|
||||
}
|
||||
|
||||
$domain
|
||||
@@ -182,46 +164,69 @@ class RDAPService
|
||||
return $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TldNotSupportedException
|
||||
* @throws MalformedDomainException
|
||||
*/
|
||||
public function getTld(string $domain): Tld
|
||||
{
|
||||
if (!str_contains($domain, '.')) {
|
||||
$tldEntity = $this->tldRepository->findOneBy(['tld' => '.']);
|
||||
if (!str_contains($domain, OfficialDataService::DOMAIN_DOT)) {
|
||||
$tldEntity = $this->tldRepository->findOneBy(['tld' => OfficialDataService::DOMAIN_DOT]);
|
||||
|
||||
if (null == $tldEntity) {
|
||||
throw new NotFoundHttpException("The requested TLD $domain is not yet supported, please try again with another one");
|
||||
throw TldNotSupportedException::fromTld(OfficialDataService::DOMAIN_DOT);
|
||||
}
|
||||
|
||||
return $tldEntity;
|
||||
}
|
||||
|
||||
$lastDotPosition = strrpos($domain, '.');
|
||||
$lastDotPosition = strrpos($domain, OfficialDataService::DOMAIN_DOT);
|
||||
|
||||
if (false === $lastDotPosition) {
|
||||
throw new BadRequestException('Domain must contain at least one dot');
|
||||
throw MalformedDomainException::fromDomain($domain);
|
||||
}
|
||||
|
||||
$tld = self::convertToIdn(substr($domain, $lastDotPosition + 1));
|
||||
$tldEntity = $this->tldRepository->findOneBy(['tld' => $tld]);
|
||||
$tldEntity = $this->tldRepository->findOneBy(['tld' => $tld, 'deletedAt' => null]);
|
||||
|
||||
if (null === $tldEntity) {
|
||||
throw new NotFoundHttpException("The requested TLD $tld is not yet supported, please try again with another one");
|
||||
$this->logger->debug('Domain name cannot be updated because the TLD is not supported', [
|
||||
'ldhName' => $domain,
|
||||
]);
|
||||
throw TldNotSupportedException::fromTld($tld);
|
||||
}
|
||||
|
||||
return $tldEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MalformedDomainException
|
||||
*/
|
||||
public static function convertToIdn(string $fqdn): string
|
||||
{
|
||||
return strtolower(idn_to_ascii($fqdn));
|
||||
$ascii = strtolower(idn_to_ascii($fqdn));
|
||||
|
||||
if (OfficialDataService::DOMAIN_DOT !== $fqdn && !preg_match('/^(xn--)?[a-z0-9-]+(\.[a-z0-9-]+)*$/', $ascii)) {
|
||||
throw MalformedDomainException::fromDomain($fqdn);
|
||||
}
|
||||
|
||||
return $ascii;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnknownRdapServerException
|
||||
*/
|
||||
private function fetchRdapServer(Tld $tld): RdapServer
|
||||
{
|
||||
$tldString = $tld->getTld();
|
||||
$rdapServer = $this->rdapServerRepository->findOneBy(['tld' => $tldString], ['updatedAt' => 'DESC']);
|
||||
|
||||
if (null === $rdapServer) {
|
||||
throw new NotFoundHttpException("TLD $tldString : Unable to determine which RDAP server to contact");
|
||||
$this->logger->debug('Unable to determine which RDAP server to contact', [
|
||||
'tld' => $tldString,
|
||||
]);
|
||||
|
||||
throw UnknownRdapServerException::fromTld($tldString);
|
||||
}
|
||||
|
||||
return $rdapServer;
|
||||
@@ -238,15 +243,14 @@ class RDAPService
|
||||
private function fetchRdapResponse(RdapServer $rdapServer, string $idnDomain, ?Domain $domain): array
|
||||
{
|
||||
$rdapServerUrl = $rdapServer->getUrl();
|
||||
$this->logger->notice('An RDAP query to update the domain name {idnDomain} will be made to {server}.', [
|
||||
'idnDomain' => $idnDomain,
|
||||
$this->logger->info('An RDAP query to update this domain name will be made', [
|
||||
'ldhName' => $idnDomain,
|
||||
'server' => $rdapServerUrl,
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->statService->incrementStat('stats.rdap_queries.count');
|
||||
|
||||
$req = $this->client->request('GET', $rdapServerUrl.'domain/'.$idnDomain);
|
||||
$this->statService->incrementStat('stats.rdap_queries.count');
|
||||
|
||||
return $req->toArray();
|
||||
} catch (\Exception $e) {
|
||||
@@ -269,8 +273,8 @@ class RDAPService
|
||||
|| ($e instanceof TransportExceptionInterface && null !== $response && !in_array('content-length', $response->getHeaders(false)) && 404 === $response->getStatusCode())
|
||||
) {
|
||||
if (null !== $domain) {
|
||||
$this->logger->notice('The domain name {idnDomain} has been deleted from the WHOIS database.', [
|
||||
'idnDomain' => $idnDomain,
|
||||
$this->logger->info('Domain name has been deleted from the WHOIS database', [
|
||||
'ldhName' => $idnDomain,
|
||||
]);
|
||||
|
||||
$domain->updateTimestamps();
|
||||
@@ -285,13 +289,16 @@ class RDAPService
|
||||
}
|
||||
|
||||
$domain->setDeleted(true);
|
||||
$this->em->persist($domain);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
throw new NotFoundHttpException("The domain name $idnDomain is not present in the WHOIS database.");
|
||||
throw DomainNotFoundException::fromDomain($idnDomain);
|
||||
}
|
||||
|
||||
$this->logger->error('Unable to perform an RDAP query for this domain name', [
|
||||
'ldhName' => $idnDomain,
|
||||
]);
|
||||
|
||||
return $e;
|
||||
}
|
||||
|
||||
@@ -299,8 +306,8 @@ class RDAPService
|
||||
{
|
||||
$domain = new Domain();
|
||||
|
||||
$this->logger->info('The domain name {idnDomain} was not known to this Domain Watchdog instance.', [
|
||||
'idnDomain' => $idnDomain,
|
||||
$this->logger->debug('Domain name was not known to this instance', [
|
||||
'ldhName' => $idnDomain,
|
||||
]);
|
||||
|
||||
return $domain->setTld($tld)->setLdhName($idnDomain)->setDeleted(false);
|
||||
@@ -308,8 +315,8 @@ class RDAPService
|
||||
|
||||
private function updateDomainStatus(Domain $domain, array $rdapData): void
|
||||
{
|
||||
if (isset($rdapData['status'])) {
|
||||
$status = array_unique($rdapData['status']);
|
||||
if (isset($rdapData['status']) && is_array($rdapData['status'])) {
|
||||
$status = array_map(fn ($s) => strtolower($s), array_unique($rdapData['status']));
|
||||
$addedStatus = array_diff($status, $domain->getStatus());
|
||||
$deletedStatus = array_diff($domain->getStatus(), $status);
|
||||
$domain->setStatus($status);
|
||||
@@ -327,8 +334,8 @@ class RDAPService
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->logger->warning('The domain name {idnDomain} has no WHOIS status.', [
|
||||
'idnDomain' => $domain->getLdhName(),
|
||||
$this->logger->warning('Domain name has no WHOIS status', [
|
||||
'ldhName' => $domain->getLdhName(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -338,21 +345,18 @@ class RDAPService
|
||||
if (isset($rdapData['handle'])) {
|
||||
$domain->setHandle($rdapData['handle']);
|
||||
} else {
|
||||
$this->logger->warning('The domain name {idnDomain} has no handle key.', [
|
||||
'idnDomain' => $domain->getLdhName(),
|
||||
$this->logger->warning('Domain name has no handle key', [
|
||||
'ldhName' => $domain->getLdhName(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateMalformedStringException
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function updateDomainEvents(Domain $domain, array $rdapData): void
|
||||
{
|
||||
foreach ($domain->getEvents()->getIterator() as $event) {
|
||||
$event->setDeleted(true);
|
||||
}
|
||||
$this->domainEventRepository->setDomainEventAsDeleted($domain);
|
||||
|
||||
if (isset($rdapData['events']) && is_array($rdapData['events'])) {
|
||||
foreach ($rdapData['events'] as $rdapEvent) {
|
||||
@@ -368,10 +372,14 @@ class RDAPService
|
||||
|
||||
if (null === $event) {
|
||||
$event = new DomainEvent();
|
||||
} else {
|
||||
// at this point Doctrine doesn't know that the events are
|
||||
// deleted in the database, so refresh in order to make the diff work
|
||||
$this->em->refresh($event);
|
||||
}
|
||||
|
||||
$domain->addEvent($event
|
||||
->setAction($rdapEvent['eventAction'])
|
||||
->setAction(strtolower($rdapEvent['eventAction']))
|
||||
->setDate(new \DateTimeImmutable($rdapEvent['eventDate']))
|
||||
->setDeleted(false));
|
||||
|
||||
@@ -381,14 +389,11 @@ class RDAPService
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateMalformedStringException
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function updateDomainEntities(Domain $domain, array $rdapData): void
|
||||
{
|
||||
foreach ($domain->getDomainEntities()->getIterator() as $domainEntity) {
|
||||
$domainEntity->setDeleted(true);
|
||||
}
|
||||
$this->domainEntityRepository->setDomainEntityAsDeleted($domain);
|
||||
|
||||
if (!isset($rdapData['entities']) || !is_array($rdapData['entities'])) {
|
||||
return;
|
||||
@@ -405,13 +410,15 @@ class RDAPService
|
||||
|
||||
if (null === $domainEntity) {
|
||||
$domainEntity = new DomainEntity();
|
||||
} else {
|
||||
$this->em->refresh($domainEntity);
|
||||
}
|
||||
|
||||
$domain->addDomainEntity($domainEntity
|
||||
->setDomain($domain)
|
||||
->setEntity($entity)
|
||||
->setRoles($roles)
|
||||
->setDeleted(false));
|
||||
->setDeletedAt(null));
|
||||
|
||||
$this->em->persist($domainEntity);
|
||||
$this->em->flush();
|
||||
@@ -419,11 +426,11 @@ class RDAPService
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateMalformedStringException
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function updateDomainNameservers(Domain $domain, array $rdapData): void
|
||||
{
|
||||
if (array_key_exists('nameservers', $rdapData) && is_array($rdapData['nameservers'])) {
|
||||
if (isset($rdapData['nameservers']) && is_array($rdapData['nameservers'])) {
|
||||
$domain->getNameservers()->clear();
|
||||
$this->em->persist($domain);
|
||||
|
||||
@@ -436,19 +443,20 @@ class RDAPService
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->logger->warning('The domain name {idnDomain} has no nameservers.', [
|
||||
'idnDomain' => $domain->getLdhName(),
|
||||
$this->logger->warning('Domain name has no nameservers', [
|
||||
'ldhName' => $domain->getLdhName(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchOrCreateNameserver(array $rdapNameserver, Domain $domain): Nameserver
|
||||
{
|
||||
$ldhName = strtolower(rtrim($rdapNameserver['ldhName'], '.'));
|
||||
$nameserver = $this->nameserverRepository->findOneBy([
|
||||
'ldhName' => strtolower($rdapNameserver['ldhName']),
|
||||
'ldhName' => $ldhName,
|
||||
]);
|
||||
|
||||
$existingDomainNS = $domain->getNameservers()->findFirst(fn (int $key, Nameserver $ns) => $ns->getLdhName() === $rdapNameserver['ldhName']);
|
||||
$existingDomainNS = $domain->getNameservers()->findFirst(fn (int $key, Nameserver $ns) => $ns->getLdhName() === $ldhName);
|
||||
|
||||
if (null !== $existingDomainNS) {
|
||||
return $existingDomainNS;
|
||||
@@ -456,13 +464,13 @@ class RDAPService
|
||||
$nameserver = new Nameserver();
|
||||
}
|
||||
|
||||
$nameserver->setLdhName($rdapNameserver['ldhName']);
|
||||
$nameserver->setLdhName($ldhName);
|
||||
|
||||
return $nameserver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateMalformedStringException
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function updateNameserverEntities(Nameserver $nameserver, array $rdapNameserver, Tld $tld): void
|
||||
{
|
||||
@@ -486,7 +494,7 @@ class RDAPService
|
||||
$nameserver->addNameserverEntity($nameserverEntity
|
||||
->setNameserver($nameserver)
|
||||
->setEntity($entity)
|
||||
->setStatus(array_unique($rdapNameserver['status']))
|
||||
->setStatus(array_map(fn ($s) => strtolower($s), array_unique($rdapNameserver['status'])))
|
||||
->setRoles($roles));
|
||||
|
||||
$this->em->persist($nameserverEntity);
|
||||
@@ -514,40 +522,14 @@ class RDAPService
|
||||
$roles = array_merge(...$roles);
|
||||
}
|
||||
|
||||
return $roles;
|
||||
return array_map(fn ($x) => strtolower($x), $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateMalformedStringException
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function registerEntity(array $rdapEntity, array $roles, string $domain, Tld $tld): Entity
|
||||
{
|
||||
$entity = null;
|
||||
|
||||
/**
|
||||
* If the RDAP server transmits the entity's IANA number, it is used as a priority to identify the entity.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7483#section-4.8
|
||||
*/
|
||||
$isIANAid = false;
|
||||
if (isset($rdapEntity['publicIds'])) {
|
||||
foreach ($rdapEntity['publicIds'] as $publicId) {
|
||||
if ('IANA Registrar ID' === $publicId['type'] && isset($publicId['identifier']) && '' !== $publicId['identifier']) {
|
||||
$entity = $this->entityRepository->findOneBy([
|
||||
'handle' => $publicId['identifier'],
|
||||
'tld' => null,
|
||||
]);
|
||||
|
||||
if (null !== $entity) {
|
||||
$rdapEntity['handle'] = $publicId['identifier'];
|
||||
$isIANAid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If there is no number to identify the entity, one is generated from the domain name and the roles associated with this entity
|
||||
*/
|
||||
@@ -555,31 +537,43 @@ class RDAPService
|
||||
sort($roles);
|
||||
$rdapEntity['handle'] = 'DW-FAKEHANDLE-'.$domain.'-'.implode(',', $roles);
|
||||
|
||||
$this->logger->warning('The entity {handle} has no handle key.', [
|
||||
$this->logger->warning('Entity has no handle key', [
|
||||
'handle' => $rdapEntity['handle'],
|
||||
'ldhName' => $domain,
|
||||
]);
|
||||
}
|
||||
|
||||
if (null === $entity) {
|
||||
$entity = $this->entityRepository->findOneBy([
|
||||
'handle' => $rdapEntity['handle'],
|
||||
'tld' => $tld,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($isIANAid && null !== $entity) {
|
||||
return $entity;
|
||||
}
|
||||
$entity = $this->entityRepository->findOneBy([
|
||||
'handle' => $rdapEntity['handle'],
|
||||
'tld' => $tld,
|
||||
]);
|
||||
|
||||
if (null === $entity) {
|
||||
$entity = (new Entity())->setTld($tld);
|
||||
|
||||
$this->logger->info('The entity {handle} was not known to this Domain Watchdog instance.', [
|
||||
$this->logger->debug('Entity was not known to this instance', [
|
||||
'handle' => $rdapEntity['handle'],
|
||||
'ldhName' => $domain,
|
||||
]);
|
||||
}
|
||||
|
||||
$entity->setHandle($rdapEntity['handle']);
|
||||
/**
|
||||
* If the RDAP server transmits the entity's IANA number, it is used as a priority to identify the entity.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc7483#section-4.8
|
||||
*/
|
||||
$icannAccreditation = null;
|
||||
if (isset($rdapEntity['publicIds'])) {
|
||||
foreach ($rdapEntity['publicIds'] as $publicId) {
|
||||
if ('IANA Registrar ID' === $publicId['type'] && isset($publicId['identifier']) && '' !== $publicId['identifier']) {
|
||||
$icannAccreditation = $this->icannAccreditationRepository->findOneBy([
|
||||
'id' => (int) $publicId['identifier'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$entity->setHandle($rdapEntity['handle'])->setIcannAccreditation($icannAccreditation);
|
||||
|
||||
if (isset($rdapEntity['remarks']) && is_array($rdapEntity['remarks'])) {
|
||||
$entity->setRemarks($rdapEntity['remarks']);
|
||||
@@ -644,7 +638,7 @@ class RDAPService
|
||||
$entity->addEvent(
|
||||
(new EntityEvent())
|
||||
->setEntity($entity)
|
||||
->setAction($rdapEntityEvent['eventAction'])
|
||||
->setAction(strtolower($rdapEntityEvent['eventAction']))
|
||||
->setDate(new \DateTimeImmutable($rdapEntityEvent['eventDate']))
|
||||
->setDeleted(false));
|
||||
}
|
||||
@@ -675,12 +669,18 @@ class RDAPService
|
||||
try {
|
||||
$blob = hex2bin($rdapDsData['digest']);
|
||||
} catch (\Exception) {
|
||||
$this->logger->warning('DNSSEC digest is not a valid hexadecimal value.');
|
||||
$this->logger->warning('DNSSEC digest is not a valid hexadecimal value', [
|
||||
'ldhName' => $domain,
|
||||
'value' => $rdapDsData['digest'],
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (false === $blob) {
|
||||
$this->logger->warning('DNSSEC digest is not a valid hexadecimal value.');
|
||||
$this->logger->warning('DNSSEC digest is not a valid hexadecimal value', [
|
||||
'ldhName' => $domain,
|
||||
'value' => $rdapDsData['digest'],
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
$dsData->setDigest($blob);
|
||||
@@ -700,7 +700,11 @@ class RDAPService
|
||||
|
||||
if (array_key_exists($dsData->getDigestType()->value, $digestLengthByte)
|
||||
&& strlen($dsData->getDigest()) / 2 !== $digestLengthByte[$dsData->getDigestType()->value]) {
|
||||
$this->logger->warning('DNSSEC digest does not have a valid length according to the digest type.');
|
||||
$this->logger->warning('DNSSEC digest does not have a valid length according to the digest type', [
|
||||
'ldhName' => $domain,
|
||||
'value' => $dsData->getDigest(),
|
||||
'type' => $dsData->getDigestType()->name,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -708,237 +712,152 @@ class RDAPService
|
||||
$this->em->persist($dsData);
|
||||
}
|
||||
} else {
|
||||
$this->logger->warning('The domain name {idnDomain} has no DS record.', [
|
||||
'idnDomain' => $domain->getLdhName(),
|
||||
$this->logger->warning('Domain name has no DS record', [
|
||||
'ldhName' => $domain->getLdhName(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws DecodingExceptionInterface
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function updateRDAPServersFromIANA(): void
|
||||
private function calculateDaysFromStatus(Domain $domain, \DateTimeImmutable $now): ?int
|
||||
{
|
||||
$this->logger->info('Start of update the RDAP server list from IANA.');
|
||||
/** @var ?DomainStatus $lastStatus */
|
||||
$lastStatus = $this->domainStatusRepository->findLastDomainStatus($domain);
|
||||
|
||||
$dnsRoot = $this->client->request(
|
||||
'GET', 'https://data.iana.org/rdap/dns.json'
|
||||
)->toArray();
|
||||
|
||||
$this->updateRDAPServers($dnsRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function updateRDAPServers(array $dnsRoot): void
|
||||
{
|
||||
foreach ($dnsRoot['services'] as $service) {
|
||||
foreach ($service[0] as $tld) {
|
||||
if ('.' === $tld && null === $this->tldRepository->findOneBy(['tld' => $tld])) {
|
||||
$this->em->persist((new Tld())->setTld('.')->setType(TldType::root));
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
$tldEntity = $this->tldRepository->findOneBy(['tld' => $tld]);
|
||||
if (null === $tldEntity) {
|
||||
$tldEntity = (new Tld())->setTld($tld)->setType(TldType::gTLD);
|
||||
$this->em->persist($tldEntity);
|
||||
}
|
||||
|
||||
foreach ($service[1] as $rdapServerUrl) {
|
||||
$server = $this->rdapServerRepository->findOneBy(['tld' => $tldEntity->getTld(), 'url' => $rdapServerUrl]);
|
||||
|
||||
if (null === $server) {
|
||||
$server = new RdapServer();
|
||||
}
|
||||
|
||||
$server
|
||||
->setTld($tldEntity)
|
||||
->setUrl($rdapServerUrl)
|
||||
->setUpdatedAt(new \DateTimeImmutable($dnsRoot['publication'] ?? 'now'));
|
||||
|
||||
$this->em->persist($server);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function updateRDAPServersFromFile(string $fileName): void
|
||||
{
|
||||
if (!file_exists($fileName)) {
|
||||
return;
|
||||
if (null === $lastStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->logger->info('Start of update the RDAP server list from custom config file.');
|
||||
$this->updateRDAPServers(Yaml::parseFile($fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws ClientExceptionInterface
|
||||
*/
|
||||
public function updateTldListIANA(): void
|
||||
{
|
||||
$this->logger->info('Start of retrieval of the list of TLDs according to IANA.');
|
||||
$tldList = array_map(
|
||||
fn ($tld) => strtolower($tld),
|
||||
explode(PHP_EOL,
|
||||
$this->client->request(
|
||||
'GET', 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt'
|
||||
)->getContent()
|
||||
));
|
||||
array_shift($tldList);
|
||||
|
||||
foreach ($tldList as $tld) {
|
||||
if ('' === $tld) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tldEntity = $this->tldRepository->findOneBy(['tld' => $tld]);
|
||||
|
||||
if (null === $tldEntity) {
|
||||
$tldEntity = new Tld();
|
||||
$tldEntity->setTld($tld);
|
||||
|
||||
$this->logger->notice('New TLD detected according to IANA ({tld}).', [
|
||||
'tld' => $tld,
|
||||
]);
|
||||
}
|
||||
|
||||
$type = $this->getTldType($tld);
|
||||
|
||||
if (null !== $type) {
|
||||
$tldEntity->setType($type);
|
||||
} elseif (null === $tldEntity->isContractTerminated()) { // ICANN managed, must be a ccTLD
|
||||
$tldEntity->setType(TldType::ccTLD);
|
||||
} else {
|
||||
$tldEntity->setType(TldType::gTLD);
|
||||
}
|
||||
|
||||
$this->em->persist($tldEntity);
|
||||
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')));
|
||||
}
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws DecodingExceptionInterface
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateRegistrarListIANA(): void
|
||||
{
|
||||
$this->logger->info('Start of retrieval of the list of Registrar IDs according to IANA.');
|
||||
$registrarList = $this->client->request(
|
||||
'GET', 'https://www.iana.org/assignments/registrar-ids/registrar-ids.xml'
|
||||
);
|
||||
|
||||
$data = new \SimpleXMLElement($registrarList->getContent());
|
||||
|
||||
foreach ($data->registry->record as $registrar) {
|
||||
$entity = $this->entityRepository->findOneBy(['handle' => $registrar->value, 'tld' => null]);
|
||||
if (null === $entity) {
|
||||
$entity = new Entity();
|
||||
}
|
||||
$entity
|
||||
->setHandle($registrar->value)
|
||||
->setTld(null)
|
||||
->setJCard(['vcard', [['version', [], 'text', '4.0'], ['fn', [], 'text', (string) $registrar->name]]])
|
||||
->setRemarks(null)
|
||||
->getIcannAccreditation()
|
||||
->setRegistrarName($registrar->name)
|
||||
->setStatus(RegistrarStatus::from($registrar->status))
|
||||
->setRdapBaseUrl($registrar->rdapurl->count() ? ($registrar->rdapurl->server) : null)
|
||||
->setUpdated(null !== $registrar->attributes()->updated ? new \DateTimeImmutable($registrar->attributes()->updated) : null)
|
||||
->setDate(null !== $registrar->attributes()->date ? new \DateTimeImmutable($registrar->attributes()->date) : null);
|
||||
|
||||
$this->em->persist($entity);
|
||||
}
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function getTldType(string $tld): ?TldType
|
||||
{
|
||||
if (in_array(strtolower($tld), self::ISO_TLD_EXCEPTION)) {
|
||||
return TldType::ccTLD;
|
||||
}
|
||||
if (in_array(strtolower($tld), self::INFRA_TLD)) {
|
||||
return TldType::iTLD;
|
||||
}
|
||||
if (in_array(strtolower($tld), self::SPONSORED_TLD)) {
|
||||
return TldType::sTLD;
|
||||
}
|
||||
if (in_array(strtolower($tld), self::TEST_TLD)) {
|
||||
return TldType::tTLD;
|
||||
if ($domain->isRedemptionPeriod()
|
||||
&& in_array('redemption period', $lastStatus->getAddStatus())
|
||||
) {
|
||||
return self::daysBetween($now, $lastStatus->getCreatedAt()->add(new \DateInterval('P'.(30 + 5).'D')));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws DecodingExceptionInterface
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateGTldListICANN(): void
|
||||
private function getRelevantDates(Domain $domain): array
|
||||
{
|
||||
$this->logger->info('Start of retrieval of the list of gTLDs according to ICANN.');
|
||||
/** @var ?DomainEvent $expirationEvent */
|
||||
$expirationEvent = $this->domainEventRepository->findLastDomainEvent($domain, 'expiration');
|
||||
/** @var ?DomainEvent $deletionEvent */
|
||||
$deletionEvent = $this->domainEventRepository->findLastDomainEvent($domain, 'deletion');
|
||||
|
||||
$gTldList = $this->client->request(
|
||||
'GET', 'https://www.icann.org/resources/registries/gtlds/v2/gtlds.json'
|
||||
)->toArray()['gTLDs'];
|
||||
return [$expirationEvent?->getDate(), $deletionEvent?->getDate()];
|
||||
}
|
||||
|
||||
foreach ($gTldList as $gTld) {
|
||||
if ('' === $gTld['gTLD']) {
|
||||
continue;
|
||||
}
|
||||
/** @var Tld|null $gtTldEntity */
|
||||
$gtTldEntity = $this->tldRepository->findOneBy(['tld' => $gTld['gTLD']]);
|
||||
|
||||
if (null === $gtTldEntity) {
|
||||
$gtTldEntity = new Tld();
|
||||
$gtTldEntity->setTld($gTld['gTLD'])->setType(TldType::gTLD);
|
||||
$this->logger->notice('New gTLD detected according to ICANN ({tld}).', [
|
||||
'tld' => $gTld['gTLD'],
|
||||
]);
|
||||
}
|
||||
|
||||
$gtTldEntity
|
||||
->setContractTerminated($gTld['contractTerminated'])
|
||||
->setRegistryOperator($gTld['registryOperator'])
|
||||
->setSpecification13($gTld['specification13']);
|
||||
// NOTICE: sTLDs are listed in ICANN's gTLD list
|
||||
|
||||
if (null !== $gTld['removalDate']) {
|
||||
$gtTldEntity->setRemovalDate(new \DateTimeImmutable($gTld['removalDate']));
|
||||
}
|
||||
if (null !== $gTld['delegationDate']) {
|
||||
$gtTldEntity->setDelegationDate(new \DateTimeImmutable($gTld['delegationDate']));
|
||||
}
|
||||
if (null !== $gTld['dateOfContractSignature']) {
|
||||
$gtTldEntity->setDateOfContractSignature(new \DateTimeImmutable($gTld['dateOfContractSignature']));
|
||||
}
|
||||
$this->em->persist($gtTldEntity);
|
||||
public function getExpiresInDays(Domain $domain): ?int
|
||||
{
|
||||
if ($domain->getDeleted()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
$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),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,39 +5,94 @@ namespace App\State;
|
||||
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\EntityManagerInterface;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Random\Randomizer;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
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
|
||||
{
|
||||
public function __construct(
|
||||
#[Autowire(service: 'api_platform.doctrine.orm.state.item_provider')]
|
||||
private ProviderInterface $itemProvider,
|
||||
private RDAPService $RDAPService,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private KernelInterface $kernel,
|
||||
private ParameterBagInterface $parameterBag,
|
||||
private RateLimiterFactory $rdapRequestsLimiter, private Security $security,
|
||||
private RateLimiterFactory $rdapRequestsLimiter,
|
||||
private Security $security,
|
||||
private LoggerInterface $logger,
|
||||
private DomainRepository $domainRepository,
|
||||
private MessageBusInterface $bus,
|
||||
private RequestStack $requestStack,
|
||||
private EntityManagerInterface $em,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
$domain = $this->itemProvider->provide($operation, $uriVariables, $context);
|
||||
$fromWatchlist = array_key_exists('root_operation', $context) && Watchlist::class === $context['root_operation']?->getClass();
|
||||
|
||||
$userId = $this->security->getUser()->getUserIdentifier();
|
||||
$idnDomain = RDAPService::convertToIdn($uriVariables['ldhName']);
|
||||
|
||||
$this->logger->info('User wants to update a domain name', [
|
||||
'username' => $userId,
|
||||
'ldhName' => $idnDomain,
|
||||
]);
|
||||
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
|
||||
/** @var ?Domain $domain */
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $idnDomain]);
|
||||
// If the domain name exists in the database, recently updated and not important, we return the stored Domain
|
||||
if (null !== $domain
|
||||
&& !$domain->getDeleted()
|
||||
&& !$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', [
|
||||
'ldhName' => $idnDomain,
|
||||
'updatedAt' => $domain->getUpdatedAt()->format(\DateTimeInterface::ATOM),
|
||||
]);
|
||||
|
||||
if (!is_null($domain)) {
|
||||
return $domain;
|
||||
}
|
||||
|
||||
if (false === $this->kernel->isDebug() && true === $this->parameterBag->get('limited_features')) {
|
||||
$limiter = $this->rdapRequestsLimiter->create($this->security->getUser()->getUserIdentifier());
|
||||
$limiter = $this->rdapRequestsLimiter->create($userId);
|
||||
$limit = $limiter->consume();
|
||||
|
||||
if (!$limit->isAccepted()) {
|
||||
@@ -45,19 +100,38 @@ readonly class AutoRegisterDomainProvider implements ProviderInterface
|
||||
}
|
||||
}
|
||||
|
||||
$ldhName = RDAPService::convertToIdn($uriVariables['ldhName']);
|
||||
$updatedAt = null === $domain ? new \DateTimeImmutable('now') : $domain->getUpdatedAt();
|
||||
|
||||
try {
|
||||
$domain = $this->RDAPService->registerDomain($ldhName);
|
||||
} catch (NotFoundHttpException) {
|
||||
$domain = $this->RDAPService->registerDomain($idnDomain);
|
||||
} catch (DomainNotFoundException $exception) {
|
||||
if (!$fromWatchlist) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $idnDomain]);
|
||||
if (null !== $domain) {
|
||||
return $domain;
|
||||
}
|
||||
|
||||
$domain = (new Domain())
|
||||
->setLdhName($ldhName)
|
||||
->setTld($this->RDAPService->getTld($ldhName))
|
||||
->setLdhName($idnDomain)
|
||||
->setTld($this->RDAPService->getTld($idnDomain))
|
||||
->setDelegationSigned(false)
|
||||
->setDeleted(true);
|
||||
|
||||
$this->entityManager->persist($domain);
|
||||
$this->entityManager->flush();
|
||||
$this->em->persist($domain);
|
||||
$this->em->flush();
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
$randomizer = new Randomizer();
|
||||
$watchlists = $randomizer->shuffleArray($domain->getWatchlists()->toArray());
|
||||
|
||||
/** @var Watchlist $watchlist */
|
||||
foreach ($watchlists as $watchlist) {
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchlist->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
}
|
||||
|
||||
return $domain;
|
||||
|
||||
@@ -7,8 +7,8 @@ use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Config\ConnectorProvider;
|
||||
use App\Entity\Connector;
|
||||
use App\Entity\User;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use App\Service\Connector\EppClientProvider;
|
||||
use App\Service\Provider\AbstractProvider;
|
||||
use App\Service\Provider\EppClientProvider;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
@@ -47,7 +47,7 @@ readonly class ConnectorCreateProcessor implements ProcessorInterface
|
||||
|
||||
$provider = $data->getProvider();
|
||||
|
||||
$this->logger->info('User {username} wants to register a connector from provider {provider}.', [
|
||||
$this->logger->info('User wants to register a connector', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'provider' => $provider->value,
|
||||
]);
|
||||
@@ -87,7 +87,7 @@ readonly class ConnectorCreateProcessor implements ProcessorInterface
|
||||
$data->setAuthData($providerClient->authenticate($authData));
|
||||
}
|
||||
|
||||
$this->logger->info('User {username} authentication data with the {provider} provider has been validated.', [
|
||||
$this->logger->info('User authentication data with this provider has been validated', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'provider' => $provider->value,
|
||||
]);
|
||||
|
||||
@@ -6,7 +6,7 @@ use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Config\ConnectorProvider;
|
||||
use App\Entity\Connector;
|
||||
use App\Service\Connector\EppClientProvider;
|
||||
use App\Service\Provider\EppClientProvider;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
@@ -30,7 +30,7 @@ readonly class ConnectorDeleteProcessor implements ProcessorInterface
|
||||
*/
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||
{
|
||||
foreach ($data->getWatchLists()->getIterator() as $watchlist) {
|
||||
foreach ($data->getWatchlists()->getIterator() as $watchlist) {
|
||||
$watchlist->setConnector(null);
|
||||
}
|
||||
|
||||
|
||||
81
src/State/FindDomainCollectionFromEntityProvider.php
Normal file
81
src/State/FindDomainCollectionFromEntityProvider.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Entity\Domain;
|
||||
use App\Service\RDAPService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
readonly class FindDomainCollectionFromEntityProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private RequestStack $requestStack,
|
||||
private EntityManagerInterface $em,
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
||||
{
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
$rsm = (new ResultSetMapping())
|
||||
->addScalarResult('domain_ids', 'domain_ids');
|
||||
|
||||
$handleBlacklist = join(',', array_map(fn (string $s) => "'$s'", RDAPService::ENTITY_HANDLE_BLACKLIST));
|
||||
|
||||
$sql = <<<SQL
|
||||
SELECT
|
||||
array_agg(DISTINCT de.domain_id) AS domain_ids
|
||||
FROM (
|
||||
SELECT
|
||||
e.handle AS handle,
|
||||
e.id,
|
||||
e.tld_id,
|
||||
jsonb_path_query_first(
|
||||
e.j_card,
|
||||
'$[1] ? (@[0] == "fn")[3]'
|
||||
) #>> '{}' AS fn,
|
||||
jsonb_path_query_first(
|
||||
e.j_card,
|
||||
'$[1] ? (@[0] == "org")[3]'
|
||||
) #>> '{}' AS org
|
||||
FROM entity e
|
||||
) sub
|
||||
JOIN domain_entity de ON de.entity_uid = sub.id
|
||||
WHERE LOWER(org||fn) NOT LIKE '%redacted%'
|
||||
AND LOWER(org||fn) NOT LIKE '%privacy%'
|
||||
AND LOWER(org||fn) NOT LIKE '%registration private%'
|
||||
AND LOWER(org||fn) NOT LIKE '%domain administrator%'
|
||||
AND LOWER(org||fn) NOT LIKE '%registry super user account%'
|
||||
AND LOWER(org||fn) NOT LIKE '%ano nymous%'
|
||||
AND LOWER(org||fn) NOT LIKE '%by proxy%'
|
||||
AND handle NOT IN ($handleBlacklist)
|
||||
AND de.roles @> '["registrant"]'
|
||||
AND sub.tld_id IS NOT NULL
|
||||
AND (LOWER(org) = LOWER(:registrant) OR LOWER(fn) = LOWER(:registrant));
|
||||
SQL;
|
||||
$result = $this->em->createNativeQuery($sql, $rsm)
|
||||
->setParameter('registrant', trim($request->get('registrant')))
|
||||
->getOneOrNullResult();
|
||||
|
||||
if (!$result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$domainList = array_filter(explode(',', trim($result['domain_ids'], '{}')));
|
||||
|
||||
if (empty($domainList)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->em->getRepository(Domain::class)
|
||||
->createQueryBuilder('d')
|
||||
->where('d.ldhName IN (:list)')
|
||||
->setParameter('list', $domainList)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@ use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\Watchlist;
|
||||
use App\Notifier\TestChatNotification;
|
||||
use App\Service\ChatNotificationService;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use App\Service\Provider\AbstractProvider;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
@@ -19,7 +19,7 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Serializer\Exception\ExceptionInterface;
|
||||
|
||||
readonly class WatchListUpdateProcessor implements ProcessorInterface
|
||||
readonly class WatchlistUpdateProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
@@ -34,9 +34,9 @@ readonly class WatchListUpdateProcessor implements ProcessorInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WatchList $data
|
||||
* @param Watchlist $data
|
||||
*
|
||||
* @return WatchList
|
||||
* @return Watchlist
|
||||
*
|
||||
* @throws ExceptionInterface
|
||||
* @throws \Exception
|
||||
@@ -45,30 +45,32 @@ readonly class WatchListUpdateProcessor implements ProcessorInterface
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->security->getUser();
|
||||
$data->setUser($user);
|
||||
$data->setUser($user)->setToken($uriVariables['token'] ?? $data->getToken());
|
||||
|
||||
if ($this->parameterBag->get('limited_features')) {
|
||||
if ($data->getDomains()->count() > (int) $this->parameterBag->get('limit_max_watchlist_domains')) {
|
||||
$this->logger->notice('User {username} tried to update a Watchlist. The maximum number of domains has been reached for this Watchlist', [
|
||||
$this->logger->notice('User tried to update a Watchlist : the maximum number of domains has been reached for this Watchlist', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'watchlist' => $data->getToken(),
|
||||
]);
|
||||
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of domain names allowed in this Watchlist');
|
||||
}
|
||||
|
||||
$userWatchLists = $user->getWatchLists();
|
||||
$userWatchlists = $user->getWatchlists();
|
||||
|
||||
/** @var Domain[] $trackedDomains */
|
||||
$trackedDomains = $userWatchLists
|
||||
->filter(fn (WatchList $wl) => $wl->getToken() !== $data->getToken())
|
||||
->reduce(fn (array $acc, WatchList $wl) => [...$acc, ...$wl->getDomains()->toArray()], []);
|
||||
$trackedDomains = $userWatchlists
|
||||
->filter(fn (Watchlist $wl) => $wl->getToken() !== $data->getToken())
|
||||
->reduce(fn (array $acc, Watchlist $wl) => [...$acc, ...$wl->getDomains()->toArray()], []);
|
||||
|
||||
/** @var Domain $domain */
|
||||
foreach ($data->getDomains()->getIterator() as $domain) {
|
||||
if (in_array($domain, $trackedDomains)) {
|
||||
$ldhName = $domain->getLdhName();
|
||||
$this->logger->notice('User {username} tried to update a watchlist with domain name {ldhName}. It is forbidden to register the same domain name twice with limited mode', [
|
||||
$this->logger->notice('User tried to update a Watchlist : it is forbidden to register the same domain name twice with limited mode', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'watchlist' => $data->getToken(),
|
||||
'ldhName' => $ldhName,
|
||||
]);
|
||||
|
||||
@@ -77,8 +79,9 @@ readonly class WatchListUpdateProcessor implements ProcessorInterface
|
||||
}
|
||||
|
||||
if (null !== $data->getWebhookDsn() && count($data->getWebhookDsn()) > (int) $this->parameterBag->get('limit_max_watchlist_webhooks')) {
|
||||
$this->logger->notice('User {username} tried to update a Watchlist. The maximum number of webhooks has been reached.', [
|
||||
$this->logger->notice('User tried to update a Watchlist : the maximum number of webhooks has been reached', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'watchlist' => $data->getToken(),
|
||||
]);
|
||||
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of webhooks allowed in this Watchlist');
|
||||
@@ -89,7 +92,7 @@ readonly class WatchListUpdateProcessor implements ProcessorInterface
|
||||
|
||||
if ($connector = $data->getConnector()) {
|
||||
if (!$user->getConnectors()->contains($connector)) {
|
||||
$this->logger->notice('The Connector ({connector}) does not belong to the user.', [
|
||||
$this->logger->notice('Connector does not belong to the user', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'connector' => $connector->getId(),
|
||||
]);
|
||||
@@ -114,9 +117,10 @@ readonly class WatchListUpdateProcessor implements ProcessorInterface
|
||||
$supported = $connectorProvider->isSupported(...$data->getDomains()->toArray());
|
||||
|
||||
if (!$supported) {
|
||||
$this->logger->notice('The Connector ({connector}) does not support all TLDs in this Watchlist', [
|
||||
$this->logger->debug('Connector does not support all TLDs in this Watchlist', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'connector' => $connector->getId(),
|
||||
'provider' => $connector->getProvider()->value,
|
||||
]);
|
||||
|
||||
throw new BadRequestHttpException('This connector does not support all TLDs in this Watchlist');
|
||||
14
src/Story/DefaultUsersStory.php
Normal file
14
src/Story/DefaultUsersStory.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Story;
|
||||
|
||||
use App\Factory\UserFactory;
|
||||
use Zenstruck\Foundry\Story;
|
||||
|
||||
final class DefaultUsersStory extends Story
|
||||
{
|
||||
public function build(): void
|
||||
{
|
||||
UserFactory::createMany(3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user