Skip to content

Commit 86cf2e0

Browse files
authored
Merge pull request #10176 from magento-gl/commpr-21755-0511
[Bluetooth] Community Pull Requests delivery - Nov
2 parents b54387b + 5f67867 commit 86cf2e0

File tree

7 files changed

+505
-16
lines changed

7 files changed

+505
-16
lines changed

app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22
/**
3-
* Copyright 2013 Adobe
3+
* Copyright 2025 Adobe
44
* All Rights Reserved.
55
*/
66

@@ -1233,8 +1233,27 @@ public function _loadAttributes($printQuery = false, $logQuery = false)
12331233
throw $e;
12341234
}
12351235

1236+
$attributeCode = $data = [];
1237+
$entityIdField = $entity->getEntityIdField();
1238+
12361239
foreach ($values as $value) {
1237-
$this->_setItemAttributeValue($value);
1240+
$entityId = $value[$entityIdField];
1241+
$attributeId = $value['attribute_id'];
1242+
if (!isset($attributeCode[$attributeId])) {
1243+
$attributeCode[$attributeId] = array_search($attributeId, $this->_selectAttributes);
1244+
if (!$attributeCode[$attributeId]) {
1245+
$attribute = $this->_eavConfig->getAttribute(
1246+
$this->getEntity()->getType(),
1247+
$attributeId
1248+
);
1249+
$attributeCode[$attributeId] = $attribute->getAttributeCode();
1250+
}
1251+
}
1252+
$data[$entityId][$attributeCode[$attributeId]] = $value['value'];
1253+
}
1254+
1255+
if ($data) {
1256+
$this->_setItemAttributeValues($data);
12381257
}
12391258
}
12401259
}
@@ -1305,6 +1324,9 @@ protected function _addLoadAttributesSelectValues($select, $table, $type)
13051324
*
13061325
* Parameter $valueInfo is _getLoadAttributesSelect fetch result row
13071326
*
1327+
* @deprecated Batch process of attribute values is introduced to reduce time complexity.
1328+
* @see _setItemAttributeValues($entityAttributeMap) uses array union (+) to acheive O(n) complexity.
1329+
*
13081330
* @param array $valueInfo
13091331
* @return $this
13101332
* @throws LocalizedException
@@ -1334,6 +1356,33 @@ protected function _setItemAttributeValue($valueInfo)
13341356
return $this;
13351357
}
13361358

1359+
/**
1360+
* Initialize entity object property value
1361+
*
1362+
* Parameter $entityAttributeMap is [entity_id => [attribute_code => value, ...]]
1363+
*
1364+
* @param array $entityAttributeMap
1365+
* @return $this
1366+
* @throws LocalizedException
1367+
*/
1368+
protected function _setItemAttributeValues(array $entityAttributeMap)
1369+
{
1370+
foreach ($entityAttributeMap as $entityId => $attributeValues) {
1371+
if (!isset($this->_itemsById[$entityId])) {
1372+
throw new LocalizedException(
1373+
__('A header row is missing for an attribute. Verify the header row and try again.')
1374+
);
1375+
}
1376+
// _itemsById[$entityId] is always an array (typically with one element)
1377+
// foreach handles edge cases where multiple objects share the same entity ID
1378+
foreach ($this->_itemsById[$entityId] as $object) {
1379+
$object->setData($object->getData()+$attributeValues);
1380+
}
1381+
1382+
}
1383+
return $this;
1384+
}
1385+
13371386
/**
13381387
* Get alias for attribute value table
13391388
*

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)