Skip to content

Commit 10fd2c9

Browse files
author
Mark Berube
committed
Merge remote-tracking branch 'origin/2.3.7-develop' into HEAD
2 parents ed95272 + bbedf18 commit 10fd2c9

File tree

97 files changed

+3592
-451
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+3592
-451
lines changed

app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use Magento\Framework\Exception\LocalizedException;
1212

1313
/**
14-
* Class Upload
14+
* Class Upload image(s)
1515
*/
1616
class Upload extends \Magento\Backend\App\Action implements HttpPostActionInterface
1717
{
@@ -108,8 +108,10 @@ public function execute()
108108

109109
$result['url'] = $this->productMediaConfig->getTmpMediaUrl($result['file']);
110110
$result['file'] = $result['file'] . '.tmp';
111-
} catch (\Exception $e) {
111+
} catch (LocalizedException $e) {
112112
$result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()];
113+
} catch (\Throwable $e) {
114+
$result = ['error' => 'Something went wrong while saving the file(s).', 'errorcode' => 0];
113115
}
114116

115117
/** @var \Magento\Framework\Controller\Result\Raw $response */
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Catalog\Model\Attribute\Backend;
10+
11+
use Magento\Catalog\Model\AbstractModel;
12+
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
13+
use Magento\Eav\Model\Entity\Attribute\Backend\DefaultBackend as ParentBackend;
14+
use Magento\Eav\Model\Entity\Attribute\Exception;
15+
use Magento\Framework\DataObject;
16+
use Magento\Framework\Exception\LocalizedException;
17+
use Magento\Framework\Validation\ValidationException;
18+
use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface;
19+
20+
/**
21+
* Default backend model for catalog attributes.
22+
*/
23+
class DefaultBackend extends ParentBackend
24+
{
25+
/**
26+
* @var WYSIWYGValidatorInterface
27+
*/
28+
private $wysiwygValidator;
29+
30+
/**
31+
* @param WYSIWYGValidatorInterface $wysiwygValidator
32+
*/
33+
public function __construct(WYSIWYGValidatorInterface $wysiwygValidator)
34+
{
35+
$this->wysiwygValidator = $wysiwygValidator;
36+
}
37+
38+
/**
39+
* Validate user HTML value.
40+
*
41+
* @param DataObject $object
42+
* @return void
43+
* @throws LocalizedException
44+
*/
45+
private function validateHtml(DataObject $object): void
46+
{
47+
$attribute = $this->getAttribute();
48+
$code = $attribute->getAttributeCode();
49+
if ($attribute instanceof Attribute && $attribute->getIsHtmlAllowedOnFront()) {
50+
$value = $object->getData($code);
51+
if ($value
52+
&& is_string($value)
53+
&& (!($object instanceof AbstractModel) || $object->getData($code) !== $object->getOrigData($code))
54+
) {
55+
try {
56+
$this->wysiwygValidator->validate($object->getData($code));
57+
} catch (ValidationException $exception) {
58+
$attributeException = new Exception(
59+
__(
60+
'Using restricted HTML elements for "%1". %2',
61+
$attribute->getName(),
62+
$exception->getMessage()
63+
),
64+
$exception
65+
);
66+
$attributeException->setAttributeCode($code)->setPart('backend');
67+
throw $attributeException;
68+
}
69+
}
70+
}
71+
}
72+
73+
/**
74+
* @inheritDoc
75+
*/
76+
public function beforeSave($object)
77+
{
78+
parent::beforeSave($object);
79+
$this->validateHtml($object);
80+
81+
return $this;
82+
}
83+
84+
/**
85+
* @inheritDoc
86+
*/
87+
public function validate($object)
88+
{
89+
$isValid = parent::validate($object);
90+
if ($isValid) {
91+
$this->validateHtml($object);
92+
}
93+
94+
return $isValid;
95+
}
96+
}

app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,10 @@ protected function processDeletedImages($product, array &$images)
3434
foreach ($images as &$image) {
3535
if (!empty($image['removed'])) {
3636
if (!empty($image['value_id'])) {
37-
if (preg_match('/\.\.(\\\|\/)/', $image['file'])) {
38-
continue;
39-
}
4037
$recordsToDelete[] = $image['value_id'];
4138
$catalogPath = $this->mediaConfig->getBaseMediaPath();
42-
$isFile = $this->mediaDirectory->isFile($catalogPath . $image['file']);
39+
$filePath = $this->mediaDirectory->getRelativePath($catalogPath . $image['file']);
40+
$isFile = $this->mediaDirectory->isFile($filePath);
4341
// only delete physical files if they are not used by any other products and if this file exist
4442
if ($isFile && !($this->resourceModel->countImageUses($image['file']) > 1)) {
4543
$filesToDelete[] = ltrim($image['file'], '/');
@@ -116,6 +114,7 @@ protected function extractStoreIds($product)
116114
*
117115
* @param array $files
118116
* @return null
117+
* @throws \Magento\Framework\Exception\FileSystemException
119118
* @since 101.0.0
120119
*/
121120
protected function removeDeletedImages(array $files)
@@ -125,5 +124,7 @@ protected function removeDeletedImages(array $files)
125124
foreach ($files as $filePath) {
126125
$this->mediaDirectory->delete($catalogPath . '/' . $filePath);
127126
}
127+
128+
return null;
128129
}
129130
}

app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
namespace Magento\Catalog\Model\ResourceModel\Eav;
88

9+
use Magento\Catalog\Model\Attribute\Backend\DefaultBackend;
910
use Magento\Catalog\Model\Attribute\LockValidatorInterface;
11+
use Magento\Eav\Model\Entity;
1012
use Magento\Framework\Api\AttributeValueFactory;
1113
use Magento\Framework\Exception\LocalizedException;
1214
use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface;
@@ -903,4 +905,17 @@ public function setIsFilterableInGrid($isFilterableInGrid)
903905
$this->setData(self::IS_FILTERABLE_IN_GRID, $isFilterableInGrid);
904906
return $this;
905907
}
908+
909+
/**
910+
* @inheritDoc
911+
*/
912+
protected function _getDefaultBackendModel()
913+
{
914+
$backend = parent::_getDefaultBackendModel();
915+
if ($backend === Entity::DEFAULT_BACKEND_MODEL) {
916+
$backend = DefaultBackend::class;
917+
}
918+
919+
return $backend;
920+
}
906921
}

app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveCategoryFormActionGroup.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<scrollToTopOfPage stepKey="scrollToTopOfTheCategoryPage"/>
1818
<click selector="{{AdminMainActionsSection.save}}" stepKey="saveCategory"/>
1919
<waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessageAppears"/>
20+
<dontSee selector="{{AdminCategoryMessagesSection.saveCategoryWarningMessage}}" stepKey="dontSeeWarningMessage"/>
2021
<see userInput="You saved the category." selector="{{AdminMessagesSection.success}}" stepKey="assertSuccessMessage"/>
2122
</actionGroup>
2223
</actionGroups>

app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductFormActionGroup.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveProductButton"/>
1818
<click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/>
1919
<waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitProductSaveSuccessMessage"/>
20+
<dontSee selector="{{AdminProductMessagesSection.saveProductWarningMessage}}" stepKey="dontSeeWarningMessage"/>
2021
<see selector="{{AdminMessagesSection.success}}" userInput="You saved the product." stepKey="seeSaveConfirmation"/>
2122
</actionGroup>
2223
</actionGroups>

app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryMessagesSection.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
<section name="AdminCategoryMessagesSection">
1212
<element name="SuccessMessage" type="text" selector=".message-success"/>
1313
<element name="errorMessage" type="text" selector="//div[@class='message message-error error']"/>
14+
<element name="saveCategoryWarningMessage" type="text" selector=".message-warning"/>
1415
</section>
1516
</sections>

app/code/Magento/Catalog/Test/Mftf/Section/AdminProductMessagesSection.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
<section name="AdminProductMessagesSection">
1212
<element name="successMessage" type="text" selector=".message-success"/>
1313
<element name="errorMessage" type="text" selector=".message.message-error.error"/>
14+
<element name="saveProductWarningMessage" type="text" selector=".message-warning"/>
1415
</section>
1516
</sections>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Catalog\Test\Unit\Model\Attribute\Backend;
10+
11+
use Magento\Catalog\Model\AbstractModel;
12+
use Magento\Catalog\Model\Attribute\Backend\DefaultBackend;
13+
use Magento\Framework\DataObject;
14+
use Magento\Framework\Validation\ValidationException;
15+
use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface;
16+
use PHPUnit\Framework\TestCase;
17+
use Magento\Eav\Model\Entity\Attribute as BasicAttribute;
18+
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
19+
use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException;
20+
21+
class DefaultBackendTest extends TestCase
22+
{
23+
/**
24+
* Different cases for attribute validation.
25+
*
26+
* @return array
27+
*/
28+
public function getAttributeConfigurations(): array
29+
{
30+
return [
31+
'basic-attribute' => [true, false, true, 'basic', 'value', false, true, false],
32+
'non-html-attribute' => [false, false, false, 'non-html', 'value', false, false, false],
33+
'empty-html-attribute' => [false, false, true, 'html', null, false, true, false],
34+
'invalid-html-attribute' => [false, false, false, 'html', 'value', false, true, true],
35+
'valid-html-attribute' => [false, true, false, 'html', 'value', false, true, false],
36+
'changed-invalid-html-attribute' => [false, false, true, 'html', 'value', true, true, true],
37+
'changed-valid-html-attribute' => [false, true, true, 'html', 'value', true, true, false]
38+
];
39+
}
40+
41+
/**
42+
* Test attribute validation.
43+
*
44+
* @param bool $isBasic
45+
* @param bool $isValidated
46+
* @param bool $isCatalogEntity
47+
* @param string $code
48+
* @param mixed $value
49+
* @param bool $isChanged
50+
* @param bool $isHtmlAttribute
51+
* @param bool $exceptionThrown
52+
* @dataProvider getAttributeConfigurations
53+
*/
54+
public function testValidate(
55+
bool $isBasic,
56+
bool $isValidated,
57+
bool $isCatalogEntity,
58+
string $code,
59+
$value,
60+
bool $isChanged,
61+
bool $isHtmlAttribute,
62+
bool $exceptionThrown
63+
): void {
64+
if ($isBasic) {
65+
$attributeMock = $this->createMock(BasicAttribute::class);
66+
} else {
67+
$attributeMock = $this->createMock(Attribute::class);
68+
$attributeMock->expects($this->any())
69+
->method('getIsHtmlAllowedOnFront')
70+
->willReturn($isHtmlAttribute);
71+
}
72+
$attributeMock->expects($this->any())->method('getAttributeCode')->willReturn($code);
73+
74+
$validatorMock = $this->getMockForAbstractClass(WYSIWYGValidatorInterface::class);
75+
if (!$isValidated) {
76+
$validatorMock->expects($this->any())
77+
->method('validate')
78+
->willThrowException(new ValidationException(__('HTML is invalid')));
79+
} else {
80+
$validatorMock->expects($this->any())->method('validate');
81+
}
82+
83+
if ($isCatalogEntity) {
84+
$objectMock = $this->createMock(AbstractModel::class);
85+
$objectMock->expects($this->any())
86+
->method('getOrigData')
87+
->willReturn($isChanged ? $value .'-OLD' : $value);
88+
} else {
89+
$objectMock = $this->createMock(DataObject::class);
90+
}
91+
$objectMock->expects($this->any())->method('getData')->with($code)->willReturn($value);
92+
93+
$model = new DefaultBackend($validatorMock);
94+
$model->setAttribute($attributeMock);
95+
96+
$actuallyThrownForSave = false;
97+
try {
98+
$model->beforeSave($objectMock);
99+
} catch (AttributeException $exception) {
100+
$actuallyThrownForSave = true;
101+
}
102+
$actuallyThrownForValidate = false;
103+
try {
104+
$model->validate($objectMock);
105+
} catch (AttributeException $exception) {
106+
$actuallyThrownForValidate = true;
107+
}
108+
$this->assertEquals($actuallyThrownForSave, $actuallyThrownForValidate);
109+
$this->assertEquals($actuallyThrownForSave, $exceptionThrown);
110+
}
111+
}

app/code/Magento/Catalog/view/base/web/js/product/name.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,33 @@
55

66
define([
77
'Magento_Ui/js/grid/columns/column',
8-
'Magento_Catalog/js/product/list/column-status-validator'
9-
], function (Column, columnStatusValidator) {
8+
'Magento_Catalog/js/product/list/column-status-validator',
9+
'escaper'
10+
], function (Column, columnStatusValidator, escaper) {
1011
'use strict';
1112

1213
return Column.extend({
14+
defaults: {
15+
allowedTags: ['div', 'span', 'b', 'strong', 'i', 'em', 'u', 'a']
16+
},
17+
1318
/**
1419
* Depends on this option, product name can be shown or hide. Depends on backend configuration
1520
*
1621
* @returns {Boolean}
1722
*/
1823
isAllowed: function () {
1924
return columnStatusValidator.isValid(this.source(), 'name', 'show_attributes');
25+
},
26+
27+
/**
28+
* Name column.
29+
*
30+
* @param {String} label
31+
* @returns {String}
32+
*/
33+
getNameUnsanitizedHtml: function (label) {
34+
return escaper.escapeHtml(label, this.allowedTags);
2035
}
2136
});
2237
});

0 commit comments

Comments
 (0)