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
28 changes: 24 additions & 4 deletions src/Condition.php
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,14 @@ private function matchesLogical(array $attributes): bool
private function matchesEqual(mixed $value): bool
{
foreach ($this->values as $expected) {
if (\is_string($expected) && \is_string($value)) {
if (\strtolower($expected) === \strtolower($value)) {
return true;
}

continue;
}

if ($expected === $value) {
return true;
}
Expand All @@ -435,12 +443,24 @@ private function matchesEqual(mixed $value): bool
private function matchesContains(mixed $value, array $needles): bool
{
if (\is_array($value)) {
return \count(array_intersect($value, $needles)) > 0;
// Mirror the old array_intersect() semantics (scalars compared as
// strings, so 200 matches '200') while folding case.
foreach ($value as $item) {
foreach ($needles as $needle) {
if (\is_scalar($item) && \is_scalar($needle)
&& \strtolower((string) $item) === \strtolower((string) $needle)) {
return true;
}
}
}

return false;
}

if (\is_string($value)) {
$haystack = \strtolower($value);
foreach ($needles as $needle) {
if (\is_string($needle) && $needle !== '' && str_contains($value, $needle)) {
if (\is_string($needle) && $needle !== '' && str_contains($haystack, \strtolower($needle))) {
return true;
}
}
Expand Down Expand Up @@ -481,7 +501,7 @@ private function matchesPrefix(mixed $value): bool
return false;
}

return str_starts_with($value, $prefix);
return str_starts_with(\strtolower($value), \strtolower($prefix));
}

private function matchesSuffix(mixed $value): bool
Expand All @@ -492,7 +512,7 @@ private function matchesSuffix(mixed $value): bool
return false;
}

return str_ends_with($value, $suffix);
return str_ends_with(\strtolower($value), \strtolower($suffix));
}

/**
Expand Down
44 changes: 44 additions & 0 deletions tests/ConditionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,50 @@ public function testConditionSerializationRoundTrip(): void
$this->assertFalse($parsed->matches(['ip' => '127.0.0.1', 'path' => '/web']));
}

public function testMatchingIsCaseInsensitive(): void
{
// equal: rule value and resolved attribute differ only in case.
$country = Condition::equal('country', ['in']);
$this->assertTrue($country->matches(['country' => 'IN']));
$this->assertTrue($country->matches(['country' => 'In']));
$this->assertFalse($country->matches(['country' => 'US']));

// notEqual inherits the same folding.
$notCountry = Condition::notEqual('country', 'in');
$this->assertFalse($notCountry->matches(['country' => 'IN']));
$this->assertTrue($notCountry->matches(['country' => 'US']));

// contains: both the string-haystack and array-membership forms fold case.
$stringContains = Condition::contains('userAgent', ['CURL']);
$this->assertTrue($stringContains->matches(['userAgent' => 'curl/8.4']));

$arrayContains = Condition::contains('tags', ['Security']);
$this->assertTrue($arrayContains->matches(['tags' => ['security', 'waf']]));

// Array membership keeps the old array_intersect() loose scalar
// semantics: an int needle still matches a numeric-string element.
$numericContains = Condition::contains('codes', [200]);
$this->assertTrue($numericContains->matches(['codes' => ['200', '404']]));

// prefix / suffix fold case too.
$startsWith = Condition::startsWith('path', '/API');
$this->assertTrue($startsWith->matches(['path' => '/api/v1']));

$endsWith = Condition::endsWith('path', '.JSON');
$this->assertTrue($endsWith->matches(['path' => '/status.json']));

// Non-string values keep strict semantics (no accidental coercion).
$numeric = Condition::equal('count', [10]);
$this->assertTrue($numeric->matches(['count' => 10]));
$this->assertFalse($numeric->matches(['count' => '10']));

// Ordering operators stay case-sensitive so string boundaries remain
// deterministic — only equality/substring matching folds case.
$between = Condition::between('name', 'BANANA', 'CHERRY');
$this->assertFalse($between->matches(['name' => 'banana']));
$this->assertTrue($between->matches(['name' => 'CANARY']));
}

public function testInvalidMethodThrowsException(): void
{
$this->expectException(ConditionException::class);
Expand Down
Loading