Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php namespace models\summit;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

/**
* Class AllowedEmailDomainsLookup
* @package models\summit
*
* Immutable per-request DTO used by discovery / matching paths to avoid
* recomputing strtolower(trim(...)) for every allowed-email-domain pattern
* on every candidate code. Partitions normalized patterns into:
*
* - $exactSet: O(1) lookup map for "@domain.tld" and "user@domain.tld" patterns
* - $suffixList: array of ".tld"-style suffixes for endsWith / str_ends_with checks
* - $patternsHash: stable sha1 over the sorted normalized pattern set; identifies
* the pattern set for change detection / equality comparisons.
* - $unrestricted: true iff the input pattern array was empty. Distinguishes
* "no patterns configured" (legacy "no restriction") from
* "patterns configured but all malformed" (which the legacy
* matcher treats as no match — see
* DomainAuthorizedPromoCodeTrait::matchesEmailDomain parity contract).
*/
final class AllowedEmailDomainsLookup
{
public function __construct(
public readonly array $exactSet,
public readonly array $suffixList,
public readonly string $patternsHash,
public readonly bool $unrestricted
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,41 @@ public function matchesEmailDomain(string $email): bool
return false;
}

/**
* Lookup-driven sibling of matchesEmailDomain().
* Consumes a precomputed AllowedEmailDomainsLookup so the caller does not
* pay strtolower/trim per pattern when matching many codes against the
* same pattern set. Must return parity with matchesEmailDomain().
*
* @param string $email
* @param AllowedEmailDomainsLookup $lookup
* @return bool
*/
public function matchesEmailDomainViaLookup(string $email, AllowedEmailDomainsLookup $lookup): bool
{
// Parity with legacy matchesEmailDomain(): only return "match anything"
// when the stored pattern array was actually empty. A non-empty array
// whose patterns all dropped as malformed must still return false here.
if ($lookup->unrestricted) return true;

$email = strtolower(trim($email));
if ($email === '') return false;

$atPos = strpos($email, '@');
if ($atPos === false) return false;

$emailDomain = substr($email, $atPos);

if (isset($lookup->exactSet[$emailDomain])) return true;
if (isset($lookup->exactSet[$email])) return true;

foreach ($lookup->suffixList as $suffix) {
if (str_ends_with($emailDomain, $suffix)) return true;
}

return false;
}

/**
* Validates email against allowed_email_domains.
* Throws ValidationException if no match.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ public function getQuantityPerAccount(): int;
*/
public function matchesEmailDomain(string $email): bool;

/**
* Lookup-driven sibling of matchesEmailDomain().
*
* Consumes a precomputed AllowedEmailDomainsLookup (normalized + partitioned
* exactSet / suffixList) so callers iterating many candidate codes against
* the same pattern set avoid re-normalizing patterns per code. Must return
* the SAME boolean as matchesEmailDomain() for any (patterns, email) pair.
*
* @param string $email
* @param AllowedEmailDomainsLookup $lookup
* @return bool
*/
public function matchesEmailDomainViaLookup(string $email, AllowedEmailDomainsLookup $lookup): bool;

/**
* @param int|null $remaining
* @return void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,14 @@ public function getBySummitAndCode(Summit $summit, string $code):?SummitRegistra
public function getDiscoverableByEmailForSummit(Summit $summit, string $email): array;

/**
* Returns date-windowed DOMAIN_AUTHORIZED_* candidates for the summit.
* Filtering by email moved to SummitPromoCodeService::discoverPromoCodes
* under option (b) of the Track-1 repository-decouple (SDS Task T1-Service).
*
* @param Summit $summit
* @param string $email
* @return SummitRegistrationPromoCode[]
*/
public function getDomainAuthorizedDiscoverableForSummit(Summit $summit, string $email): array;
public function getDomainAuthorizedDiscoverableForSummit(Summit $summit): array;

/**
* @param Summit $summit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,32 +661,43 @@ public function getBySummitAndCode(Summit $summit, string $code):?SummitRegistra
* Returns domain-authorized types (matched by email domain) and
* existing email-linked types (member/speaker, matched by associated email).
*
* @deprecated Track 1 (SDS task T1-Service) moved the discover hot path to
* SummitPromoCodeService::discoverPromoCodes calling the two leaf methods
* directly. This aggregator is retained only to preserve its public
* by-email contract for any non-grepped caller. Remove once Track 2 ships
* and a `grep -rn getDiscoverableByEmailForSummit` confirms zero callers.
*
* @param Summit $summit
* @param string $email
* @return SummitRegistrationPromoCode[]
*/
public function getDiscoverableByEmailForSummit(Summit $summit, string $email): array
{
if (empty($email)) return [];
$normalized = strtolower(trim($email));
if ($normalized === '') return [];

$email = strtolower(trim($email));
$daCandidates = $this->getDomainAuthorizedDiscoverableForSummit($summit);

return array_merge(
$this->getDomainAuthorizedDiscoverableForSummit($summit, $email),
$this->getEmailLinkedDiscoverableForSummit($summit, $email)
);
$daMatched = [];
foreach ($daCandidates as $code) {
if ($code instanceof IDomainAuthorizedPromoCode && $code->matchesEmailDomain($normalized)) {
$daMatched[] = $code;
}
}

$emailLinked = $this->getEmailLinkedDiscoverableForSummit($summit, $normalized);
return array_merge($daMatched, $emailLinked);
}

/**
* @param Summit $summit
* @param string $email
* @return SummitRegistrationPromoCode[]
*/
public function getDomainAuthorizedDiscoverableForSummit(Summit $summit, string $email): array
public function getDomainAuthorizedDiscoverableForSummit(Summit $summit): array
{
$em = $this->getEntityManager();
$em = $this->getEntityManager();
$now = new \DateTime('now', new \DateTimeZone('UTC'));
$daPromoClass = DomainAuthorizedSummitRegistrationPromoCode::class;
$daPromoClass = DomainAuthorizedSummitRegistrationPromoCode::class;
$daDiscountClass = DomainAuthorizedSummitRegistrationDiscountCode::class;

$qb = $em->createQueryBuilder();
Expand All @@ -702,16 +713,7 @@ public function getDomainAuthorizedDiscoverableForSummit(Summit $summit, string
->setParameter('summit_id', $summit->getId())
->setParameter('now', $now);

$candidates = $qb->getQuery()->getResult();
$results = [];

foreach ($candidates as $code) {
if ($code instanceof IDomainAuthorizedPromoCode && $code->matchesEmailDomain($email)) {
$results[] = $code;
}
}

return $results;
return $qb->getQuery()->getResult();
}

/**
Expand Down
87 changes: 87 additions & 0 deletions app/Services/Model/Imp/AllowedEmailDomainsLookupBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php namespace App\Services\Model;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use models\summit\AllowedEmailDomainsLookup;

/**
* Class AllowedEmailDomainsLookupBuilder
* @package App\Services\Model
*
* Pure builder: turns a raw list of allowed-email-domain patterns into an
* immutable AllowedEmailDomainsLookup DTO. No DB, no cache, no Doctrine.
*
* Normalization rules:
* - strtolower(trim((string) $raw)) on every pattern
* - drop empty values
* - case-insensitive dedup (first occurrence wins)
*
* Partition rules:
* - leading '@' -> exactSet[$p] = true (e.g. "@acme.com")
* - leading '.' -> suffixList[] = $p (e.g. ".edu")
* - contains '@' (not at start) -> exactSet[$p] = true (e.g. "user@acme.com")
* - otherwise -> dropped silently
*
* patternsHash = sha1(implode('|', $sortedNormalizedPatterns)) — stable
* regardless of input order, so callers can use it for change detection / equality.
*/
final class AllowedEmailDomainsLookupBuilder
{
public function build(array $patterns): AllowedEmailDomainsLookup
{
// Capture "no patterns configured" from the RAW input BEFORE any
// normalization or partitioning. Required for legacy parity: a
// non-empty input whose patterns all drop as malformed must NOT
// be treated as unrestricted.
$unrestricted = count($patterns) === 0;

$exactSet = [];
$suffixList = [];
$seen = [];
$normalized = [];

foreach ($patterns as $raw) {
$p = strtolower(trim((string) $raw));
if ($p === '') {
continue;
}
if (isset($seen[$p])) {
continue;
}
$seen[$p] = true;

$first = $p[0];
if ($first === '@') {
$exactSet[$p] = true;
$normalized[] = $p;
continue;
}
if ($first === '.') {
$suffixList[] = $p;
$normalized[] = $p;
continue;
}
if (strpos($p, '@') !== false) {
$exactSet[$p] = true;
$normalized[] = $p;
continue;
}
// otherwise: dropped silently
}

sort($normalized);
$patternsHash = sha1(implode('|', $normalized));

return new AllowedEmailDomainsLookup($exactSet, $suffixList, $patternsHash, $unrestricted);
}
}
Loading
Loading