Skip to content

Commit 2c0cc6e

Browse files
committed
Merge remote-tracking branch 'origin/ACP2E-4128' into PR_2025_08_25_muntianu
2 parents bfdb491 + eb4c231 commit 2c0cc6e

File tree

4 files changed

+589
-10
lines changed

4 files changed

+589
-10
lines changed

app/code/Magento/Csp/Plugin/GenerateAssetIntegrity.php

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2017 Adobe
4+
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
77

@@ -88,4 +88,76 @@ public function afterCreateRequireJsConfigAsset(
8888

8989
return $result;
9090
}
91+
92+
/**
93+
* Generates integrity for RequireJs mixins asset.
94+
*
95+
* @param FileManager $subject
96+
* @param File $result
97+
*
98+
* @return File
99+
*
100+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
101+
*/
102+
public function afterCreateRequireJsMixinsAsset(
103+
FileManager $subject,
104+
File $result
105+
): File {
106+
if (PHP_SAPI === 'cli') {
107+
$this->generateHash($result);
108+
}
109+
110+
return $result;
111+
}
112+
113+
/**
114+
* Generates integrity for static JS asset.
115+
*
116+
* @param FileManager $subject
117+
* @param File|false $result
118+
*
119+
* @return File|false
120+
*
121+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
122+
*/
123+
public function afterCreateStaticJsAsset(
124+
FileManager $subject,
125+
mixed $result
126+
): mixed {
127+
if ($result !== false && PHP_SAPI === 'cli') {
128+
$this->generateHash($result);
129+
}
130+
131+
return $result;
132+
}
133+
134+
/**
135+
* Generates hash for the given file result if it matches the supported content types.
136+
*
137+
* @param File $result
138+
* @return void
139+
*/
140+
private function generateHash(File $result): void
141+
{
142+
if (in_array($result->getContentType(), self::CONTENT_TYPES)) {
143+
try {
144+
$content = $result->getContent();
145+
} catch (\Exception $e) {
146+
$content = null;
147+
}
148+
$path = $result->getPath();
149+
150+
if ($content !== null) {
151+
$integrity = $this->integrityFactory->create(
152+
[
153+
"data" => [
154+
'hash' => $this->hashGenerator->generate($content),
155+
'path' => $path
156+
]
157+
]
158+
);
159+
$this->integrityCollector->collect($integrity);
160+
}
161+
}
162+
}
91163
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Csp\Test\Unit\Plugin;
9+
10+
use Magento\Csp\Plugin\GenerateAssetIntegrity;
11+
use Magento\Csp\Model\SubresourceIntegrityFactory;
12+
use Magento\Csp\Model\SubresourceIntegrityCollector;
13+
use Magento\Csp\Model\SubresourceIntegrity\HashGenerator;
14+
use Magento\Csp\Model\SubresourceIntegrity;
15+
use Magento\Framework\View\Asset\File;
16+
use Magento\RequireJs\Model\FileManager;
17+
use PHPUnit\Framework\TestCase;
18+
use PHPUnit\Framework\MockObject\MockObject;
19+
20+
/**
21+
* Unit test for GenerateAssetIntegrity plugin.
22+
*/
23+
class GenerateAssetIntegrityTest extends TestCase
24+
{
25+
/**
26+
* @var GenerateAssetIntegrity
27+
*/
28+
private $plugin;
29+
30+
/**
31+
* @var HashGenerator|MockObject
32+
*/
33+
private $hashGenerator;
34+
35+
/**
36+
* @var SubresourceIntegrityFactory|MockObject
37+
*/
38+
private $integrityFactory;
39+
40+
/**
41+
* @var SubresourceIntegrityCollector|MockObject
42+
*/
43+
private $integrityCollector;
44+
45+
/**
46+
* @var FileManager|MockObject
47+
*/
48+
private $fileManager;
49+
50+
/**
51+
* @var File|MockObject
52+
*/
53+
private $assetFile;
54+
55+
/**
56+
* @var SubresourceIntegrity|MockObject
57+
*/
58+
private $integrity;
59+
60+
/**
61+
* @inheritDoc
62+
*/
63+
protected function setUp(): void
64+
{
65+
$this->hashGenerator = $this->createMock(HashGenerator::class);
66+
$this->integrityFactory = $this->createMock(SubresourceIntegrityFactory::class);
67+
$this->integrityCollector = $this->createMock(SubresourceIntegrityCollector::class);
68+
$this->fileManager = $this->createMock(FileManager::class);
69+
$this->assetFile = $this->createMock(File::class);
70+
$this->integrity = $this->createMock(SubresourceIntegrity::class);
71+
72+
$this->plugin = new GenerateAssetIntegrity(
73+
$this->hashGenerator,
74+
$this->integrityFactory,
75+
$this->integrityCollector
76+
);
77+
}
78+
79+
/**
80+
* Test afterCreateRequireJsConfigAsset with JS content type.
81+
*/
82+
public function testAfterCreateRequireJsConfigAssetWithJsContent(): void
83+
{
84+
$this->mockJsAssetFile();
85+
$this->mockIntegrityCreation('test-hash', 'test/path.js');
86+
87+
$result = $this->plugin->afterCreateRequireJsConfigAsset($this->fileManager, $this->assetFile);
88+
89+
$this->assertSame($this->assetFile, $result);
90+
91+
$this->integrityCollector->expects($this->any())->method('collect');
92+
}
93+
94+
/**
95+
* Test afterCreateRequireJsMixinsAsset with JS content type.
96+
*/
97+
public function testAfterCreateRequireJsMixinsAssetWithJsContent(): void
98+
{
99+
$this->mockJsAssetFile();
100+
$this->mockIntegrityCreation('test-hash', 'test/path.js');
101+
102+
$result = $this->plugin->afterCreateRequireJsMixinsAsset($this->fileManager, $this->assetFile);
103+
104+
$this->assertSame($this->assetFile, $result);
105+
$this->integrityCollector->expects($this->any())->method('collect');
106+
}
107+
108+
/**
109+
* Test afterCreateRequireJsMixinsAsset with null content.
110+
*/
111+
public function testAfterCreateRequireJsMixinsAssetWithNullContent(): void
112+
{
113+
$this->mockJsAssetFileWithNullContent();
114+
115+
$result = $this->plugin->afterCreateRequireJsMixinsAsset($this->fileManager, $this->assetFile);
116+
117+
$this->assertSame($this->assetFile, $result);
118+
$this->integrityCollector->expects($this->any())->method('collect');
119+
}
120+
121+
/**
122+
* Test afterCreateStaticJsAsset with JS content type.
123+
*/
124+
public function testAfterCreateStaticJsAssetWithJsContent(): void
125+
{
126+
$this->mockJsAssetFile();
127+
$this->mockIntegrityCreation('test-hash', 'test/path.js');
128+
129+
$result = $this->plugin->afterCreateStaticJsAsset($this->fileManager, $this->assetFile);
130+
131+
$this->assertSame($this->assetFile, $result);
132+
133+
$this->integrityCollector->expects($this->any())->method('collect');
134+
}
135+
136+
/**
137+
* Test afterCreateStaticJsAsset with null content.
138+
*/
139+
public function testAfterCreateStaticJsAssetWithNullContent(): void
140+
{
141+
$this->mockJsAssetFileWithNullContent();
142+
143+
$result = $this->plugin->afterCreateStaticJsAsset($this->fileManager, $this->assetFile);
144+
145+
$this->assertSame($this->assetFile, $result);
146+
$this->integrityCollector->expects($this->never())->method('collect');
147+
}
148+
149+
/**
150+
* Test that the plugin correctly handles null content.
151+
*/
152+
public function testPluginLogicWithNullContent(): void
153+
{
154+
$this->mockJsAssetFileWithNullContent();
155+
156+
$result = $this->plugin->afterCreateRequireJsMixinsAsset($this->fileManager, $this->assetFile);
157+
158+
$this->assertSame($this->assetFile, $result);
159+
$this->integrityCollector->expects($this->never())->method('collect');
160+
}
161+
162+
/**
163+
* Mock JS asset file with valid content.
164+
*/
165+
private function mockJsAssetFile(): void
166+
{
167+
$this->assetFile->method('getContentType')->willReturn('js');
168+
$this->assetFile->method('getContent')->willReturn('console.log("test");');
169+
$this->assetFile->method('getPath')->willReturn('test/path.js');
170+
}
171+
172+
/**
173+
* Mock JS asset file with null content.
174+
*/
175+
private function mockJsAssetFileWithNullContent(): void
176+
{
177+
$this->assetFile->method('getContentType')->willReturn('js');
178+
$this->assetFile->method('getContent')->willReturn(null);
179+
$this->assetFile->method('getPath')->willReturn('test/path.js');
180+
}
181+
182+
/**
183+
* Mock integrity creation.
184+
*/
185+
private function mockIntegrityCreation(string $hash, string $path): void
186+
{
187+
$this->hashGenerator->method('generate')->willReturn($hash);
188+
189+
$this->integrityFactory->method('create')
190+
->willReturn($this->integrity);
191+
192+
$this->integrity->method('getHash')->willReturn($hash);
193+
$this->integrity->method('getPath')->willReturn($path);
194+
}
195+
}

app/code/Magento/Deploy/Service/DeployRequireJsConfig.php

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2017 Adobe
4+
* All Rights Reserved.
55
*/
66
namespace Magento\Deploy\Service;
77

88
use Magento\Framework\Locale\ResolverInterfaceFactory;
99
use Magento\Framework\Locale\ResolverInterface;
10+
use Magento\Framework\View\Asset\ConfigInterface;
1011
use Magento\RequireJs\Model\FileManagerFactory;
1112
use Magento\Framework\View\DesignInterfaceFactory;
1213
use Magento\Framework\View\Design\Theme\ListInterface;
@@ -18,10 +19,7 @@
1819
*/
1920
class DeployRequireJsConfig
2021
{
21-
/**
22-
* Default jobs amount
23-
*/
24-
const DEFAULT_JOBS_AMOUNT = 4;
22+
public const DEFAULT_JOBS_AMOUNT = 4;
2523

2624
/**
2725
* @var ListInterface
@@ -53,6 +51,11 @@ class DeployRequireJsConfig
5351
*/
5452
private $localeFactory;
5553

54+
/**
55+
* @var ConfigInterface
56+
*/
57+
private $bundleConfig;
58+
5659
/**
5760
* DeployRequireJsConfig constructor
5861
*
@@ -62,24 +65,29 @@ class DeployRequireJsConfig
6265
* @param FileManagerFactory $fileManagerFactory
6366
* @param ConfigFactory $requireJsConfigFactory
6467
* @param ResolverInterfaceFactory $localeFactory
68+
* @param ConfigInterface $bundleConfig
6569
*/
6670
public function __construct(
6771
ListInterface $themeList,
6872
DesignInterfaceFactory $designFactory,
6973
RepositoryFactory $assetRepoFactory,
7074
FileManagerFactory $fileManagerFactory,
7175
ConfigFactory $requireJsConfigFactory,
72-
ResolverInterfaceFactory $localeFactory
76+
ResolverInterfaceFactory $localeFactory,
77+
ConfigInterface $bundleConfig
7378
) {
7479
$this->themeList = $themeList;
7580
$this->designFactory = $designFactory;
7681
$this->assetRepoFactory = $assetRepoFactory;
7782
$this->fileManagerFactory = $fileManagerFactory;
7883
$this->requireJsConfigFactory = $requireJsConfigFactory;
7984
$this->localeFactory = $localeFactory;
85+
$this->bundleConfig = $bundleConfig;
8086
}
8187

8288
/**
89+
* Deploy RequireJS configuration for a specific theme and locale
90+
*
8391
* @param string $areaCode
8492
* @param string $themePath
8593
* @param string $localeCode
@@ -110,8 +118,11 @@ public function deploy($areaCode, $themePath, $localeCode)
110118
]
111119
);
112120

121+
if ($this->bundleConfig->isBundlingJsFiles()) {
122+
$fileManager->createStaticJsAsset();
123+
$fileManager->createRequireJsMixinsAsset();
124+
}
113125
$fileManager->createRequireJsConfigAsset();
114-
115126
$fileManager->createMinResolverAsset();
116127

117128
return true;

0 commit comments

Comments
 (0)