Skip to content

Commit 492f0ca

Browse files
authored
Merge pull request #484 from magento-falcons/MAGETWO-58976
Bugs: MAGETWO-58976: [Magento Cloud] - Import issue with a multiselect option having special symbols (, and |)
2 parents dbbf925 + b4f69c3 commit 492f0ca

File tree

16 files changed

+348
-37
lines changed

16 files changed

+348
-37
lines changed

app/code/Magento/CatalogImportExport/Model/Export/Product.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,7 @@ protected function collectRawData()
953953
if (is_scalar($attrValue)) {
954954
if (!in_array($fieldName, $this->_getExportMainAttrCodes())) {
955955
$additionalAttributes[$fieldName] = $fieldName .
956-
ImportProduct::PAIR_NAME_VALUE_SEPARATOR . $attrValue;
956+
ImportProduct::PAIR_NAME_VALUE_SEPARATOR . $this->wrapValue($attrValue);
957957
}
958958
$data[$itemId][$storeId][$fieldName] = htmlspecialchars_decode($attrValue);
959959
}
@@ -963,7 +963,7 @@ protected function collectRawData()
963963
$additionalAttributes[$code] = $fieldName .
964964
ImportProduct::PAIR_NAME_VALUE_SEPARATOR . implode(
965965
ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR,
966-
$this->collectedMultiselectsData[$storeId][$productLinkId][$code]
966+
$this->wrapValue($this->collectedMultiselectsData[$storeId][$productLinkId][$code])
967967
);
968968
}
969969
}
@@ -994,6 +994,25 @@ protected function collectRawData()
994994
return $data;
995995
}
996996

997+
/**
998+
* Wrap values with double quotes if "Fields Enclosure" option is enabled
999+
*
1000+
* @param string|array $value
1001+
* @return string|array
1002+
*/
1003+
private function wrapValue($value)
1004+
{
1005+
if (!empty($this->_parameters[\Magento\ImportExport\Model\Export::FIELDS_ENCLOSURE])) {
1006+
$wrap = function ($value) {
1007+
return sprintf('"%s"', str_replace('"', '""', $value));
1008+
};
1009+
1010+
$value = is_array($value) ? array_map($wrap, $value) : $wrap($value);
1011+
}
1012+
1013+
return $value;
1014+
}
1015+
9971016
/**
9981017
* @return array
9991018
*/

app/code/Magento/CatalogImportExport/Model/Import/Product.php

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,13 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
644644
*/
645645
private $productEntityIdentifierField;
646646

647+
/**
648+
* Escaped separator value for regular expression.
649+
* The value is based on PSEUDO_MULTI_LINE_SEPARATOR constant.
650+
* @var string
651+
*/
652+
private $multiLineSeparatorForRegexp;
653+
647654
/**
648655
* @param \Magento\Framework\Json\Helper\Data $jsonHelper
649656
* @param \Magento\ImportExport\Helper\Data $importExportData
@@ -2438,14 +2445,40 @@ private function _parseAdditionalAttributes($rowData)
24382445
}
24392446

24402447
/**
2441-
* Retrieves additional attributes as array code=>value.
2448+
* Retrieves additional attributes in format:
2449+
* [
2450+
* code1 => value1,
2451+
* code2 => value2,
2452+
* ...
2453+
* codeN => valueN
2454+
* ]
24422455
*
2443-
* @param string $additionalAttributes
2456+
* @param string $additionalAttributes Attributes data that will be parsed
24442457
* @return array
24452458
*/
24462459
private function parseAdditionalAttributes($additionalAttributes)
24472460
{
2448-
$attributeNameValuePairs = explode($this->getMultipleValueSeparator(), $additionalAttributes);
2461+
return empty($this->_parameters[Import::FIELDS_ENCLOSURE])
2462+
? $this->parseAttributesWithoutWrappedValues($additionalAttributes)
2463+
: $this->parseAttributesWithWrappedValues($additionalAttributes);
2464+
}
2465+
2466+
/**
2467+
* Parses data and returns attributes in format:
2468+
* [
2469+
* code1 => value1,
2470+
* code2 => value2,
2471+
* ...
2472+
* codeN => valueN
2473+
* ]
2474+
*
2475+
* @param string $attributesData Attributes data that will be parsed. It keeps data in format:
2476+
* code=value,code2=value2...,codeN=valueN
2477+
* @return array
2478+
*/
2479+
private function parseAttributesWithoutWrappedValues($attributesData)
2480+
{
2481+
$attributeNameValuePairs = explode($this->getMultipleValueSeparator(), $attributesData);
24492482
$preparedAttributes = [];
24502483
$code = '';
24512484
foreach ($attributeNameValuePairs as $attributeData) {
@@ -2463,6 +2496,75 @@ private function parseAdditionalAttributes($additionalAttributes)
24632496
return $preparedAttributes;
24642497
}
24652498

2499+
/**
2500+
* Parses data and returns attributes in format:
2501+
* [
2502+
* code1 => value1,
2503+
* code2 => value2,
2504+
* ...
2505+
* codeN => valueN
2506+
* ]
2507+
* All values have unescaped data except mupliselect attributes,
2508+
* they should be parsed in additional method - parseMultiselectValues()
2509+
*
2510+
* @param string $attributesData Attributes data that will be parsed. It keeps data in format:
2511+
* code="value",code2="value2"...,codeN="valueN"
2512+
* where every value is wrapped in double quotes. Double quotes as part of value should be duplicated.
2513+
* E.g. attribute with code 'attr_code' has value 'my"value'. This data should be stored as attr_code="my""value"
2514+
*
2515+
* @return array
2516+
*/
2517+
private function parseAttributesWithWrappedValues($attributesData)
2518+
{
2519+
$attributes = [];
2520+
preg_match_all('~((?:[a-z0-9_])+)="((?:[^"]|""|"' . $this->getMultiLineSeparatorForRegexp() . '")+)"+~',
2521+
$attributesData,
2522+
$matches
2523+
);
2524+
foreach ($matches[1] as $i => $attributeCode) {
2525+
$attribute = $this->retrieveAttributeByCode($attributeCode);
2526+
$value = 'multiselect' != $attribute->getFrontendInput()
2527+
? str_replace('""', '"', $matches[2][$i])
2528+
: '"' . $matches[2][$i] . '"';
2529+
$attributes[$attributeCode] = $value;
2530+
}
2531+
return $attributes;
2532+
}
2533+
2534+
/**
2535+
* Parse values of multiselect attributes depends on "Fields Enclosure" parameter
2536+
*
2537+
* @param string $values
2538+
* @return array
2539+
*/
2540+
public function parseMultiselectValues($values)
2541+
{
2542+
if (empty($this->_parameters[Import::FIELDS_ENCLOSURE])) {
2543+
return explode(self::PSEUDO_MULTI_LINE_SEPARATOR, $values);
2544+
}
2545+
if (preg_match_all('~"((?:[^"]|"")*)"~', $values, $matches)) {
2546+
return $values = array_map(function ($value) {
2547+
return str_replace('""', '"', $value);
2548+
}, $matches[1]);
2549+
}
2550+
return [$values];
2551+
}
2552+
2553+
/**
2554+
* Retrieves escaped PSEUDO_MULTI_LINE_SEPARATOR if it is metacharacter for regular expression
2555+
*
2556+
* @return string
2557+
*/
2558+
private function getMultiLineSeparatorForRegexp()
2559+
{
2560+
if (!$this->multiLineSeparatorForRegexp) {
2561+
$this->multiLineSeparatorForRegexp = in_array(self::PSEUDO_MULTI_LINE_SEPARATOR, str_split('[\^$.|?*+(){}'))
2562+
? '\\' . self::PSEUDO_MULTI_LINE_SEPARATOR
2563+
: self::PSEUDO_MULTI_LINE_SEPARATOR;
2564+
}
2565+
return $this->multiLineSeparatorForRegexp;
2566+
}
2567+
24662568
/**
24672569
* Set values in use_config_ fields.
24682570
*

app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -490,23 +490,25 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe
490490
$resultAttrs = [];
491491

492492
foreach ($this->_getProductAttributes($rowData) as $attrCode => $attrParams) {
493-
if (!$attrParams['is_static']) {
494-
if (isset($rowData[$attrCode]) && strlen($rowData[$attrCode])) {
495-
$resultAttrs[$attrCode] = in_array($attrParams['type'], ['select', 'boolean'])
496-
? $attrParams['options'][strtolower($rowData[$attrCode])]
497-
: $rowData[$attrCode];
498-
if ('multiselect' == $attrParams['type']) {
499-
$resultAttrs[$attrCode] = [];
500-
foreach (explode(Product::PSEUDO_MULTI_LINE_SEPARATOR, $rowData[$attrCode]) as $value) {
501-
$resultAttrs[$attrCode][] = $attrParams['options'][strtolower($value)];
502-
}
503-
$resultAttrs[$attrCode] = implode(',', $resultAttrs[$attrCode]);
493+
if ($attrParams['is_static']) {
494+
continue;
495+
}
496+
if (isset($rowData[$attrCode]) && strlen($rowData[$attrCode])) {
497+
if (in_array($attrParams['type'], ['select', 'boolean'])) {
498+
$resultAttrs[$attrCode] = $attrParams['options'][strtolower($rowData[$attrCode])];
499+
} elseif ('multiselect' == $attrParams['type']) {
500+
$resultAttrs[$attrCode] = [];
501+
foreach ($this->_entityModel->parseMultiselectValues($rowData[$attrCode]) as $value) {
502+
$resultAttrs[$attrCode][] = $attrParams['options'][strtolower($value)];
504503
}
505-
} elseif (array_key_exists($attrCode, $rowData)) {
504+
$resultAttrs[$attrCode] = implode(',', $resultAttrs[$attrCode]);
505+
} else {
506506
$resultAttrs[$attrCode] = $rowData[$attrCode];
507-
} elseif ($withDefaultValue && null !== $attrParams['default_value']) {
508-
$resultAttrs[$attrCode] = $attrParams['default_value'];
509507
}
508+
} elseif (array_key_exists($attrCode, $rowData)) {
509+
$resultAttrs[$attrCode] = $rowData[$attrCode];
510+
} elseif ($withDefaultValue && null !== $attrParams['default_value']) {
511+
$resultAttrs[$attrCode] = $attrParams['default_value'];
510512
}
511513
}
512514

app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,32 @@ protected function textValidation($attrCode, $type)
7171
return $valid;
7272
}
7373

74+
/**
75+
* Check if value is valid attribute option
76+
*
77+
* @param string $attrCode
78+
* @param array $possibleOptions
79+
* @param string $value
80+
* @return bool
81+
*/
82+
private function validateOption($attrCode, $possibleOptions, $value)
83+
{
84+
if (!isset($possibleOptions[strtolower($value)])) {
85+
$this->_addMessages(
86+
[
87+
sprintf(
88+
$this->context->retrieveMessageTemplate(
89+
RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_OPTION
90+
),
91+
$attrCode
92+
)
93+
]
94+
);
95+
return false;
96+
}
97+
return true;
98+
}
99+
74100
/**
75101
* @param mixed $attrCode
76102
* @param string $type
@@ -166,23 +192,15 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData)
166192
break;
167193
case 'select':
168194
case 'boolean':
195+
$valid = $this->validateOption($attrCode, $attrParams['options'], $rowData[$attrCode]);
196+
break;
169197
case 'multiselect':
170-
$values = explode(Product::PSEUDO_MULTI_LINE_SEPARATOR, $rowData[$attrCode]);
171-
$valid = true;
198+
$values = $this->context->parseMultiselectValues($rowData[$attrCode]);
172199
foreach ($values as $value) {
173-
$valid = $valid && isset($attrParams['options'][strtolower($value)]);
174-
}
175-
if (!$valid) {
176-
$this->_addMessages(
177-
[
178-
sprintf(
179-
$this->context->retrieveMessageTemplate(
180-
RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_OPTION
181-
),
182-
$attrCode
183-
)
184-
]
185-
);
200+
$valid = $this->validateOption($attrCode, $attrParams['options'], $value);
201+
if (!$valid) {
202+
break;
203+
}
186204
}
187205
break;
188206
case 'datetime':

app/code/Magento/ImportExport/Block/Adminhtml/Export/Edit/Form.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ protected function _prepareForm()
8686
'values' => $this->_formatFactory->create()->toOptionArray()
8787
]
8888
);
89+
$fieldset->addField(
90+
\Magento\ImportExport\Model\Export::FIELDS_ENCLOSURE,
91+
'checkbox',
92+
[
93+
'name' => \Magento\ImportExport\Model\Export::FIELDS_ENCLOSURE,
94+
'label' => __('Fields Enclosure'),
95+
'title' => __('Fields Enclosure'),
96+
'value' => 1,
97+
]
98+
);
8999

90100
$form->setUseContainer(true);
91101
$this->setForm($form);

app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,16 @@ protected function _prepareForm()
174174
'value' => Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR,
175175
]
176176
);
177+
$fieldsets[$behaviorCode]->addField(
178+
$behaviorCode . \Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE,
179+
'checkbox',
180+
[
181+
'name' => \Magento\ImportExport\Model\Import::FIELDS_ENCLOSURE,
182+
'label' => __('Fields enclosure'),
183+
'title' => __('Fields enclosure'),
184+
'value' => 1,
185+
]
186+
);
177187
}
178188

179189
// fieldset for file uploading

app/code/Magento/ImportExport/Model/Export.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ class Export extends \Magento\ImportExport\Model\AbstractModel
2020

2121
const FILTER_ELEMENT_SKIP = 'skip_attr';
2222

23+
/**
24+
* Allow multiple values wrapping in double quotes for additional attributes.
25+
*/
26+
const FIELDS_ENCLOSURE = 'fields_enclosure';
27+
2328
/**
2429
* Filter fields types.
2530
*/

app/code/Magento/ImportExport/Model/Import.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ class Import extends \Magento\ImportExport\Model\AbstractModel
7878
*/
7979
const FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR = '_import_multiple_value_separator';
8080

81+
/**
82+
* Allow multiple values wrapping in double quotes for additional attributes.
83+
*/
84+
const FIELDS_ENCLOSURE = 'fields_enclosure';
85+
8186
/**#@-*/
8287

8388
/**

app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ require([
8080
var oldAction = form.action;
8181
var url = oldAction + ((oldAction.slice(-1) != '/') ? '/' : '') + 'entity/' + $F('entity')
8282
+ '/file_format/' + $F('file_format');
83+
if ($F('fields_enclosure')) {
84+
url += '/fields_enclosure/' + $F('fields_enclosure');
85+
}
8386
form.action = url;
8487
form.submit();
8588
form.action = oldAction;

dev/tests/integration/testsuite/Magento/Catalog/_files/multiselect_attribute.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
'option_1' => ['Option 1'],
4141
'option_2' => ['Option 2'],
4242
'option_3' => ['Option 3'],
43-
'option_4' => ['Option 4 "!@#$%^&*'],
43+
'option_4' => ['Option 4 "!@#$%^&*']
4444
],
4545
'order' => [
4646
'option_1' => 1,

0 commit comments

Comments
 (0)