Skip to content

Commit fd43645

Browse files
Merge remote-tracking branch '39502/39382-media-gallery-case-insensitive-bug' into commpr-21755-0511
2 parents fdae064 + 4737be6 commit fd43645

File tree

3 files changed

+422
-1
lines changed

3 files changed

+422
-1
lines changed

app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,50 @@
1010
use Magento\Framework\Api\Filter;
1111
use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface;
1212
use Magento\Framework\Data\Collection\AbstractDb;
13+
use Psr\Log\LoggerInterface;
14+
use Magento\Framework\App\ObjectManager;
1315

1416
class Directory implements CustomFilterInterface
1517
{
18+
/**
19+
* @var LoggerInterface
20+
*/
21+
private $logger;
22+
23+
/**
24+
* @param LoggerInterface|null $logger
25+
*/
26+
public function __construct(?LoggerInterface $logger = null)
27+
{
28+
$this->logger = $logger ?: ObjectManager::getInstance()->create(LoggerInterface::class);
29+
}
30+
1631
/**
1732
* @inheritDoc
1833
*/
1934
public function apply(Filter $filter, AbstractDb $collection): bool
2035
{
2136
$value = $filter->getValue() !== null ? str_replace('%', '', $filter->getValue()) : '';
22-
$collection->getSelect()->where('path REGEXP ? ', '^' . $value . '/[^\/]*$');
37+
38+
try {
39+
/**
40+
* Use BINARY comparison for case-sensitive path filtering.
41+
* Without BINARY, MySQL's default case-insensitive comparison would match
42+
* directories like "Testing" and "testing" as the same, leading to incorrect
43+
* file visibility across directories with different case variations.
44+
* The regex '^{path}/[^\/]*$' ensures we only match files directly in the
45+
* specified directory, not in subdirectories.
46+
*/
47+
$collection->getSelect()->where('BINARY path REGEXP ? ', '^' . $value . '/[^\/]*$');
48+
} catch (\Exception $e) {
49+
// Log the error for debugging but continue with case-insensitive fallback
50+
// Note: This fallback means directory filtering will not be case-sensitive
51+
$this->logger->error(
52+
'MediaGallery Directory Filter: BINARY REGEXP not supported, ' .
53+
'using case-insensitive fallback: ' . $e->getMessage()
54+
);
55+
$collection->getSelect()->where('path REGEXP ? ', '^' . $value . '/[^\/]*$');
56+
}
2357

2458
return true;
2559
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\MediaGalleryUi\Test\Integration\Model\SearchCriteria\CollectionProcessor\FilterProcessor;
9+
10+
use Magento\Framework\Api\Filter;
11+
use Magento\Framework\Data\Collection\AbstractDb;
12+
use Magento\Framework\DB\Select;
13+
use Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Directory;
14+
use Magento\TestFramework\Helper\Bootstrap;
15+
use PHPUnit\Framework\TestCase;
16+
17+
/**
18+
* Integration test for Directory filter processor with case-sensitive path filtering
19+
*
20+
* @magentoDbIsolation enabled
21+
*/
22+
class DirectoryIntegrationTest extends TestCase
23+
{
24+
/**
25+
* @var Directory
26+
*/
27+
private $directoryFilterProcessor;
28+
29+
/**
30+
* Set up test environment
31+
*/
32+
protected function setUp(): void
33+
{
34+
$objectManager = Bootstrap::getObjectManager();
35+
$this->directoryFilterProcessor = $objectManager->create(Directory::class);
36+
}
37+
38+
/**
39+
* Test the FilterProcessor applies BINARY case-sensitive SQL query
40+
* This is the core test that verifies the BINARY keyword fix
41+
*/
42+
public function testFilterProcessorAppliesBinaryCaseSensitiveQuery(): void
43+
{
44+
// Create mock collection and select objects
45+
$mockCollection = $this->createMock(AbstractDb::class);
46+
$mockSelect = $this->createMock(Select::class);
47+
48+
$mockCollection->expects($this->once())
49+
->method('getSelect')
50+
->willReturn($mockSelect);
51+
52+
// Verify that the BINARY keyword is used in the WHERE clause
53+
// This is the exact fix we implemented to make directory filtering case-sensitive
54+
$mockSelect->expects($this->once())
55+
->method('where')
56+
->with(
57+
$this->equalTo('BINARY path REGEXP ? '),
58+
$this->equalTo('^testing/[^\/]*$')
59+
);
60+
61+
// Create filter for lowercase 'testing' directory
62+
$filter = Bootstrap::getObjectManager()->create(Filter::class);
63+
$filter->setField('directory');
64+
$filter->setValue('testing');
65+
66+
// Apply the filter - this should call the BINARY SQL query
67+
$result = $this->directoryFilterProcessor->apply($filter, $mockCollection);
68+
$this->assertTrue($result);
69+
}
70+
71+
/**
72+
* Test case-sensitive behavior with uppercase directory
73+
*/
74+
public function testFilterProcessorWithUppercaseDirectory(): void
75+
{
76+
$mockCollection = $this->createMock(AbstractDb::class);
77+
$mockSelect = $this->createMock(Select::class);
78+
79+
$mockCollection->expects($this->once())
80+
->method('getSelect')
81+
->willReturn($mockSelect);
82+
83+
// Verify uppercase directory generates correct case-sensitive query
84+
$mockSelect->expects($this->once())
85+
->method('where')
86+
->with(
87+
$this->equalTo('BINARY path REGEXP ? '),
88+
$this->equalTo('^Testing/[^\/]*$')
89+
);
90+
91+
$filter = Bootstrap::getObjectManager()->create(Filter::class);
92+
$filter->setField('directory');
93+
$filter->setValue('Testing');
94+
95+
$result = $this->directoryFilterProcessor->apply($filter, $mockCollection);
96+
$this->assertTrue($result);
97+
}
98+
99+
/**
100+
* Test that percentage signs are properly stripped from filter value
101+
*/
102+
public function testFilterProcessorStripsPercentageSigns(): void
103+
{
104+
$mockCollection = $this->createMock(AbstractDb::class);
105+
$mockSelect = $this->createMock(Select::class);
106+
107+
$mockCollection->expects($this->once())
108+
->method('getSelect')
109+
->willReturn($mockSelect);
110+
111+
// Verify percentage signs are stripped from the regex pattern
112+
$mockSelect->expects($this->once())
113+
->method('where')
114+
->with(
115+
$this->equalTo('BINARY path REGEXP ? '),
116+
$this->equalTo('^TestingDirectory/[^\/]*$')
117+
);
118+
119+
$filter = Bootstrap::getObjectManager()->create(Filter::class);
120+
$filter->setField('directory');
121+
$filter->setValue('Testing%Directory%');
122+
123+
$result = $this->directoryFilterProcessor->apply($filter, $mockCollection);
124+
$this->assertTrue($result);
125+
}
126+
127+
/**
128+
* Test with null filter value
129+
*/
130+
public function testFilterProcessorWithNullValue(): void
131+
{
132+
$mockCollection = $this->createMock(AbstractDb::class);
133+
$mockSelect = $this->createMock(Select::class);
134+
135+
$mockCollection->expects($this->once())
136+
->method('getSelect')
137+
->willReturn($mockSelect);
138+
139+
// Verify null value creates empty directory pattern
140+
$mockSelect->expects($this->once())
141+
->method('where')
142+
->with(
143+
$this->equalTo('BINARY path REGEXP ? '),
144+
$this->equalTo('^/[^\/]*$')
145+
);
146+
147+
$filter = Bootstrap::getObjectManager()->create(Filter::class);
148+
$filter->setField('directory');
149+
$filter->setValue(null);
150+
151+
$result = $this->directoryFilterProcessor->apply($filter, $mockCollection);
152+
$this->assertTrue($result);
153+
}
154+
155+
/**
156+
* Test regex pattern correctly excludes subdirectories
157+
* The pattern should match direct children only, not files in subdirectories
158+
*/
159+
public function testRegexPatternExcludesSubdirectories(): void
160+
{
161+
$mockCollection = $this->createMock(AbstractDb::class);
162+
$mockSelect = $this->createMock(Select::class);
163+
164+
$mockCollection->expects($this->once())
165+
->method('getSelect')
166+
->willReturn($mockSelect);
167+
168+
// Verify the regex pattern uses [^\/]*$ to exclude subdirectories
169+
// This pattern matches: testing/file.jpg (✓)
170+
// But not: testing/subfolder/file.jpg (✗)
171+
$mockSelect->expects($this->once())
172+
->method('where')
173+
->with(
174+
$this->equalTo('BINARY path REGEXP ? '),
175+
$this->matchesRegularExpression('/\^\w+\/\[\\^\\\\\/\]\*\$/')
176+
);
177+
178+
$filter = Bootstrap::getObjectManager()->create(Filter::class);
179+
$filter->setField('directory');
180+
$filter->setValue('testing');
181+
182+
$result = $this->directoryFilterProcessor->apply($filter, $mockCollection);
183+
$this->assertTrue($result);
184+
}
185+
186+
/**
187+
* Test with mixed case directory name
188+
*/
189+
public function testFilterProcessorWithMixedCaseDirectory(): void
190+
{
191+
$mockCollection = $this->createMock(AbstractDb::class);
192+
$mockSelect = $this->createMock(Select::class);
193+
194+
$mockCollection->expects($this->once())
195+
->method('getSelect')
196+
->willReturn($mockSelect);
197+
198+
$mockSelect->expects($this->once())
199+
->method('where')
200+
->with(
201+
$this->equalTo('BINARY path REGEXP ? '),
202+
$this->equalTo('^MyTestDir/[^\/]*$')
203+
);
204+
205+
$filter = Bootstrap::getObjectManager()->create(Filter::class);
206+
$filter->setField('directory');
207+
$filter->setValue('MyTestDir');
208+
209+
$result = $this->directoryFilterProcessor->apply($filter, $mockCollection);
210+
$this->assertTrue($result);
211+
}
212+
}

0 commit comments

Comments
 (0)