refactor: search domain by registrant name

This commit is contained in:
Maël Gangloff 2025-11-08 20:02:37 +01:00
parent d769c48955
commit 8b03c54a16
No known key found for this signature in database
GPG Key ID: 11FDC81C24A7F629
6 changed files with 194 additions and 55 deletions

View File

@ -42,6 +42,7 @@
"protonlabs/vobject": "^4.31", "protonlabs/vobject": "^4.31",
"psr/http-client": "^1.0", "psr/http-client": "^1.0",
"runtime/frankenphp-symfony": "^0.2.0", "runtime/frankenphp-symfony": "^0.2.0",
"scienta/doctrine-json-functions": "^6.3",
"symfony/asset": "7.3.*", "symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*", "symfony/asset-mapper": "7.3.*",
"symfony/cache": "7.3.*", "symfony/cache": "7.3.*",

76
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "49fd7fad6776160ff572f3fb9201a8a2", "content-hash": "1db291e0d108c6bb06d447134f1250c6",
"packages": [ "packages": [
{ {
"name": "api-platform/core", "name": "api-platform/core",
@ -4189,6 +4189,78 @@
}, },
"time": "2024-09-06T08:00:55+00:00" "time": "2024-09-06T08:00:55+00:00"
}, },
{
"name": "scienta/doctrine-json-functions",
"version": "6.3.0",
"source": {
"type": "git",
"url": "https://github.com/ScientaNL/DoctrineJsonFunctions.git",
"reference": "554b2fd281e976a791501fc4753ffd4c5891ec62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ScientaNL/DoctrineJsonFunctions/zipball/554b2fd281e976a791501fc4753ffd4c5891ec62",
"reference": "554b2fd281e976a791501fc4753ffd4c5891ec62",
"shasum": ""
},
"require": {
"doctrine/dbal": "^3.2 || ^4",
"doctrine/lexer": "^2.0 || ^3.0",
"doctrine/orm": "^2.19 || ^3",
"ext-pdo": "*",
"php": "^8.1"
},
"require-dev": {
"doctrine/coding-standard": "^9.0 || ^10.0 || ^11.0 || ^12.0",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^1.12",
"phpstan/phpstan-doctrine": "^1.4",
"phpstan/phpstan-phpunit": "^1.4",
"phpunit/phpunit": "^10.1",
"psalm/plugin-phpunit": "^0.18",
"slevomat/coding-standard": "~8",
"symfony/cache": "^5.4 || ^6.4 || ^7",
"vimeo/psalm": "^5.2",
"webmozart/assert": "^1.11"
},
"suggest": {
"dunglas/doctrine-json-odm": "To serialize / deserialize objects as JSON documents."
},
"type": "library",
"autoload": {
"psr-4": {
"Scienta\\DoctrineJsonFunctions\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Doctrine Json Functions Contributors",
"homepage": "https://github.com/ScientaNL/DoctrineJsonFunctions/contributors"
}
],
"description": "A set of extensions to Doctrine that add support for json query functions.",
"keywords": [
"database",
"doctrine",
"dql",
"json",
"mariadb",
"mysql",
"orm",
"postgres",
"postgresql",
"sqlite"
],
"support": {
"issues": "https://github.com/ScientaNL/DoctrineJsonFunctions/issues",
"source": "https://github.com/ScientaNL/DoctrineJsonFunctions/tree/6.3.0"
},
"time": "2024-11-08T12:33:19+00:00"
},
{ {
"name": "symfony/asset", "name": "symfony/asset",
"version": "v7.3.0", "version": "v7.3.0",
@ -15319,7 +15391,7 @@
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": ">=8.2", "php": ">=8.4",
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-simplexml": "*" "ext-simplexml": "*"

View File

@ -24,6 +24,9 @@ doctrine:
alias: App alias: App
controller_resolver: controller_resolver:
auto_mapping: false auto_mapping: false
dql:
string_functions:
JSONB_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Postgresql\JsonbContains
when@test: when@test:
doctrine: doctrine:

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251108171723 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add j_card_fn and j_card_org';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE entity ADD j_card_fn VARCHAR(255) GENERATED ALWAYS AS (UPPER(jsonb_path_query_first(j_card, \'$[1]?(@[0] == "fn")[3]\') #>> \'{}\')) STORED');
$this->addSql('ALTER TABLE entity ADD j_card_org VARCHAR(255) GENERATED ALWAYS AS (UPPER(jsonb_path_query_first(j_card, \'$[1]?(@[0] == "org")[3]\') #>> \'{}\')) STORED');
$this->addSql('CREATE INDEX entity_j_card_fn_idx ON entity (j_card_fn)');
$this->addSql('CREATE INDEX entity_j_card_org_idx ON entity (j_card_org)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX entity_j_card_fn_idx');
$this->addSql('DROP INDEX entity_j_card_org_idx');
$this->addSql('ALTER TABLE entity DROP j_card_fn');
$this->addSql('ALTER TABLE entity DROP j_card_org');
}
}

View File

@ -14,6 +14,8 @@ use Symfony\Component\Serializer\Attribute\SerializedName;
#[ORM\UniqueConstraint( #[ORM\UniqueConstraint(
columns: ['tld_id', 'handle'] columns: ['tld_id', 'handle']
)] )]
#[ORM\Index(name: 'entity_j_card_fn_idx', columns: ['j_card_fn'])]
#[ORM\Index(name: 'entity_j_card_org_idx', columns: ['j_card_org'])]
class Entity class Entity
{ {
#[ORM\Id] #[ORM\Id]
@ -63,6 +65,24 @@ class Entity
#[Groups(['entity:item', 'domain:item', 'watchlist:item'])] #[Groups(['entity:item', 'domain:item', 'watchlist:item'])]
private array $jCard = []; private array $jCard = [];
#[ORM\Column(
type: 'string',
insertable: false,
updatable: false,
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (LOWER(jsonb_path_query_first(j_card, '$[1]?(@[0] == \"fn\")[3]') #>> '{}')) STORED",
generated: 'ALWAYS',
)]
private ?string $jCardFn;
#[ORM\Column(
type: 'string',
insertable: false,
updatable: false,
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (LOWER(jsonb_path_query_first(j_card, '$[1]?(@[0] == \"org\")[3]') #>> '{}')) STORED",
generated: 'ALWAYS',
)]
private ?string $jCardOrg;
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
#[Groups(['entity:item', 'domain:item', 'watchlist:item'])] #[Groups(['entity:item', 'domain:item', 'watchlist:item'])]
private ?array $remarks = null; private ?array $remarks = null;
@ -239,4 +259,28 @@ class Entity
return $this; return $this;
} }
public function getJCardFn(): ?string
{
return $this->jCardFn;
}
public function getJCardOrg(): ?string
{
return $this->jCardOrg;
}
public function setJCardFn(?string $jCardFn): Entity
{
$this->jCardFn = $jCardFn;
return $this;
}
public function setJCardOrg(?string $jCardOrg): Entity
{
$this->jCardOrg = $jCardOrg;
return $this;
}
} }

View File

@ -4,79 +4,63 @@ namespace App\State;
use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\ProviderInterface;
use App\Entity\Domain; use App\Repository\DomainRepository;
use App\Repository\EntityRepository;
use App\Service\RDAPService; use App\Service\RDAPService;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMapping;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\HttpException;
readonly class FindDomainCollectionFromEntityProvider implements ProviderInterface readonly class FindDomainCollectionFromEntityProvider implements ProviderInterface
{ {
public function __construct( public function __construct(
private RequestStack $requestStack, private RequestStack $requestStack,
private EntityManagerInterface $em, private EntityRepository $entityRepository,
private DomainRepository $domainRepository,
) { ) {
} }
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{ {
$request = $this->requestStack->getCurrentRequest(); $request = $this->requestStack->getCurrentRequest();
$rsm = (new ResultSetMapping()) $registrant = trim((string) $request->get('registrant'));
->addScalarResult('domain_ids', 'domain_ids');
$handleBlacklist = join(',', array_map(fn (string $s) => "'$s'", RDAPService::ENTITY_HANDLE_BLACKLIST)); $forbidden = [
'redacted',
'privacy',
'registration private',
'domain administrator',
'registry super user account',
'ano nymous',
'by proxy',
];
$sql = <<<SQL foreach ($forbidden as $word) {
SELECT if (str_contains(strtolower($registrant), $word)) {
array_agg(DISTINCT de.domain_id) AS domain_ids throw new HttpException(403, 'Forbidden search term');
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 for privacy%'
AND LOWER(org||fn) NOT LIKE '%data protected%'
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 '%redacted%'
AND LOWER(org||fn) NOT IN ('na', 'n/a', '-', 'none', 'not applicable')
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'], '{}'))); $entities = $this->entityRepository->createQueryBuilder('e')
->where('e.tld IS NOT NULL')
->andWhere('e.handle NOT IN (:blacklist)')
->andWhere('UPPER(e.jCardOrg) = UPPER(:registrant) OR UPPER(e.jCardFn) = UPPER(:registrant)')
->setParameter('registrant', $registrant)
->setParameter('blacklist', RDAPService::ENTITY_HANDLE_BLACKLIST)
->getQuery()
->getResult();
if (empty($domainList)) { if (empty($entities)) {
return []; return [];
} }
return $this->em->getRepository(Domain::class) return $this->domainRepository->createQueryBuilder('d')
->createQueryBuilder('d') ->select('DISTINCT d')
->where('d.ldhName IN (:list)') ->join('d.domainEntities', 'de')
->setParameter('list', $domainList) ->where('de.entity IN (:entityIds)')
->getQuery() ->andWhere('JSONB_CONTAINS(de.roles, :role) = true')
->getResult(); ->andWhere('de.deletedAt IS NULL')
->setParameter('entityIds', array_map(fn ($e) => $e->getId(), $entities))
->setParameter('role', '"registrant"')
->getQuery()->getResult();
} }
} }