Skip to content

Commit c7bf284

Browse files
authored
Merge branch '2.4-develop' into PlatformHealthScope26May25
2 parents c46a41c + 4ca7360 commit c7bf284

File tree

14 files changed

+856
-144
lines changed

14 files changed

+856
-144
lines changed

app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use Magento\Framework\App\Config\ScopeConfigInterface;
1212
use Magento\Framework\DB\Select;
1313
use Magento\Store\Model\ScopeInterface;
14+
use Magento\Framework\DB\Adapter\AdapterInterface;
15+
use Magento\Framework\DB\Ddl\Table;
1416

1517
/**
1618
* Category resource collection
@@ -370,40 +372,80 @@ public function loadProductCount($items, $countRegular = true, $countAnchor = tr
370372
* @param array $categoryIds
371373
* @param int $websiteId
372374
* @return array
375+
* @throws \Zend_Db_Exception
373376
*/
374377
private function getCountFromCategoryTableBulk(
375378
array $categoryIds,
376379
int $websiteId
377380
) : array {
378-
$subSelect = clone $this->_conn->select();
379-
$subSelect->from(['ce2' => $this->getTable('catalog_category_entity')], 'ce2.entity_id')
380-
->where("ce2.path LIKE CONCAT(ce.path, '/%') OR ce2.path = ce.path");
381-
382-
$select = clone $this->_conn->select();
383-
$select->from(
384-
['ce' => $this->getTable('catalog_category_entity')],
385-
'ce.entity_id'
386-
);
387-
$joinCondition = new \Zend_Db_Expr("cp.category_id IN ({$subSelect})");
388-
$select->joinLeft(
389-
['cp' => $this->getProductTable()],
390-
$joinCondition,
391-
'COUNT(DISTINCT cp.product_id) AS product_count'
381+
$connection = $this->_conn;
382+
$tempTableName = 'temp_category_descendants_' . uniqid();
383+
$tempTable = $connection->newTable($tempTableName)
384+
->addColumn(
385+
'category_id',
386+
Table::TYPE_INTEGER,
387+
null,
388+
['unsigned' => true, 'nullable' => false],
389+
'Category ID'
390+
)
391+
->addColumn(
392+
'descendant_id',
393+
Table::TYPE_INTEGER,
394+
null,
395+
['unsigned' => true, 'nullable' => false],
396+
'Descendant ID'
397+
)
398+
->addIndex(
399+
$connection->getIndexName($tempTableName, ['category_id', 'descendant_id']),
400+
['category_id', 'descendant_id'],
401+
['type' => AdapterInterface::INDEX_TYPE_PRIMARY]
402+
);
403+
$connection->createTemporaryTable($tempTable);
404+
$selectDescendants = $connection->select()
405+
->from(
406+
['ce' => $this->getTable('catalog_category_entity')],
407+
['category_id' => 'ce.entity_id', 'descendant_id' => 'ce2.entity_id']
408+
)
409+
->joinInner(
410+
['ce2' => $this->getTable('catalog_category_entity')],
411+
'ce2.path LIKE CONCAT(ce.path, \'/%\') OR ce2.entity_id = ce.entity_id',
412+
[]
413+
)
414+
->where('ce.entity_id IN (?)', $categoryIds);
415+
416+
$connection->query(
417+
$connection->insertFromSelect(
418+
$selectDescendants,
419+
$tempTableName,
420+
['category_id', 'descendant_id']
421+
)
392422
);
423+
$select = $connection->select()
424+
->from(
425+
['t' => $tempTableName],
426+
['category_id' => 't.category_id']
427+
)
428+
->joinLeft(
429+
['cp' => $this->getTable('catalog_category_product')],
430+
'cp.category_id = t.descendant_id',
431+
['product_count' => 'COUNT(DISTINCT cp.product_id)']
432+
);
393433
if ($websiteId) {
394434
$select->join(
395435
['w' => $this->getProductWebsiteTable()],
396436
'cp.product_id = w.product_id',
397437
[]
398-
)->where(
399-
'w.website_id = ?',
400-
$websiteId
401-
);
438+
)->where('w.website_id = ?', $websiteId);
439+
}
440+
$select->group('t.category_id');
441+
$result = $connection->fetchPairs($select);
442+
$connection->dropTemporaryTable($tempTableName);
443+
$counts = array_fill_keys($categoryIds, 0);
444+
foreach ($result as $categoryId => $count) {
445+
$counts[$categoryId] = (int)$count;
402446
}
403-
$select->where('ce.entity_id IN(?)', $categoryIds);
404-
$select->group('ce.entity_id');
405447

406-
return $this->_conn->fetchPairs($select);
448+
return $counts;
407449
}
408450

409451
/**

app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/CollectionTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Category;
99

10+
use Magento\Catalog\Model\Category;
1011
use Magento\Framework\Data\Collection\EntityFactory;
12+
use Magento\Store\Model\Store;
1113
use Psr\Log\LoggerInterface;
1214
use Magento\Framework\Data\Collection\Db\FetchStrategyInterface;
1315
use Magento\Framework\Event\ManagerInterface;
@@ -215,4 +217,58 @@ public function testLoadProductCount() : void
215217
->willReturn([]);
216218
$this->collection->loadProductCount([]);
217219
}
220+
221+
/**
222+
* Test that loadProductCount calls getCountFromCategoryTableBulk
223+
*/
224+
public function testLoadProductCountCallsBulkMethodForLargeCategoryCount()
225+
{
226+
$websiteId = 1;
227+
$storeId = 1;
228+
$categoryCount = 401;
229+
$items = [];
230+
$categoryIds = [];
231+
for ($i = 1; $i <= $categoryCount; $i++) {
232+
$category = $this->getMockBuilder(Category::class)
233+
->addMethods(['getIsAnchor'])
234+
->onlyMethods(['getId', 'setProductCount'])
235+
->disableOriginalConstructor()
236+
->getMock();
237+
$category->method('getId')->willReturn($i);
238+
$category->method('getIsAnchor')->willReturn(true);
239+
$category->expects($this->once())->method('setProductCount')->with(5);
240+
$items[$i] = $category;
241+
$categoryIds[] = $i;
242+
}
243+
$storeMock = $this->createMock(Store::class);
244+
$storeMock->method('getWebsiteId')->willReturn($websiteId);
245+
$this->storeManager->method('getStore')->with($storeId)->willReturn($storeMock);
246+
$this->connection->method('select')->willReturn($this->select);
247+
$counts = array_fill_keys($categoryIds, 5);
248+
$tableMock = $this->createMock(\Magento\Framework\DB\Ddl\Table::class);
249+
$tableMock->method('addColumn')->willReturnSelf();
250+
$tableMock->method('addIndex')->willReturnSelf();
251+
$this->connection->method('newTable')
252+
->with($this->stringContains('temp_category_descendants_'))
253+
->willReturn($tableMock);
254+
$this->connection->expects($this->once())->method('createTemporaryTable')->with($tableMock);
255+
$this->connection->expects($this->once())->method('dropTemporaryTable')
256+
->with($this->stringContains('temp_category_descendants_'));
257+
$this->select->method('from')->willReturnSelf();
258+
$this->select->method('joinInner')->willReturnSelf();
259+
$this->select->method('where')->willReturnSelf();
260+
$this->connection->method('select')->willReturn($this->select);
261+
$this->connection->method('insertFromSelect')->willReturn('INSERT QUERY');
262+
$this->connection->method('query')->with('INSERT QUERY')->willReturnSelf();
263+
$this->select->method('from')->willReturnSelf();
264+
$this->select->method('joinLeft')->willReturnSelf();
265+
$this->select->method('join')->willReturnSelf();
266+
$this->select->method('where')->willReturnSelf();
267+
$this->select->method('group')->willReturnSelf();
268+
$this->connection->method('fetchPairs')
269+
->with($this->isInstanceOf(Select::class))
270+
->willReturn($counts);
271+
$this->collection->setProductStoreId($storeId);
272+
$this->collection->loadProductCount($items, false, true);
273+
}
218274
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Csp\Plugin;
9+
10+
use Magento\Csp\Model\SubresourceIntegrity\HashGenerator;
11+
use Magento\Csp\Model\SubresourceIntegrityCollector;
12+
use Magento\Csp\Model\SubresourceIntegrityFactory;
13+
use Magento\Deploy\Service\Bundle;
14+
use Magento\Framework\App\Filesystem\DirectoryList;
15+
use Magento\Framework\Exception\FileSystemException;
16+
use Magento\Framework\Filesystem;
17+
use Magento\Framework\Filesystem\Io\File;
18+
19+
class GenerateBundleAssetIntegrity
20+
{
21+
/**
22+
* @var HashGenerator
23+
*/
24+
private HashGenerator $hashGenerator;
25+
26+
/**
27+
* @var SubresourceIntegrityFactory
28+
*/
29+
private SubresourceIntegrityFactory $integrityFactory;
30+
31+
/**
32+
* @var SubresourceIntegrityCollector
33+
*/
34+
private SubresourceIntegrityCollector $integrityCollector;
35+
36+
/**
37+
* @var Filesystem
38+
*/
39+
private Filesystem $filesystem;
40+
41+
/**
42+
* @var File
43+
*/
44+
private File $fileIo;
45+
46+
/**
47+
* @param HashGenerator $hashGenerator
48+
* @param SubresourceIntegrityFactory $integrityFactory
49+
* @param SubresourceIntegrityCollector $integrityCollector
50+
* @param Filesystem $filesystem
51+
* @param File $fileIo
52+
*/
53+
public function __construct(
54+
HashGenerator $hashGenerator,
55+
SubresourceIntegrityFactory $integrityFactory,
56+
SubresourceIntegrityCollector $integrityCollector,
57+
Filesystem $filesystem,
58+
File $fileIo,
59+
) {
60+
$this->hashGenerator = $hashGenerator;
61+
$this->integrityFactory = $integrityFactory;
62+
$this->integrityCollector = $integrityCollector;
63+
$this->filesystem = $filesystem;
64+
$this->fileIo = $fileIo;
65+
}
66+
67+
/**
68+
* Generate SRI hashes for JS files in the bundle directory.
69+
*
70+
* @param Bundle $subject
71+
* @param string|null $result
72+
* @param string $area
73+
* @param string $theme
74+
* @param string $locale
75+
* @return void
76+
* @throws FileSystemException
77+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
78+
*/
79+
public function afterDeploy(Bundle $subject, ?string $result, string $area, string $theme, string $locale)
80+
{
81+
if (PHP_SAPI == 'cli') {
82+
$pubStaticDir = $this->filesystem->getDirectoryRead(DirectoryList::STATIC_VIEW);
83+
$files = $pubStaticDir->search(
84+
$area ."/" . $theme . "/" . $locale . "/" . Bundle::BUNDLE_JS_DIR . "/*.js"
85+
);
86+
foreach ($files as $file) {
87+
$integrity = $this->integrityFactory->create(
88+
[
89+
"data" => [
90+
'hash' => $this->hashGenerator->generate(
91+
$pubStaticDir->readFile($file)
92+
),
93+
'path' => $area . '/' . $theme . '/' . $locale .
94+
"/" . Bundle::BUNDLE_JS_DIR . '/' . $this->fileIo->getPathInfo($file)['basename']
95+
]
96+
]
97+
);
98+
99+
$this->integrityCollector->collect($integrity);
100+
}
101+
}
102+
}
103+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Csp\Plugin;
9+
10+
use Magento\Csp\Model\SubresourceIntegrityRepositoryPool;
11+
use Magento\Csp\Model\SubresourceIntegrityRepository;
12+
use Magento\Csp\Model\SubresourceIntegrity\HashGenerator;
13+
use Magento\Csp\Model\SubresourceIntegrityFactory;
14+
use Magento\Framework\App\Area;
15+
use Magento\Framework\App\Filesystem\DirectoryList;
16+
use Magento\Framework\Exception\FileSystemException;
17+
use Magento\Framework\Filesystem;
18+
use Magento\Framework\View\Asset\LocalInterface;
19+
use Magento\Framework\View\Asset\MergeStrategy\FileExists;
20+
21+
class GenerateMergedAssetIntegrity
22+
{
23+
/**
24+
* @var SubresourceIntegrityRepository
25+
*/
26+
private SubresourceIntegrityRepository $sourceIntegrityRepository;
27+
28+
/**
29+
* @var HashGenerator
30+
*/
31+
private HashGenerator $hashGenerator;
32+
33+
/**
34+
* @var SubresourceIntegrityFactory
35+
*/
36+
private SubresourceIntegrityFactory $integrityFactory;
37+
38+
/**
39+
* @var Filesystem
40+
*/
41+
private Filesystem $filesystem;
42+
43+
/**
44+
* @param SubresourceIntegrityRepositoryPool $sourceIntegrityRepositoryPool
45+
* @param HashGenerator $hashGenerator
46+
* @param SubresourceIntegrityFactory $integrityFactory
47+
* @param Filesystem $filesystem
48+
*/
49+
public function __construct(
50+
SubresourceIntegrityRepositoryPool $sourceIntegrityRepositoryPool,
51+
HashGenerator $hashGenerator,
52+
SubresourceIntegrityFactory $integrityFactory,
53+
Filesystem $filesystem
54+
) {
55+
$this->sourceIntegrityRepository = $sourceIntegrityRepositoryPool->get(Area::AREA_FRONTEND);
56+
$this->hashGenerator = $hashGenerator;
57+
$this->integrityFactory = $integrityFactory;
58+
$this->filesystem = $filesystem;
59+
}
60+
61+
/**
62+
* Generate SRI hash for merged JS files.
63+
*
64+
* @param FileExists $subject
65+
* @param string|null $result
66+
* @param array $assetsToMerge
67+
* @param LocalInterface $resultAsset
68+
* @return string|null
69+
* @throws FileSystemException
70+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
71+
*/
72+
public function afterMerge(FileExists $subject, ?string $result, array $assetsToMerge, LocalInterface $resultAsset)
73+
{
74+
if ($resultAsset->getContentType() !== 'js') {
75+
return $result;
76+
}
77+
$pubStaticDir = $this->filesystem->getDirectoryWrite(DirectoryList::STATIC_VIEW);
78+
$integrity = $this->integrityFactory->create(
79+
[
80+
"data" => [
81+
'hash' => $this->hashGenerator->generate(
82+
$pubStaticDir->readFile($resultAsset->getPath())
83+
),
84+
'path' => $resultAsset->getPath()
85+
]
86+
]
87+
);
88+
89+
$this->sourceIntegrityRepository->save($integrity);
90+
91+
return $result;
92+
}
93+
}

0 commit comments

Comments
 (0)