diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml
deleted file mode 100644
index e02c34fd8868e..0000000000000
--- a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml
index e6782dca897d7..f9d3c49d509e9 100644
--- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml
@@ -17,11 +17,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.xml
new file mode 100644
index 0000000000000..a37bb443224b4
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Check error message in validation message box
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.xml
new file mode 100644
index 0000000000000..35ac68b602a5e
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Check if there's a validation message box on page and asserts the validation messages number
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.xml
new file mode 100644
index 0000000000000..f0afcffca816c
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Clicks 'Add to Cart' on a Storefront Bundled Product page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml
index 7a188fd58e1af..739c2839e990d 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml
@@ -14,8 +14,8 @@
-
-
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml
index c47cf6095c777..1dea8958c3552 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml
@@ -17,7 +17,7 @@
-
+
@@ -38,5 +38,6 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml
new file mode 100644
index 0000000000000..91cc58ee0119b
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml
index 5b56598dc58e2..4ba6fd6183653 100644
--- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml
+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml
@@ -8,40 +8,55 @@
getOption() ?>
getSelections() ?>
+escapeHtmlAttr($_option->getId()) ?>
+escapeHtmlAttr($_option->getId()) ?>
+escapeHtmlAttr($_option->getId()) . ']' ?>
+escapeHtmlAttr($_option->getId()) . ']"]:checked\'}"' ?>
+
= $block->escapeHtml($_option->getTitle()) ?>
- showSingle()) : ?>
+ showSingle()): ?>
= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?>
= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?>
-
-
+
+
+ getSelectionId() ?>
- getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . $block->escapeHtmlAttr($_option->getId()) . ']"]:checked\'}"'; } ?>
- name="bundle_option[= $block->escapeHtmlAttr($_option->getId()) ?>][= $block->escapeHtmlAttr($_selection->getId()) ?>]"
- data-selector="bundle_option[= $block->escapeHtmlAttr($_option->getId()) ?>][= $block->escapeHtmlAttr($_selection->getId()) ?>]"
- isSelected($_selection)) { echo ' checked="checked"'; } ?>
- isSaleable()) { echo ' disabled="disabled"'; } ?>
- value="= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/>
+ getRequired()): ?>
+ = /* @noEscape */ $dataValidation ?>
+
+ name="=/* @noEscape */ $inputName .'['. $block->escapeHtmlAttr($sectionId)?>]"
+ data-selector="= /* @noEscape */ $inputName.'['.$block->escapeHtmlAttr($sectionId)?>]"
+ isSelected($selection)): ?>
+ = ' checked="checked"' ?>
+
+ isSaleable()): ?>
+ = ' disabled="disabled"' ?>
+
+ value="= $block->escapeHtmlAttr($sectionId) ?>"
+ data-errors-message-box="#validation-message-box"/>
- = /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection) ?>
+ for="= /* @noEscape */ $inputId . '-' . $block->escapeHtmlAttr($sectionId) ?>">
+ = /* @noEscape */ $block->getSelectionQtyTitlePrice($selection) ?>
- = /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?>
+ = /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($selection) ?>
+
diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml
index 9103c4191544c..030c9f5efcf50 100644
--- a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml
+++ b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml
@@ -14,7 +14,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
index cf12e332be86d..8da2614eb2ced 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php
@@ -261,6 +261,10 @@ public function execute()
unset($data['apply_to']);
}
+ if ($model->getBackendType() == 'static' && !$model->getIsUserDefined()) {
+ $data['frontend_class'] = $model->getFrontendClass();
+ }
+
$model->addData($data);
if (!$attributeId) {
diff --git a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php
index 497ed2fd49953..a928ddea03a70 100644
--- a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php
+++ b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php
@@ -8,21 +8,21 @@
namespace Magento\Catalog\Model;
/**
- * Filter custom attributes for product using the blacklist
+ * Filter custom attributes for product using the excluded list
*/
class FilterProductCustomAttribute
{
/**
* @var array
*/
- private $blackList;
+ private $excludedList;
/**
- * @param array $blackList
+ * @param array $excludedList
*/
- public function __construct(array $blackList = [])
+ public function __construct(array $excludedList = [])
{
- $this->blackList = $blackList;
+ $this->excludedList = $excludedList;
}
/**
@@ -33,6 +33,6 @@ public function __construct(array $blackList = [])
*/
public function execute(array $attributes): array
{
- return array_diff_key($attributes, array_flip($this->blackList));
+ return array_diff_key($attributes, array_flip($this->excludedList));
}
}
diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php
index 77dedb9eb0121..3494fd00a8b6c 100644
--- a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php
+++ b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php
@@ -72,6 +72,8 @@ public function renderRangeLabel($fromPrice, $toPrice)
}
/**
+ * Prepare range data
+ *
* @param int $range
* @param int[] $dbRanges
* @return array
@@ -81,12 +83,10 @@ public function renderRangeData($range, $dbRanges)
if (empty($dbRanges)) {
return [];
}
- $lastIndex = array_keys($dbRanges);
- $lastIndex = $lastIndex[count($lastIndex) - 1];
foreach ($dbRanges as $index => $count) {
- $fromPrice = $index == 1 ? '' : ($index - 1) * $range;
- $toPrice = $index == $lastIndex ? '' : $index * $range;
+ $fromPrice = $index == 1 ? 0 : ($index - 1) * $range;
+ $toPrice = $index * $range;
$this->itemDataBuilder->addItemData(
$this->renderRangeLabel($fromPrice, $toPrice),
$fromPrice . '-' . $toPrice,
diff --git a/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php
index 3d4d9f607da48..40fe6a01e260c 100644
--- a/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php
+++ b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php
@@ -83,6 +83,12 @@ public function getFailedItems()
}
}
+ /**
+ * Clear validation messages to prevent wrong validation for subsequent price update.
+ * Work around for backward compatible changes.
+ */
+ $this->failedItems = [];
+
return $failedItems;
}
}
diff --git a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php b/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php
deleted file mode 100644
index 404760a51eff5..0000000000000
--- a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php
+++ /dev/null
@@ -1,57 +0,0 @@
-productRepository = $productRepository;
- $this->storeManager = $storeManager;
- }
-
- /**
- * Check if product is salable.
- *
- * @param int|string $productId
- * @param int|null $storeId
- * @return bool
- */
- public function isSalable($productId, $storeId = null): bool
- {
- if ($storeId === null) {
- $storeId = $this->storeManager->getStore()->getId();
- }
- /** @var \Magento\Catalog\Model\Product $product */
- $product = $this->productRepository->getById($productId, false, $storeId);
-
- return $product->isSalable();
- }
-}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php
index fab2441db26c9..939f9d354af85 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php
@@ -8,11 +8,15 @@
use Magento\Catalog\Model\Category;
/**
+ * Aggregate count for parent category after deleting child category
+ *
* Class AggregateCount
*/
class AggregateCount
{
/**
+ * Reduces children count for parent categories
+ *
* @param Category $category
* @return void
*/
@@ -25,9 +29,7 @@ public function processDelete(Category $category)
*/
$parentIds = $category->getParentIds();
if ($parentIds) {
- $childDecrease = $category->getChildrenCount() + 1;
- // +1 is itself
- $data = ['children_count' => new \Zend_Db_Expr('children_count - ' . $childDecrease)];
+ $data = ['children_count' => new \Zend_Db_Expr('children_count - 1')];
$where = ['entity_id IN(?)' => $parentIds];
$resourceModel->getConnection()->update($resourceModel->getEntityTable(), $data, $where);
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php
index cf5760b0c33a9..8d03eb3ccafc9 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php
@@ -147,7 +147,7 @@ private function processCategoryLinks($newCategoryPositions, &$oldCategoryPositi
* @param bool $insert
* @return array
*/
- private function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false)
+ public function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false)
{
if (empty($insertLinks)) {
return [];
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml
new file mode 100644
index 0000000000000..020fb27063be7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Switch the Storefront to the provided Store.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml
new file mode 100644
index 0000000000000..14a7967422332
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Updates the Category Name for proper Store View.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml
new file mode 100644
index 0000000000000..bd7eb664819dd
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ Enable the category
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml
new file mode 100644
index 0000000000000..8ecef0df400be
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Set "Manage Stock" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml
new file mode 100644
index 0000000000000..0f6a8df1ebf8c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Fills in the "Maximum Qty Allowed in Shopping Cart" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml
new file mode 100644
index 0000000000000..abbfdacc15395
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Fills in the "Minimum Qty Allowed in Shopping Cart" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml
new file mode 100644
index 0000000000000..4ecfa0762db9f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Fills in the "Notify for Quantity Below" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml
new file mode 100644
index 0000000000000..7846689a8d643
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Set "Qty Uses Decimals" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml
new file mode 100644
index 0000000000000..98156eb1ad9b1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Set "Stock status" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml
new file mode 100644
index 0000000000000..3a75b0a3cd361
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml
new file mode 100644
index 0000000000000..e0a98a8932d4d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml
new file mode 100644
index 0000000000000..cead98091d268
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Validate that the Category is not present in menu on Frontend.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml
new file mode 100644
index 0000000000000..c56a18b4895a4
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Validate that the Category is present in menu on Frontend.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml
new file mode 100644
index 0000000000000..65858be673dfa
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml
new file mode 100644
index 0000000000000..5b7dd3026a905
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Click on the product item from the sidebar comparing list.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml
new file mode 100644
index 0000000000000..4a403364a91e3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Switch the Storefront to the provided Store.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml
index c9b67e0db4398..1d6bb970ea4d3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml
@@ -30,4 +30,9 @@
No
0
+
+
+ cataloginventory/options/stock_threshold_qty
+ 0
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml
index 26946692ce050..7a829a5475758 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml
index 544bdf85681c9..b919cdff2bb92 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml
@@ -8,12 +8,12 @@
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml
index 78818dd37a5d4..7be02126e3a0f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml
@@ -25,5 +25,6 @@
+
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
index a94610abf0918..6dcdde75bb2b7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
@@ -34,42 +34,46 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml
index e64707a895fd4..cb6ae9244e958 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml
@@ -37,41 +37,48 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml
index 192bab7c6d126..2cdec1405e9f9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml
@@ -31,21 +31,19 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml
index b8e58eae8a98a..83404391abca9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml
@@ -23,16 +23,13 @@
-
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml
index 4b0774d2307dd..e66984dda4427 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml
@@ -21,10 +21,8 @@
-
-
-
+
@@ -37,39 +35,32 @@
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
-
-
-
-
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml
index 40bd3bdcfea20..4979b06a1051e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml
@@ -27,16 +27,19 @@
-
-
-
+
+
+
+
-
-
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml
index fe07360d6b9ca..4310c6f06219a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml
@@ -33,59 +33,48 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
-
-
-
-
+
+
+
+
+
+
-
-
+
+
+
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml
index 0ca8e74c4e59e..f4d464455491b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml
@@ -32,16 +32,12 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -50,32 +46,40 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml
index 9b5fa25085e1a..48e6245b011ba 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml
@@ -37,23 +37,26 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml
new file mode 100644
index 0000000000000..507e4ae14e83c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml
new file mode 100644
index 0000000000000..dc608a7f12dd3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml
new file mode 100644
index 0000000000000..914ac3444db22
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php
index ca35d49113f41..681cef8489796 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php
@@ -7,10 +7,12 @@
namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute;
+use Magento\Backend\Model\Session;
use Magento\Backend\Model\View\Result\Redirect as ResultRedirect;
use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save;
use Magento\Catalog\Helper\Product as ProductHelper;
+use Magento\Catalog\Model\Product\Attribute\Frontend\Inputtype\Presentation;
use Magento\Catalog\Model\Product\AttributeSet\Build;
use Magento\Catalog\Model\Product\AttributeSet\BuildFactory;
use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory;
@@ -31,63 +33,64 @@
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.TooManyFields)
*/
class SaveTest extends AttributeTest
{
/**
* @var BuildFactory|MockObject
*/
- protected $buildFactoryMock;
+ private $buildFactoryMock;
/**
* @var FilterManager|MockObject
*/
- protected $filterManagerMock;
+ private $filterManagerMock;
/**
* @var ProductHelper|MockObject
*/
- protected $productHelperMock;
+ private $productHelperMock;
/**
* @var AttributeFactory|MockObject
*/
- protected $attributeFactoryMock;
+ private $attributeFactoryMock;
/**
* @var ValidatorFactory|MockObject
*/
- protected $validatorFactoryMock;
+ private $validatorFactoryMock;
/**
* @var CollectionFactory|MockObject
*/
- protected $groupCollectionFactoryMock;
+ private $groupCollectionFactoryMock;
/**
* @var LayoutFactory|MockObject
*/
- protected $layoutFactoryMock;
+ private $layoutFactoryMock;
/**
* @var ResultRedirect|MockObject
*/
- protected $redirectMock;
+ private $redirectMock;
/**
- * @var AttributeSet|MockObject
+ * @var AttributeSetInterface|MockObject
*/
- protected $attributeSetMock;
+ private $attributeSetMock;
/**
* @var Build|MockObject
*/
- protected $builderMock;
+ private $builderMock;
/**
* @var InputTypeValidator|MockObject
*/
- protected $inputTypeValidatorMock;
+ private $inputTypeValidatorMock;
/**
* @var FormData|MockObject
@@ -104,19 +107,34 @@ class SaveTest extends AttributeTest
*/
private $attributeCodeValidatorMock;
+ /**
+ * @var Presentation|MockObject
+ */
+ private $presentationMock;
+
+ /**
+ * @var Session|MockObject
+ */
+
+ private $sessionMock;
+
protected function setUp(): void
{
parent::setUp();
+ $this->filterManagerMock = $this->createMock(FilterManager::class);
+ $this->productHelperMock = $this->createMock(ProductHelper::class);
+ $this->attributeSetMock = $this->createMock(AttributeSetInterface::class);
+ $this->builderMock = $this->createMock(Build::class);
+ $this->inputTypeValidatorMock = $this->createMock(InputTypeValidator::class);
+ $this->formDataSerializerMock = $this->createMock(FormData::class);
+ $this->attributeCodeValidatorMock = $this->createMock(AttributeCodeValidator::class);
+ $this->presentationMock = $this->createMock(Presentation::class);
+ $this->sessionMock = $this->createMock(Session::class);
+ $this->layoutFactoryMock = $this->createMock(LayoutFactory::class);
$this->buildFactoryMock = $this->getMockBuilder(BuildFactory::class)
->setMethods(['create'])
->disableOriginalConstructor()
->getMock();
- $this->filterManagerMock = $this->getMockBuilder(FilterManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->productHelperMock = $this->getMockBuilder(ProductHelper::class)
- ->disableOriginalConstructor()
- ->getMock();
$this->attributeFactoryMock = $this->getMockBuilder(AttributeFactory::class)
->setMethods(['create'])
->disableOriginalConstructor()
@@ -129,32 +147,23 @@ protected function setUp(): void
->setMethods(['create'])
->disableOriginalConstructor()
->getMock();
- $this->layoutFactoryMock = $this->getMockBuilder(LayoutFactory::class)
- ->disableOriginalConstructor()
- ->getMock();
$this->redirectMock = $this->getMockBuilder(ResultRedirect::class)
->setMethods(['setData', 'setPath'])
->disableOriginalConstructor()
->getMock();
- $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class)
- ->disableOriginalConstructor()
- ->getMockForAbstractClass();
- $this->builderMock = $this->getMockBuilder(Build::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->formDataSerializerMock = $this->getMockBuilder(FormData::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class)
- ->disableOriginalConstructor()
- ->getMock();
$this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class)
- ->setMethods(['getId', 'get'])
- ->getMockForAbstractClass();
-
+ ->setMethods(
+ [
+ 'getId',
+ 'get',
+ 'getBackendTypeByInput',
+ 'getDefaultValueByInput',
+ 'getBackendType',
+ 'getFrontendClass',
+ 'addData',
+ 'save'
+ ]
+ )->getMockForAbstractClass();
$this->buildFactoryMock->expects($this->any())
->method('create')
->willReturn($this->builderMock);
@@ -167,7 +176,7 @@ protected function setUp(): void
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
protected function getModel()
{
@@ -184,7 +193,9 @@ protected function getModel()
'groupCollectionFactory' => $this->groupCollectionFactoryMock,
'layoutFactory' => $this->layoutFactoryMock,
'formDataSerializer' => $this->formDataSerializerMock,
- 'attributeCodeValidator' => $this->attributeCodeValidatorMock
+ 'attributeCodeValidator' => $this->attributeCodeValidatorMock,
+ 'presentation' => $this->presentationMock,
+ '_session' => $this->sessionMock
]);
}
@@ -214,6 +225,67 @@ public function testExecuteWithEmptyData()
$this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute());
}
+ public function testExecuteSaveFrontendClass()
+ {
+ $data = [
+ 'frontend_input' => 'test_frontend_input',
+ ];
+
+ $this->requestMock->expects($this->any())
+ ->method('getParam')
+ ->willReturnMap([
+ ['isAjax', null, null],
+ ['serialized_options', '[]', ''],
+ ['set', null, 1],
+ ['attribute_code', null, 'test_attribute_code'],
+ ]);
+ $this->formDataSerializerMock
+ ->expects($this->once())
+ ->method('unserialize')
+ ->with('')
+ ->willReturn([]);
+ $this->requestMock->expects($this->once())
+ ->method('getPostValue')
+ ->willReturn($data);
+ $this->inputTypeValidatorMock->expects($this->any())
+ ->method('isValid')
+ ->with($data['frontend_input'])
+ ->willReturn(true);
+ $this->presentationMock->expects($this->once())
+ ->method('convertPresentationDataToInputType')
+ ->willReturn($data);
+ $this->productHelperMock->expects($this->once())
+ ->method('getAttributeSourceModelByInputType')
+ ->with($data['frontend_input'])
+ ->willReturn(null);
+ $this->productHelperMock->expects($this->once())
+ ->method('getAttributeBackendModelByInputType')
+ ->with($data['frontend_input'])
+ ->willReturn(null);
+ $this->productAttributeMock->expects($this->once())
+ ->method('getBackendTypeByInput')
+ ->with($data['frontend_input'])
+ ->willReturnSelf('test_backend_type');
+ $this->productAttributeMock->expects($this->once())
+ ->method('getDefaultValueByInput')
+ ->with($data['frontend_input'])
+ ->willReturn(null);
+ $this->productAttributeMock->expects($this->once())
+ ->method('getBackendType')
+ ->willReturn('static');
+ $this->productAttributeMock->expects($this->once())
+ ->method('getFrontendClass')
+ ->willReturn('static');
+ $this->resultFactoryMock->expects($this->any())
+ ->method('create')
+ ->willReturn($this->redirectMock);
+ $this->redirectMock->expects($this->any())
+ ->method('setPath')
+ ->willReturnSelf();
+
+ $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute());
+ }
+
public function testExecute()
{
$data = [
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php
new file mode 100644
index 0000000000000..c73e02fb7ecbf
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php
@@ -0,0 +1,91 @@
+categoryMock = $this->createMock(Category::class);
+ $this->resourceCategoryMock = $this->createMock(ResourceCategory::class);
+ $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)
+ ->getMockForAbstractClass();
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->aggregateCount = $this->objectManagerHelper->getObject(AggregateCount::class);
+ }
+
+ /**
+ * @return void
+ */
+ public function testProcessDelete(): void
+ {
+ $parentIds = 3;
+ $table = 'catalog_category_entity';
+
+ $this->categoryMock->expects($this->once())
+ ->method('getResource')
+ ->willReturn($this->resourceCategoryMock);
+ $this->categoryMock->expects($this->once())
+ ->method('getParentIds')
+ ->willReturn($parentIds);
+ $this->resourceCategoryMock->expects($this->any())
+ ->method('getEntityTable')
+ ->willReturn($table);
+ $this->resourceCategoryMock->expects($this->once())
+ ->method('getConnection')
+ ->willReturn($this->connectionMock);
+ $this->connectionMock->expects($this->once())
+ ->method('update')
+ ->with(
+ $table,
+ ['children_count' => new \Zend_Db_Expr('children_count - 1')],
+ ['entity_id IN(?)' => $parentIds]
+ );
+ $this->aggregateCount->processDelete($this->categoryMock);
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php
index 254d893d24584..17318d4207841 100644
--- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php
@@ -7,13 +7,16 @@
namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier;
-use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection;
-use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Categories;
+use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
+use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection;
use Magento\Framework\AuthorizationInterface;
use Magento\Framework\DB\Helper as DbHelper;
use Magento\Framework\UrlInterface;
use Magento\Store\Model\Store;
+use Magento\Backend\Model\Auth\Session;
+use Magento\Authorization\Model\Role;
+use Magento\User\Model\User;
use PHPUnit\Framework\MockObject\MockObject;
/**
@@ -51,6 +54,11 @@ class CategoriesTest extends AbstractModifierTest
*/
private $authorizationMock;
+ /**
+ * @var Session|MockObject
+ */
+ private $sessionMock;
+
protected function setUp(): void
{
parent::setUp();
@@ -72,7 +80,10 @@ protected function setUp(): void
$this->authorizationMock = $this->getMockBuilder(AuthorizationInterface::class)
->disableOriginalConstructor()
->getMockForAbstractClass();
-
+ $this->sessionMock = $this->getMockBuilder(Session::class)
+ ->setMethods(['getUser'])
+ ->disableOriginalConstructor()
+ ->getMock();
$this->categoryCollectionFactoryMock->expects($this->any())
->method('create')
->willReturn($this->categoryCollectionMock);
@@ -88,6 +99,26 @@ protected function setUp(): void
$this->categoryCollectionMock->expects($this->any())
->method('getIterator')
->willReturn(new \ArrayIterator([]));
+
+ $roleAdmin = $this->getMockBuilder(Role::class)
+ ->setMethods(['getId'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $roleAdmin->expects($this->any())
+ ->method('getId')
+ ->willReturn(0);
+
+ $userAdmin = $this->getMockBuilder(User::class)
+ ->setMethods(['getRole'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $userAdmin->expects($this->any())
+ ->method('getRole')
+ ->willReturn($roleAdmin);
+
+ $this->sessionMock->expects($this->any())
+ ->method('getUser')
+ ->willReturn($userAdmin);
}
/**
@@ -101,11 +132,28 @@ protected function createModel()
'locator' => $this->locatorMock,
'categoryCollectionFactory' => $this->categoryCollectionFactoryMock,
'arrayManager' => $this->arrayManagerMock,
- 'authorization' => $this->authorizationMock
+ 'authorization' => $this->authorizationMock,
+ 'session' => $this->sessionMock
]
);
}
+ /**
+ * @param object $object
+ * @param string $method
+ * @param array $args
+ * @return mixed
+ * @throws \ReflectionException
+ */
+ private function invokeMethod($object, $method, $args = [])
+ {
+ $class = new \ReflectionClass(Categories::class);
+ $method = $class->getMethod($method);
+ $method->setAccessible(true);
+
+ return $method->invokeArgs($object, $args);
+ }
+
public function testModifyData()
{
$this->assertSame([], $this->getModel()->modifyData([]));
@@ -176,4 +224,44 @@ public function modifyMetaLockedDataProvider()
{
return [[true], [false]];
}
+
+ /**
+ * Asserts that a user with an ACL role ID of 0 and a user with an ACL role ID of 1 do not have the same cache IDs
+ * Assumes a store ID of 0
+ *
+ * @throws \ReflectionException
+ */
+ public function testAclCacheIds()
+ {
+ $categoriesAdmin = $this->createModel();
+ $cacheIdAdmin = $this->invokeMethod($categoriesAdmin, 'getCategoriesTreeCacheId', [0]);
+
+ $roleAclUser = $this->getMockBuilder(Role::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $roleAclUser->expects($this->any())
+ ->method('getId')
+ ->willReturn(1);
+
+ $userAclUser = $this->getMockBuilder(User::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $userAclUser->expects($this->any())
+ ->method('getRole')
+ ->will($this->returnValue($roleAclUser));
+
+ $this->sessionMock = $this->getMockBuilder(Session::class)
+ ->setMethods(['getUser'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->sessionMock->expects($this->any())
+ ->method('getUser')
+ ->will($this->returnValue($userAclUser));
+
+ $categoriesAclUser = $this->createModel();
+ $cacheIdAclUser = $this->invokeMethod($categoriesAclUser, 'getCategoriesTreeCacheId', [0]);
+
+ $this->assertNotEquals($cacheIdAdmin, $cacheIdAclUser);
+ }
}
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php
index 7608173c8edfc..c0d5f0a1af3b8 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Categories.php
@@ -18,12 +18,14 @@
use Magento\Framework\UrlInterface;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\Framework\AuthorizationInterface;
+use Magento\Backend\Model\Auth\Session;
/**
* Data provider for categories field of product page
*
* @api
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
* @since 101.0.0
*/
class Categories extends AbstractModifier
@@ -86,6 +88,11 @@ class Categories extends AbstractModifier
*/
private $authorization;
+ /**
+ * @var Session
+ */
+ private $session;
+
/**
* @param LocatorInterface $locator
* @param CategoryCollectionFactory $categoryCollectionFactory
@@ -94,6 +101,7 @@ class Categories extends AbstractModifier
* @param ArrayManager $arrayManager
* @param SerializerInterface $serializer
* @param AuthorizationInterface $authorization
+ * @param Session $session
*/
public function __construct(
LocatorInterface $locator,
@@ -102,7 +110,8 @@ public function __construct(
UrlInterface $urlBuilder,
ArrayManager $arrayManager,
SerializerInterface $serializer = null,
- AuthorizationInterface $authorization = null
+ AuthorizationInterface $authorization = null,
+ Session $session = null
) {
$this->locator = $locator;
$this->categoryCollectionFactory = $categoryCollectionFactory;
@@ -111,6 +120,7 @@ public function __construct(
$this->arrayManager = $arrayManager;
$this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class);
$this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class);
+ $this->session = $session ?: ObjectManager::getInstance()->get(Session::class);
}
/**
@@ -370,10 +380,16 @@ protected function getCategoriesTree($filter = null)
* @param string $filter
* @return string
*/
- private function getCategoriesTreeCacheId(int $storeId, string $filter = '') : string
+ private function getCategoriesTreeCacheId(int $storeId, string $filter = ''): string
{
+ if ($this->session->getUser() !== null) {
+ return self::CATEGORY_TREE_ID
+ . '_' . (string)$storeId
+ . '_' . $this->session->getUser()->getAclRole()
+ . '_' . $filter;
+ }
return self::CATEGORY_TREE_ID
- . '_' . (string) $storeId
+ . '_' . (string)$storeId
. '_' . $filter;
}
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php
index 0295e778f2b9b..dd757841410e2 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php
@@ -40,7 +40,7 @@
use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory;
/**
- * Data provider for eav attributes on product page
+ * Class Eav data provider for product editing form
*
* @api
*
@@ -791,7 +791,9 @@ private function getAttributeDefaultValue(ProductAttributeInterface $attribute)
\Magento\Store\Model\ScopeInterface::SCOPE_STORE,
$this->storeManager->getStore()
);
- $attribute->setDefaultValue($defaultValue);
+ if ($defaultValue !== null) {
+ $attribute->setDefaultValue($defaultValue);
+ }
}
return $attribute->getDefaultValue();
}
diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml
index 1c97c920266df..a0aa48fb76b13 100644
--- a/app/code/Magento/Catalog/etc/db_schema.xml
+++ b/app/code/Magento/Catalog/etc/db_schema.xml
@@ -138,6 +138,11 @@
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/etc/db_schema_whitelist.json b/app/code/Magento/Catalog/etc/db_schema_whitelist.json
index d4bd6927d4345..f4cda73c371d0 100644
--- a/app/code/Magento/Catalog/etc/db_schema_whitelist.json
+++ b/app/code/Magento/Catalog/etc/db_schema_whitelist.json
@@ -69,7 +69,8 @@
},
"index": {
"CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID": true,
- "CATALOG_PRODUCT_ENTITY_INT_STORE_ID": true
+ "CATALOG_PRODUCT_ENTITY_INT_STORE_ID": true,
+ "CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID_STORE_ID_VALUE": true
},
"constraint": {
"PRIMARY": true,
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
index 320e0adc29b9f..140659abfbfe6 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
@@ -8,6 +8,7 @@
namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation;
use Magento\Framework\App\ResourceConnection;
+use Magento\Store\Model\Store;
/**
* Fetch product attribute option data including attribute info
@@ -41,16 +42,18 @@ public function __construct(ResourceConnection $resourceConnection)
* Get option data. Return list of attributes with option data
*
* @param array $optionIds
+ * @param int|null $storeId
* @param array $attributeCodes
* @return array
* @throws \Zend_Db_Statement_Exception
*/
- public function getOptions(array $optionIds, array $attributeCodes = []): array
+ public function getOptions(array $optionIds, ?int $storeId, array $attributeCodes = []): array
{
if (!$optionIds) {
return [];
}
+ $storeId = $storeId ?: Store::DEFAULT_STORE_ID;
$connection = $this->resourceConnection->getConnection();
$select = $connection->select()
->from(
@@ -70,9 +73,21 @@ public function getOptions(array $optionIds, array $attributeCodes = []): array
['option_value' => $this->resourceConnection->getTableName('eav_attribute_option_value')],
'options.option_id = option_value.option_id',
[
- 'option_label' => 'option_value.value',
'option_id' => 'option_value.option_id',
]
+ )->joinLeft(
+ ['option_value_store' => $this->resourceConnection->getTableName('eav_attribute_option_value')],
+ "options.option_id = option_value_store.option_id AND option_value_store.store_id = {$storeId}",
+ [
+ 'option_label' => $connection->getCheckSql(
+ 'option_value_store.value_id > 0',
+ 'option_value_store.value',
+ 'option_value.value'
+ )
+ ]
+ )->where(
+ 'a.attribute_id = options.attribute_id AND option_value.store_id = ?',
+ Store::DEFAULT_STORE_ID
);
$select->where('option_value.option_id IN (?)', $optionIds);
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
index 0ec65c88024f2..105e91320de49 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
@@ -71,7 +71,7 @@ public function __construct(
*/
public function build(AggregationInterface $aggregation, ?int $storeId): array
{
- $attributeOptions = $this->getAttributeOptions($aggregation);
+ $attributeOptions = $this->getAttributeOptions($aggregation, $storeId);
// build layer per attribute
$result = [];
@@ -133,10 +133,11 @@ private function isBucketEmpty(?BucketInterface $bucket): bool
* Get list of attributes with options
*
* @param AggregationInterface $aggregation
+ * @param int|null $storeId
* @return array
* @throws \Zend_Db_Statement_Exception
*/
- private function getAttributeOptions(AggregationInterface $aggregation): array
+ private function getAttributeOptions(AggregationInterface $aggregation, ?int $storeId): array
{
$attributeOptionIds = [];
$attributes = [];
@@ -154,6 +155,6 @@ function (AggregationValueInterface $value) {
return [];
}
- return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $attributes);
+ return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $storeId, $attributes);
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
index 69592657241a0..0bfd9d58ec969 100644
--- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
+++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
@@ -8,7 +8,10 @@
namespace Magento\CatalogGraphQl\Model;
use GraphQL\Language\AST\FieldNode;
+use GraphQL\Language\AST\InlineFragmentNode;
+use GraphQL\Language\AST\NodeKind;
use Magento\Eav\Model\Entity\Collection\AbstractCollection;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* Joins attributes for provided field node field names.
@@ -43,11 +46,12 @@ public function __construct(array $fieldToAttributeMap = [])
*
* @param FieldNode $fieldNode
* @param AbstractCollection $collection
+ * @param ResolveInfo $resolveInfo
* @return void
*/
- public function join(FieldNode $fieldNode, AbstractCollection $collection): void
+ public function join(FieldNode $fieldNode, AbstractCollection $collection, ResolveInfo $resolveInfo): void
{
- foreach ($this->getQueryFields($fieldNode) as $field) {
+ foreach ($this->getQueryFields($fieldNode, $resolveInfo) as $field) {
$this->addFieldToCollection($collection, $field);
}
}
@@ -56,26 +60,70 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection): void
* Get an array of queried fields.
*
* @param FieldNode $fieldNode
+ * @param ResolveInfo $resolveInfo
* @return string[]
*/
- public function getQueryFields(FieldNode $fieldNode): array
+ public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): array
{
if (null === $this->getFieldNodeSelections($fieldNode)) {
$query = $fieldNode->selectionSet->selections;
$selectedFields = [];
+ $fragmentFields = [];
/** @var FieldNode $field */
foreach ($query as $field) {
- if ($field->kind === 'InlineFragment') {
- continue;
+ if ($field->kind === NodeKind::INLINE_FRAGMENT) {
+ $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field);
+ } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD &&
+ ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) {
+
+ foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) {
+ if (isset($spreadNode->selectionSet->selections)) {
+ $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo);
+ } else {
+ $selectedFields[] = $spreadNode->name->value;
+ }
+ }
+ } else {
+ $selectedFields[] = $field->name->value;
}
- $selectedFields[] = $field->name->value;
}
- $this->setSelectionsForFieldNode($fieldNode, $selectedFields);
+ if ($fragmentFields) {
+ $selectedFields = array_merge($selectedFields, array_merge(...$fragmentFields));
+ }
+ $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields));
}
return $this->getFieldNodeSelections($fieldNode);
}
+ /**
+ * Add fields from inline fragment nodes
+ *
+ * @param ResolveInfo $resolveInfo
+ * @param InlineFragmentNode $inlineFragmentField
+ * @param array $inlineFragmentFields
+ * @return string[]
+ */
+ private function addInlineFragmentFields(
+ ResolveInfo $resolveInfo,
+ InlineFragmentNode $inlineFragmentField,
+ $inlineFragmentFields = []
+ ): array {
+ $query = $inlineFragmentField->selectionSet->selections;
+ /** @var FieldNode $field */
+ foreach ($query as $field) {
+ if ($field->kind === NodeKind::INLINE_FRAGMENT) {
+ $this->addInlineFragmentFields($resolveInfo, $field, $inlineFragmentFields);
+ } elseif (isset($field->selectionSet->selections)) {
+ continue;
+ } else {
+ $inlineFragmentFields[] = $field->name->value;
+ }
+ }
+
+ return array_unique($inlineFragmentFields);
+ }
+
/**
* Add field to collection select
*
diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
index b5d02511da4e7..ab100c7272ba0 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
@@ -8,6 +8,9 @@
namespace Magento\CatalogGraphQl\Model\Category;
use GraphQL\Language\AST\FieldNode;
+use GraphQL\Language\AST\InlineFragmentNode;
+use GraphQL\Language\AST\NodeKind;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* Used for determining the depth information for a requested category tree in a GraphQL request
@@ -17,22 +20,57 @@ class DepthCalculator
/**
* Calculate the total depth of a category tree inside a GraphQL request
*
+ * @param ResolveInfo $resolveInfo
* @param FieldNode $fieldNode
* @return int
*/
- public function calculate(FieldNode $fieldNode) : int
+ public function calculate(ResolveInfo $resolveInfo, FieldNode $fieldNode) : int
{
$selections = $fieldNode->selectionSet->selections ?? [];
$depth = count($selections) ? 1 : 0;
$childrenDepth = [0];
foreach ($selections as $node) {
- if ($node->kind === 'InlineFragment' || null !== $node->alias) {
+ if (isset($node->alias) && null !== $node->alias) {
continue;
}
- $childrenDepth[] = $this->calculate($node);
+ if ($node->kind === NodeKind::INLINE_FRAGMENT) {
+ $childrenDepth[] = $this->addInlineFragmentDepth($resolveInfo, $node);
+ } elseif ($node->kind === NodeKind::FRAGMENT_SPREAD && isset($resolveInfo->fragments[$node->name->value])) {
+ foreach ($resolveInfo->fragments[$node->name->value]->selectionSet->selections as $spreadNode) {
+ $childrenDepth[] = $this->calculate($resolveInfo, $spreadNode);
+ }
+ } else {
+ $childrenDepth[] = $this->calculate($resolveInfo, $node);
+ }
}
return $depth + max($childrenDepth);
}
+
+ /**
+ * Add inline fragment fields into calculating of category depth
+ *
+ * @param ResolveInfo $resolveInfo
+ * @param InlineFragmentNode $inlineFragmentField
+ * @param array $depth
+ * @return int
+ */
+ private function addInlineFragmentDepth(
+ ResolveInfo $resolveInfo,
+ InlineFragmentNode $inlineFragmentField,
+ $depth = []
+ ): int {
+ $selections = $inlineFragmentField->selectionSet->selections;
+ /** @var FieldNode $field */
+ foreach ($selections as $field) {
+ if ($field->kind === NodeKind::INLINE_FRAGMENT) {
+ $depth[] = $this->addInlineFragmentDepth($resolveInfo, $field, $depth);
+ } elseif ($field->selectionSet && $field->selectionSet->selections) {
+ $depth[] = $this->calculate($resolveInfo, $field);
+ }
+ }
+
+ return $depth ? max($depth) : 0;
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php
index 5a230ceed0ca4..c6de07bdedd19 100644
--- a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php
+++ b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php
@@ -10,7 +10,7 @@
use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface;
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
class ProductLinksTypeResolver implements TypeResolverInterface
{
@@ -20,9 +20,9 @@ class ProductLinksTypeResolver implements TypeResolverInterface
private $linkTypes = ['related', 'upsell', 'crosssell'];
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
- public function resolveType(array $data) : string
+ public function resolveType(array $data): string
{
if (isset($data['link_type'])) {
$linkType = $data['link_type'];
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php
index 535fe3a80cd25..d7118d71db89b 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php
@@ -7,18 +7,18 @@
namespace Magento\CatalogGraphQl\Model\Resolver;
-use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories;
-use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\Catalog\Model\ResourceModel\Category\Collection;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
use Magento\CatalogGraphQl\Model\AttributesJoiner;
+use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator;
+use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories;
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
-use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;
-use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator;
+use Magento\Framework\GraphQl\Query\ResolverInterface;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Store\Model\StoreManagerInterface;
/**
@@ -121,7 +121,7 @@ function () use ($that, $categoryIds, $info) {
}
if (!$this->collection->isLoaded()) {
- $that->attributesJoiner->join($info->fieldNodes[0], $this->collection);
+ $that->attributesJoiner->join($info->fieldNodes[0], $this->collection, $info);
$this->collection->addIdFilter($this->categoryIds);
}
/** @var CategoryInterface | \Magento\Catalog\Model\Category $item */
@@ -130,7 +130,7 @@ function () use ($that, $categoryIds, $info) {
// Try to extract all requested fields from the loaded collection data
$categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item, true);
$categories[$item->getId()]['model'] = $item;
- $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0]);
+ $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0], $info);
$extractedFields = array_keys($categories[$item->getId()]);
$foundFields = array_intersect($requestedFields, $extractedFields);
if (count($requestedFields) === count($foundFields)) {
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php
index 14732ecf37c63..187fd05c1001e 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php
@@ -22,7 +22,15 @@ class BatchProductLinks implements BatchServiceContractResolverInterface
/**
* @var string[]
*/
- private static $linkTypes = ['related', 'upsell', 'crosssell'];
+ private $linkTypes;
+
+ /**
+ * @param array $linkTypes
+ */
+ public function __construct(array $linkTypes)
+ {
+ $this->linkTypes = $linkTypes;
+ }
/**
* @inheritDoc
@@ -44,7 +52,7 @@ public function convertToServiceArgument(ResolveRequestInterface $request)
/** @var \Magento\Catalog\Model\Product $product */
$product = $value['model'];
- return new ListCriteria((string)$product->getId(), self::$linkTypes, $product);
+ return new ListCriteria((string)$product->getId(), $this->linkTypes, $product);
}
/**
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php
index 9ddad4e6451fa..3139c35774008 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php
@@ -7,6 +7,7 @@
namespace Magento\CatalogGraphQl\Model\Resolver\Product;
+use GraphQL\Language\AST\NodeKind;
use Magento\Framework\GraphQl\Query\FieldTranslator;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
@@ -43,9 +44,9 @@ public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeN
continue;
}
foreach ($node->selectionSet->selections as $selectionNode) {
- if ($selectionNode->kind === 'InlineFragment') {
+ if ($selectionNode->kind === NodeKind::INLINE_FRAGMENT) {
foreach ($selectionNode->selectionSet->selections as $inlineSelection) {
- if ($inlineSelection->kind === 'InlineFragment') {
+ if ($inlineSelection->kind === NodeKind::INLINE_FRAGMENT) {
continue;
}
$fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value);
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php
index fc5a563c82b4e..c553d4486f9e9 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php
@@ -8,15 +8,16 @@
namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider;
use GraphQL\Language\AST\FieldNode;
-use Magento\CatalogGraphQl\Model\Category\DepthCalculator;
-use Magento\CatalogGraphQl\Model\Category\LevelCalculator;
-use Magento\Framework\EntityManager\MetadataPool;
-use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
+use GraphQL\Language\AST\NodeKind;
use Magento\Catalog\Api\Data\CategoryInterface;
+use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\ResourceModel\Category\Collection;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
use Magento\CatalogGraphQl\Model\AttributesJoiner;
-use Magento\Catalog\Model\Category;
+use Magento\CatalogGraphQl\Model\Category\DepthCalculator;
+use Magento\CatalogGraphQl\Model\Category\LevelCalculator;
+use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* Category tree data provider
@@ -85,8 +86,8 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato
{
$categoryQuery = $resolveInfo->fieldNodes[0];
$collection = $this->collectionFactory->create();
- $this->joinAttributesRecursively($collection, $categoryQuery);
- $depth = $this->depthCalculator->calculate($categoryQuery);
+ $this->joinAttributesRecursively($collection, $categoryQuery, $resolveInfo);
+ $depth = $this->depthCalculator->calculate($resolveInfo, $categoryQuery);
$level = $this->levelCalculator->calculate($rootCategoryId);
// If root category is being filter, we've to remove first slash
@@ -124,24 +125,27 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato
*
* @param Collection $collection
* @param FieldNode $fieldNode
+ * @param ResolveInfo $resolveInfo
* @return void
*/
- private function joinAttributesRecursively(Collection $collection, FieldNode $fieldNode) : void
- {
+ private function joinAttributesRecursively(
+ Collection $collection,
+ FieldNode $fieldNode,
+ ResolveInfo $resolveInfo
+ ): void {
if (!isset($fieldNode->selectionSet->selections)) {
return;
}
$subSelection = $fieldNode->selectionSet->selections;
- $this->attributesJoiner->join($fieldNode, $collection);
+ $this->attributesJoiner->join($fieldNode, $collection, $resolveInfo);
/** @var FieldNode $node */
foreach ($subSelection as $node) {
- if ($node->kind === 'InlineFragment') {
+ if ($node->kind === NodeKind::INLINE_FRAGMENT || $node->kind === NodeKind::FRAGMENT_SPREAD) {
continue;
}
-
- $this->joinAttributesRecursively($collection, $node);
+ $this->joinAttributesRecursively($collection, $node, $resolveInfo);
}
}
}
diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml
index 5fec7bfd4fda7..03f9d7ad03f04 100644
--- a/app/code/Magento/CatalogGraphQl/etc/di.xml
+++ b/app/code/Magento/CatalogGraphQl/etc/di.xml
@@ -74,4 +74,14 @@
+
+
+
+
+ - related
+ - upsell
+ - crosssell
+
+
+
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
index c5fcac99767bd..189bfa61f2c42 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
@@ -1595,6 +1595,7 @@ protected function _saveProducts()
}
$rowSku = $rowData[self::COL_SKU];
+ $rowSkuNormalized = mb_strtolower($rowSku);
if (null === $rowSku) {
$this->getErrorAggregator()->addRowToSkip($rowNum);
@@ -1604,9 +1605,9 @@ protected function _saveProducts()
$storeId = !empty($rowData[self::COL_STORE])
? $this->getStoreIdByCode($rowData[self::COL_STORE])
: Store::DEFAULT_STORE_ID;
- $rowExistingImages = $existingImages[$storeId][$rowSku] ?? [];
+ $rowExistingImages = $existingImages[$storeId][$rowSkuNormalized] ?? [];
$rowStoreMediaGalleryValues = $rowExistingImages;
- $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSku] ?? [];
+ $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSkuNormalized] ?? [];
if (self::SCOPE_STORE == $rowScope) {
// set necessary data from SCOPE_DEFAULT row
@@ -1762,10 +1763,11 @@ protected function _saveProducts()
continue;
}
- if (isset($rowExistingImages[$uploadedFile])) {
- $currentFileData = $rowExistingImages[$uploadedFile];
+ $uploadedFileNormalized = ltrim($uploadedFile, '/\\');
+ if (isset($rowExistingImages[$uploadedFileNormalized])) {
+ $currentFileData = $rowExistingImages[$uploadedFileNormalized];
$currentFileData['store_id'] = $storeId;
- $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFile]);
+ $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFileNormalized]);
if (array_key_exists($uploadedFile, $imageHiddenStates)
&& $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile]
) {
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php
index a45338c391a58..78ff26675930e 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php
@@ -89,7 +89,6 @@ public function saveLinks(
$resource = $this->linkFactory->create();
$mainTable = $resource->getMainTable();
$positionAttrId = [];
- $nextLinkId = $this->resourceHelper->getNextAutoincrement($mainTable);
// pre-load 'position' attributes ID for each link type once
foreach ($this->linkNameToId as $linkId) {
@@ -103,6 +102,7 @@ public function saveLinks(
$positionAttrId[$linkId] = $importEntity->getConnection()->fetchOne($select, $bind);
}
while ($bunch = $dataSourceModel->getNextBunch()) {
+ $nextLinkId = $this->resourceHelper->getNextAutoincrement($mainTable);
$this->processLinkBunches($importEntity, $linkField, $bunch, $resource, $nextLinkId, $positionAttrId);
}
}
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php
index a94a87a44b32a..d4694b72ba64f 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php
@@ -384,7 +384,9 @@ public function getExistingImages(array $bunch)
foreach ($this->connection->fetchAll($select) as $image) {
$storeId = $image['store_id'];
unset($image['store_id']);
- $result[$storeId][$image['sku']][$image['value']] = $image;
+ $sku = mb_strtolower($image['sku']);
+ $value = ltrim($image['value'], '/\\');
+ $result[$storeId][$sku][$value] = $image;
}
return $result;
diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml
index b050c6ae3b6ca..751fa465bdb17 100644
--- a/app/code/Magento/CatalogInventory/etc/di.xml
+++ b/app/code/Magento/CatalogInventory/etc/di.xml
@@ -37,7 +37,7 @@
-
+
- quantity_and_stock_status
diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php
index 332bb991bf29f..b2aaa054ebc34 100644
--- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php
+++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php
@@ -176,15 +176,16 @@ public function getCurrencyRate()
*
* @param float|string $fromPrice
* @param float|string $toPrice
+ * @param boolean $isLast
* @return float|\Magento\Framework\Phrase
*/
- protected function _renderRangeLabel($fromPrice, $toPrice)
+ protected function _renderRangeLabel($fromPrice, $toPrice, $isLast = false)
{
$fromPrice = empty($fromPrice) ? 0 : $fromPrice * $this->getCurrencyRate();
$toPrice = empty($toPrice) ? $toPrice : $toPrice * $this->getCurrencyRate();
$formattedFromPrice = $this->priceCurrency->format($fromPrice);
- if ($toPrice === '') {
+ if ($isLast) {
return __('%1 and above', $formattedFromPrice);
} elseif ($fromPrice == $toPrice && $this->dataProvider->getOnePriceIntervalValue()) {
return $formattedFromPrice;
@@ -215,12 +216,15 @@ protected function _getItemsData()
$data = [];
if (count($facets) > 1) { // two range minimum
+ $lastFacet = array_key_last($facets);
foreach ($facets as $key => $aggregation) {
$count = $aggregation['count'];
if (strpos($key, '_') === false) {
continue;
}
- $data[] = $this->prepareData($key, $count, $data);
+
+ $isLast = $lastFacet === $key;
+ $data[] = $this->prepareData($key, $count, $isLast);
}
}
@@ -264,18 +268,13 @@ protected function getFrom($from)
*
* @param string $key
* @param int $count
+ * @param boolean $isLast
* @return array
*/
- private function prepareData($key, $count)
+ private function prepareData($key, $count, $isLast = false)
{
- list($from, $to) = explode('_', $key);
- if ($from == '*') {
- $from = $this->getFrom($to);
- }
- if ($to == '*') {
- $to = $this->getTo($to);
- }
- $label = $this->_renderRangeLabel($from, $to);
+ [$from, $to] = explode('_', $key);
+ $label = $this->_renderRangeLabel($from, $to, $isLast);
$value = $from . '-' . $to . $this->dataProvider->getAdditionalRequestData();
$data = [
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml
index c3f3865ef4549..c81540382c86f 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml
@@ -16,7 +16,8 @@
-
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml
index e2d4fd2e89c2f..daa27b9918e47 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml
index e82f3c0588835..4185261993ffd 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml
@@ -9,6 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
+
@@ -18,6 +19,7 @@
+
@@ -46,19 +48,23 @@
-
-
+
+
+
+
+
+
-
+
@@ -93,8 +99,8 @@
-
-
+
+
@@ -107,13 +113,13 @@
-
+
-
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml
index 6f16fa54a6ebf..ebf024490cce6 100644
--- a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml
index 112335e726270..a6f4e7780d096 100644
--- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml
@@ -9,7 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
-
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml
index 1869a6544c3d3..5be91f61e1e1e 100644
--- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml
@@ -38,6 +38,8 @@
+
+
diff --git a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php
index 257f8ba9bfe53..9e360481e8eb3 100644
--- a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php
+++ b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php
@@ -7,6 +7,7 @@
namespace Magento\CmsUrlRewrite\Plugin\Cms\Model\Store;
+use Magento\Cms\Api\Data\PageInterface;
use Magento\Cms\Api\PageRepositoryInterface;
use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator;
use Magento\Framework\Api\SearchCriteriaBuilder;
@@ -21,6 +22,8 @@
*/
class View
{
+ private const ALL_STORE_VIEWS = '0';
+
/**
* @var UrlPersistInterface
*/
@@ -89,9 +92,8 @@ private function generateCmsPagesUrls(int $storeId): array
{
$rewrites = [];
$urls = [];
- $searchCriteria = $this->searchCriteriaBuilder->create();
- $cmsPagesCollection = $this->pageRepository->getList($searchCriteria)->getItems();
- foreach ($cmsPagesCollection as $page) {
+
+ foreach ($this->getCmsPageItems() as $page) {
$page->setStoreId($storeId);
$rewrites[] = $this->cmsPageUrlRewriteGenerator->generate($page);
}
@@ -99,4 +101,18 @@ private function generateCmsPagesUrls(int $storeId): array
return $urls;
}
+
+ /**
+ * Return cms page items for all store view
+ *
+ * @return PageInterface[]
+ */
+ private function getCmsPageItems(): array
+ {
+ $searchCriteria = $this->searchCriteriaBuilder->addFilter('store_id', self::ALL_STORE_VIEWS)
+ ->create();
+ $list = $this->pageRepository->getList($searchCriteria);
+
+ return $list->getItems();
+ }
}
diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php
index 356c6ca17da18..f61e99529c3cc 100644
--- a/app/code/Magento/Config/Model/Config.php
+++ b/app/code/Magento/Config/Model/Config.php
@@ -208,6 +208,7 @@ public function save()
);
$groupChangedPaths = $this->getChangedPaths($sectionId, $groupId, $groupData, $oldConfig, $extraOldGroups);
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
$changedPaths = \array_merge($changedPaths, $groupChangedPaths);
}
@@ -370,6 +371,7 @@ private function getChangedPaths(
$oldConfig,
$extraOldGroups
);
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
$changedPaths = \array_merge($changedPaths, $subGroupChangedPaths);
}
}
@@ -435,11 +437,11 @@ protected function _processGroup(
if (!isset($fieldData['value'])) {
$fieldData['value'] = null;
}
-
+
if ($field->getType() == 'multiline' && is_array($fieldData['value'])) {
$fieldData['value'] = trim(implode(PHP_EOL, $fieldData['value']));
}
-
+
$data = [
'field' => $fieldId,
'groups' => $groups,
@@ -453,7 +455,7 @@ protected function _processGroup(
$backendModel->addData($data);
$this->_checkSingleStoreMode($field, $backendModel);
- $path = $this->getFieldPath($field, $fieldId, $extraOldGroups, $oldConfig);
+ $path = $this->getFieldPath($field, $fieldId, $oldConfig, $extraOldGroups);
$backendModel->setPath($path)->setValue($fieldData['value']);
$inherit = !empty($fieldData['inherit']);
diff --git a/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php b/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php
new file mode 100644
index 0000000000000..fbc45a9cfc791
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php
@@ -0,0 +1,42 @@
+authorization = $authorization;
+ }
+
+ /**
+ * Check that user is allowed to manage attributes
+ *
+ * @return bool
+ */
+ public function isAllowedToManageAttributes(): bool
+ {
+ return $this->authorization->isAllowed('Magento_Catalog::attributes_attributes');
+ }
+}
diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php
index 1555e88700a45..2f333e7ca6f6e 100644
--- a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php
+++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php
@@ -4,11 +4,21 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\ConfigurableProduct\Plugin\Model\ResourceModel;
+use Magento\Catalog\Api\Data\ProductAttributeInterface;
+use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
+use Magento\ConfigurableProduct\Api\Data\OptionInterface;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
+use Magento\Framework\Api\FilterBuilder;
+use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Indexer\ActionInterface;
+/**
+ * Plugin product resource model
+ */
class Product
{
/**
@@ -21,18 +31,45 @@ class Product
*/
private $productIndexer;
+ /**
+ * @var ProductAttributeRepositoryInterface
+ */
+ private $productAttributeRepository;
+
+ /**
+ * @var SearchCriteriaBuilder
+ */
+ private $searchCriteriaBuilder;
+
+ /**
+ * @var FilterBuilder
+ */
+ private $filterBuilder;
+
/**
* Initialize Product dependencies.
*
* @param Configurable $configurable
* @param ActionInterface $productIndexer
+ * @param ProductAttributeRepositoryInterface $productAttributeRepository
+ * @param SearchCriteriaBuilder $searchCriteriaBuilder
+ * @param FilterBuilder $filterBuilder
*/
public function __construct(
Configurable $configurable,
- ActionInterface $productIndexer
+ ActionInterface $productIndexer,
+ ProductAttributeRepositoryInterface $productAttributeRepository = null,
+ SearchCriteriaBuilder $searchCriteriaBuilder = null,
+ FilterBuilder $filterBuilder = null
) {
$this->configurable = $configurable;
$this->productIndexer = $productIndexer;
+ $this->productAttributeRepository = $productAttributeRepository ?: ObjectManager::getInstance()
+ ->get(ProductAttributeRepositoryInterface::class);
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance()
+ ->get(SearchCriteriaBuilder::class);
+ $this->filterBuilder = $filterBuilder ?: ObjectManager::getInstance()
+ ->get(FilterBuilder::class);
}
/**
@@ -41,6 +78,7 @@ public function __construct(
* @param \Magento\Catalog\Model\ResourceModel\Product $subject
* @param \Magento\Framework\DataObject $object
* @return void
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
@@ -51,6 +89,39 @@ public function beforeSave(
/** @var \Magento\Catalog\Model\Product $object */
if ($object->getTypeId() == Configurable::TYPE_CODE) {
$object->getTypeInstance()->getSetAttributes($object);
+ $this->resetConfigurableOptionsData($object);
+ }
+ }
+
+ /**
+ * Set null for configurable options attribute of configurable product
+ *
+ * @param \Magento\Catalog\Model\Product $object
+ * @return void
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function resetConfigurableOptionsData($object)
+ {
+ $extensionAttribute = $object->getExtensionAttributes();
+ if ($extensionAttribute && $extensionAttribute->getConfigurableProductOptions()) {
+ $attributeIds = [];
+ /** @var OptionInterface $option */
+ foreach ($extensionAttribute->getConfigurableProductOptions() as $option) {
+ $attributeIds[] = $option->getAttributeId();
+ }
+
+ $filter = $this->filterBuilder
+ ->setField(ProductAttributeInterface::ATTRIBUTE_ID)
+ ->setConditionType('in')
+ ->setValue($attributeIds)
+ ->create();
+ $this->searchCriteriaBuilder->addFilters([$filter]);
+ $searchCriteria = $this->searchCriteriaBuilder->create();
+ $optionAttributes = $this->productAttributeRepository->getList($searchCriteria)->getItems();
+
+ foreach ($optionAttributes as $optionAttribute) {
+ $object->setData($optionAttribute->getAttributeCode(), null);
+ }
}
}
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml
new file mode 100644
index 0000000000000..c48f22a3656d5
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ Adds 3 provided Options to a new Attribute on the Configurable Product creation/edit page. Selected default first option. Set "Use in Layered Navigation" to "Yes".
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml
new file mode 100644
index 0000000000000..cc709b80efebb
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Goes to the Admin Product grid page. Fill basic value for Configurable Product using the default Product Options.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml
new file mode 100644
index 0000000000000..969a41e27d459
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Goes to the select values page from each attribute to include in the product.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml
new file mode 100644
index 0000000000000..cc2ff9a63ae40
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Click to check option.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml
new file mode 100644
index 0000000000000..3cca319d9569c
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Set quantity 1 to all child skus for configurable product. Save a configurable product and confirm.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php
index abab103fa6d37..3d5a0d1cc6a3f 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php
@@ -7,20 +7,38 @@
namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Model\ResourceModel;
+use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
+use Magento\Catalog\Model\Product as ModelProduct;
use Magento\Catalog\Model\Product\Type;
+use Magento\Catalog\Model\ProductAttributeSearchResults;
+use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute;
+use Magento\Catalog\Model\ResourceModel\Product as ResourceModelProduct;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
-use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product;
+use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute as ConfigurableAttribute;
+use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product as PluginResourceModelProduct;
+use Magento\Framework\Api\ExtensionAttributesInterface;
+use Magento\Framework\Api\FilterBuilder;
+use Magento\Framework\Api\SearchCriteria;
+use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Indexer\ActionInterface;
-use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class ProductTest extends TestCase
{
/**
- * @var ObjectManager
+ * @var PluginResourceModelProduct
*/
- private $objectManager;
+ private $model;
+
+ /**
+ * @var ObjectManagerHelper
+ */
+ private $objectManagerHelper;
/**
* @var Configurable|MockObject
@@ -33,39 +51,128 @@ class ProductTest extends TestCase
private $actionMock;
/**
- * @var Product
+ * @var ProductAttributeRepositoryInterface|MockObject
*/
- private $model;
+ private $productAttributeRepositoryMock;
+
+ /**
+ * @var SearchCriteriaBuilder|MockObject
+ */
+ private $searchCriteriaBuilderMock;
+
+ /**
+ * @var FilterBuilder|MockObject
+ */
+ private $filterBuilderMock;
protected function setUp(): void
{
- $this->objectManager = new ObjectManager($this);
$this->configurableMock = $this->createMock(Configurable::class);
$this->actionMock = $this->getMockForAbstractClass(ActionInterface::class);
-
- $this->model = $this->objectManager->getObject(
- Product::class,
+ $this->productAttributeRepositoryMock = $this->getMockBuilder(ProductAttributeRepositoryInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getList'])
+ ->getMockForAbstractClass();
+ $this->searchCriteriaBuilderMock = $this->createPartialMock(
+ SearchCriteriaBuilder::class,
+ ['addFilters', 'create']
+ );
+ $this->filterBuilderMock = $this->createPartialMock(
+ FilterBuilder::class,
+ ['setField', 'setConditionType', 'setValue', 'create']
+ );
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->model = $this->objectManagerHelper->getObject(
+ PluginResourceModelProduct::class,
[
'configurable' => $this->configurableMock,
'productIndexer' => $this->actionMock,
+ 'productAttributeRepository' => $this->productAttributeRepositoryMock,
+ 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock,
+ 'filterBuilder' => $this->filterBuilderMock
]
);
}
- public function testBeforeSaveConfigurable()
+ public function testBeforeSaveConfigurable(): void
{
- /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */
- $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
- /** @var \Magento\Catalog\Model\Product|MockObject $object */
- $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']);
+ /** @var ResourceModelProduct|MockObject $subject */
+ $subject = $this->createMock(ResourceModelProduct::class);
+ /** @var ModelProduct|MockObject $object */
+ $object = $this->createPartialMock(
+ ModelProduct::class,
+ [
+ 'getTypeId',
+ 'getTypeInstance',
+ 'getExtensionAttributes',
+ 'setData'
+ ]
+ );
$type = $this->createPartialMock(
Configurable::class,
['getSetAttributes']
);
- $type->expects($this->once())->method('getSetAttributes')->with($object);
-
- $object->expects($this->once())->method('getTypeId')->willReturn(Configurable::TYPE_CODE);
- $object->expects($this->once())->method('getTypeInstance')->willReturn($type);
+ $extensionAttributes = $this->getMockBuilder(ExtensionAttributesInterface::class)
+ ->disableOriginalConstructor()
+ ->addMethods(['getConfigurableProductOptions'])
+ ->getMock();
+ $option = $this->createPartialMock(
+ ConfigurableAttribute::class,
+ ['getAttributeId']
+ );
+ $extensionAttributes->expects($this->exactly(2))
+ ->method('getConfigurableProductOptions')
+ ->willReturn([$option]);
+ $object->expects($this->once())
+ ->method('getExtensionAttributes')
+ ->willReturn($extensionAttributes);
+
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('setField')
+ ->willReturnSelf();
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('setValue')
+ ->willReturnSelf();
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('setConditionType')
+ ->willReturnSelf();
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('create')
+ ->willReturnSelf();
+ $searchCriteria = $this->createMock(SearchCriteria::class);
+ $this->searchCriteriaBuilderMock->expects($this->once())
+ ->method('create')
+ ->willReturn($searchCriteria);
+ $searchResultMockClass = $this->createPartialMock(
+ ProductAttributeSearchResults::class,
+ ['getItems']
+ );
+ $this->productAttributeRepositoryMock->expects($this->once())
+ ->method('getList')
+ ->with($searchCriteria)
+ ->willReturn($searchResultMockClass);
+ $optionAttribute = $this->createPartialMock(
+ EavAttribute::class,
+ ['getAttributeCode']
+ );
+ $searchResultMockClass->expects($this->once())
+ ->method('getItems')
+ ->willReturn([$optionAttribute]);
+ $type->expects($this->once())
+ ->method('getSetAttributes')
+ ->with($object);
+ $object->expects($this->once())
+ ->method('getTypeId')
+ ->will($this->returnValue(Configurable::TYPE_CODE));
+ $object->expects($this->once())
+ ->method('getTypeInstance')
+ ->will($this->returnValue($type));
+ $object->expects($this->once())
+ ->method('setData');
+ $option->expects($this->once())
+ ->method('getAttributeId');
+ $optionAttribute->expects($this->once())
+ ->method('getAttributeCode');
$this->model->beforeSave(
$subject,
@@ -73,14 +180,23 @@ public function testBeforeSaveConfigurable()
);
}
- public function testBeforeSaveSimple()
+ public function testBeforeSaveSimple(): void
{
- /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */
- $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
- /** @var \Magento\Catalog\Model\Product|MockObject $object */
- $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']);
- $object->expects($this->once())->method('getTypeId')->willReturn(Type::TYPE_SIMPLE);
- $object->expects($this->never())->method('getTypeInstance');
+ /** @var ResourceModelProduct|MockObject$subject */
+ $subject = $this->createMock(ResourceModelProduct::class);
+ /** @var ModelProduct|MockObject $object */
+ $object = $this->createPartialMock(
+ ModelProduct::class,
+ [
+ 'getTypeId',
+ 'getTypeInstance'
+ ]
+ );
+ $object->expects($this->once())
+ ->method('getTypeId')
+ ->will($this->returnValue(Type::TYPE_SIMPLE));
+ $object->expects($this->never())
+ ->method('getTypeInstance');
$this->model->beforeSave(
$subject,
@@ -88,29 +204,35 @@ public function testBeforeSaveSimple()
);
}
- public function testAroundDelete()
+ public function testAroundDelete(): void
{
$productId = '1';
$parentConfigId = ['2'];
- /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */
- $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
- /** @var \Magento\Catalog\Model\Product|MockObject $product */
+ /** @var ResourceModelProduct|MockObject $subject */
+ $subject = $this->createMock(ResourceModelProduct::class);
+ /** @var ModelProduct|MockObject $product */
$product = $this->createPartialMock(
- \Magento\Catalog\Model\Product::class,
+ ModelProduct::class,
['getId', 'delete']
);
- $product->expects($this->once())->method('getId')->willReturn($productId);
- $product->expects($this->once())->method('delete')->willReturn(true);
+ $product->expects($this->once())
+ ->method('getId')
+ ->willReturn($productId);
+ $product->expects($this->once())
+ ->method('delete')
+ ->willReturn(true);
$this->configurableMock->expects($this->once())
->method('getParentIdsByChild')
->with($productId)
->willReturn($parentConfigId);
- $this->actionMock->expects($this->once())->method('executeList')->with($parentConfigId);
+ $this->actionMock->expects($this->once())
+ ->method('executeList')
+ ->with($parentConfigId);
$return = $this->model->aroundDelete(
$subject,
- /** @var \Magento\Catalog\Model\Product|MockObject $prod */
- function (\Magento\Catalog\Model\Product $prod) use ($subject) {
+ /** @var ModelProduct|MockObject $prod */
+ function (ModelProduct $prod) use ($subject) {
$prod->delete();
return $subject;
},
diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml b/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml
index a084abfc31eaa..ffd17a8bf4734 100644
--- a/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml
+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml
@@ -48,6 +48,7 @@
- configurableModal
- productFormConfigurable
+ Magento\ConfigurableProduct\Block\DataProviders\PermissionsData
diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml
index e996df8260719..e94d94e0ded55 100644
--- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml
+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml
@@ -5,6 +5,9 @@
*/
/* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\AttributeValues */
+$isAllowedToManageAttributes = $block->getPermissions()->isAllowedToManageAttributes();
+$attributesUrl = $block->getUrl('catalog/product_attribute/getAttributes');
+$optionsUrl = $block->getUrl('catalog/product_attribute/createOptions');
?>
= $block->escapeHtml(
@@ -12,7 +15,8 @@
); ?>
= $block->escapeHtml(
- __('Select values from each attribute to include in this product. Each unique combination of values creates a unique product SKU.')
+ __('Select values from each attribute to include in this product. ' .
+ 'Each unique combination of values creates a unique product SKU.')
);?>
@@ -72,7 +76,8 @@
-
@@ -120,8 +125,8 @@
"= /* @noEscape */ $block->getComponentName() ?>": {
"component": "Magento_ConfigurableProduct/js/variations/steps/attributes_values",
"appendTo": "= /* @noEscape */ $block->getParentComponentName() ?>",
- "optionsUrl": "= /* @noEscape */ $block->getUrl('catalog/product_attribute/getAttributes') ?>",
- "createOptionsUrl": "= /* @noEscape */ $block->getUrl('catalog/product_attribute/createOptions') ?>"
+ "optionsUrl": "= /* @noEscape */ $attributesUrl ?>",
+ "createOptionsUrl": "= /* @noEscape */ $optionsUrl ?>"
}
}
}
diff --git a/app/code/Magento/Contact/view/frontend/templates/form.phtml b/app/code/Magento/Contact/view/frontend/templates/form.phtml
index d218e650657ac..eee9f742a59a4 100644
--- a/app/code/Magento/Contact/view/frontend/templates/form.phtml
+++ b/app/code/Magento/Contact/view/frontend/templates/form.phtml
@@ -4,6 +4,9 @@
* See COPYING.txt for license details.
*/
+// phpcs:disable Magento2.Templates.ThisInTemplate
+// phpcs:disable Generic.Files.LineLength.TooLong
+
/** @var \Magento\Contact\Block\ContactForm $block */
/** @var \Magento\Contact\ViewModel\UserDataProvider $viewModel */
@@ -23,35 +26,35 @@ $viewModel = $block->getViewModel();
@@ -60,12 +63,12 @@ $viewModel = $block->getViewModel();
= $block->escapeHtml(__('What’s on your mind?')) ?>
-
@@ -81,3 +84,12 @@ $viewModel = $block->getViewModel();
+
diff --git a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php
index 2be340c8ccca4..b4c737f6600bf 100644
--- a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php
+++ b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php
@@ -52,7 +52,7 @@ public function getFrontendLabel(string $attributeCode): string
{
try {
$attribute = $this->addressMetadata->getAttributeMetadata($attributeCode);
- $frontendLabel = $attribute->getFrontendLabel();
+ $frontendLabel = $attribute->getStoreLabel() ?: $attribute->getFrontendLabel();
} catch (NoSuchEntityException $e) {
$frontendLabel = '';
}
diff --git a/app/code/Magento/Customer/Model/CustomerRegistry.php b/app/code/Magento/Customer/Model/CustomerRegistry.php
index d68904f6d1645..f2868132790cf 100644
--- a/app/code/Magento/Customer/Model/CustomerRegistry.php
+++ b/app/code/Magento/Customer/Model/CustomerRegistry.php
@@ -101,8 +101,10 @@ public function retrieve($customerId)
public function retrieveByEmail($customerEmail, $websiteId = null)
{
if ($websiteId === null) {
- $websiteId = $this->storeManager->getStore()->getWebsiteId();
+ $websiteId = $this->storeManager->getStore()->getWebsiteId()
+ ?: $this->storeManager->getDefaultStoreView()->getWebsiteId();
}
+
$emailKey = $this->getEmailKey($customerEmail, $websiteId);
if (isset($this->customerRegistryByEmail[$emailKey])) {
return $this->customerRegistryByEmail[$emailKey];
diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php
index 9eb9ffb806c9f..b877b2cca67a5 100644
--- a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php
+++ b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php
@@ -7,7 +7,10 @@
namespace Magento\Customer\Model\Plugin;
use Magento\Authorization\Model\UserContextInterface;
+use Magento\Customer\Model\CustomerFactory;
+use Magento\Customer\Model\ResourceModel\Customer as CustomerResource;
use Magento\Integration\Api\AuthorizationServiceInterface as AuthorizationService;
+use Magento\Store\Model\StoreManagerInterface;
/**
* Plugin around \Magento\Framework\Authorization::isAllowed
@@ -19,16 +22,41 @@ class CustomerAuthorization
/**
* @var UserContextInterface
*/
- protected $userContext;
+ private $userContext;
+
+ /**
+ * @var CustomerFactory
+ */
+ private $customerFactory;
+
+ /**
+ * @var CustomerResource
+ */
+ private $customerResource;
+
+ /**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
/**
* Inject dependencies.
*
* @param UserContextInterface $userContext
+ * @param CustomerFactory $customerFactory
+ * @param CustomerResource $customerResource
+ * @param StoreManagerInterface $storeManager
*/
- public function __construct(UserContextInterface $userContext)
- {
+ public function __construct(
+ UserContextInterface $userContext,
+ CustomerFactory $customerFactory,
+ CustomerResource $customerResource,
+ StoreManagerInterface $storeManager
+ ) {
$this->userContext = $userContext;
+ $this->customerFactory = $customerFactory;
+ $this->customerResource = $customerResource;
+ $this->storeManager = $storeManager;
}
/**
@@ -53,9 +81,15 @@ public function aroundIsAllowed(
&& $this->userContext->getUserId()
&& $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER
) {
- return true;
- } else {
- return $proceed($resource, $privilege);
+ $customer = $this->customerFactory->create();
+ $this->customerResource->load($customer, $this->userContext->getUserId());
+ $currentStoreId = $this->storeManager->getStore()->getId();
+ $sharedStoreIds = $customer->getSharedStoreIds();
+ if (in_array($currentStoreId, $sharedStoreIds)) {
+ return true;
+ }
}
+
+ return $proceed($resource, $privilege);
}
}
diff --git a/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php b/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php
new file mode 100644
index 0000000000000..fdde31e05fb2e
--- /dev/null
+++ b/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php
@@ -0,0 +1,81 @@
+request = $request;
+ }
+
+ /**
+ * Update customer by id from request if exist
+ *
+ * @param CustomerRepositoryInterface $customerRepository
+ * @param CustomerInterface $customer
+ * @param string|null $passwordHash
+ * @return array
+ */
+ public function beforeSave(
+ CustomerRepositoryInterface $customerRepository,
+ CustomerInterface $customer,
+ ?string $passwordHash = null
+ ): array {
+ $customerId = $this->request->getParam('customerId');
+
+ if ($customerId) {
+ $customer = $this->getUpdatedCustomer($customerRepository->getById($customerId), $customer);
+ }
+
+ return [$customer, $passwordHash];
+ }
+
+ /**
+ * Return updated customer
+ *
+ * @param CustomerInterface $originCustomer
+ * @param CustomerInterface $customer
+ * @return CustomerInterface
+ */
+ private function getUpdatedCustomer(
+ CustomerInterface $originCustomer,
+ CustomerInterface $customer
+ ): CustomerInterface {
+ $newCustomer = clone $originCustomer;
+ foreach ($customer->__toArray() as $name => $value) {
+ if ($name === CustomerInterface::CUSTOM_ATTRIBUTES) {
+ $value = $customer->getCustomAttributes();
+ } elseif ($name === CustomerInterface::EXTENSION_ATTRIBUTES_KEY) {
+ $value = $customer->getExtensionAttributes();
+ } elseif ($name === CustomerInterface::KEY_ADDRESSES) {
+ $value = $customer->getAddresses();
+ }
+
+ $newCustomer->setData($name, $value);
+ }
+
+ return $newCustomer;
+ }
+}
diff --git a/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php
new file mode 100644
index 0000000000000..c2b7189b808a3
--- /dev/null
+++ b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php
@@ -0,0 +1,78 @@
+orderRepository = $orderRepository;
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ }
+
+ /**
+ * Upgrade order customer email when customer has changed email
+ *
+ * @param Observer $observer
+ * @return void
+ */
+ public function execute(Observer $observer): void
+ {
+ /** @var Customer $originalCustomer */
+ $originalCustomer = $observer->getEvent()->getOrigCustomerDataObject();
+ if (!$originalCustomer) {
+ return;
+ }
+
+ /** @var Customer $customer */
+ $customer = $observer->getEvent()->getCustomerDataObject();
+ $customerEmail = $customer->getEmail();
+
+ if ($customerEmail === $originalCustomer->getEmail()) {
+ return;
+ }
+ $searchCriteria = $this->searchCriteriaBuilder
+ ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId())
+ ->create();
+
+ /**
+ * @var Collection $orders
+ */
+ $orders = $this->orderRepository->getList($searchCriteria);
+ $orders->setDataToAll(OrderInterface::CUSTOMER_EMAIL, $customerEmail);
+ $orders->save();
+ }
+}
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml
new file mode 100644
index 0000000000000..b827cba8490b8
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml
new file mode 100644
index 0000000000000..bbdc4de330840
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml
new file mode 100644
index 0000000000000..66b464006aa0f
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml
new file mode 100644
index 0000000000000..16688be61171e
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml
new file mode 100644
index 0000000000000..f287c728bb28d
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ Create Order via API assigned to Customer.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml
new file mode 100644
index 0000000000000..5dafe59bf3c48
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml
new file mode 100644
index 0000000000000..39a67968c66e4
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml
index ec5141d84b1bd..61ce050aa3ef2 100644
--- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml
@@ -17,5 +17,9 @@
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml
new file mode 100644
index 0000000000000..ba113c739d706
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php
new file mode 100644
index 0000000000000..d05c10c00e6c3
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php
@@ -0,0 +1,222 @@
+orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class)
+ ->getMock();
+
+ $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->eventMock = $this->getMockBuilder(Event::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getCustomerDataObject', 'getOrigCustomerDataObject'])
+ ->getMock();
+
+ $this->observerMock = $this->getMockBuilder(Observer::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->observerMock->expects($this->any())->method('getEvent')->willReturn($this->eventMock);
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->orderCustomerEmailObserver = $this->objectManagerHelper->getObject(
+ UpgradeOrderCustomerEmailObserver::class,
+ [
+ 'orderRepository' => $this->orderRepositoryMock,
+ 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock,
+ ]
+ );
+ }
+
+ /**
+ * Verifying that the order email is not updated when the customer email is not updated
+ *
+ */
+ public function testUpgradeOrderCustomerEmailWhenMailIsNotChanged(): void
+ {
+ $customer = $this->createCustomerMock();
+ $originalCustomer = $this->createCustomerMock();
+
+ $this->setCustomerToEventMock($customer);
+ $this->setOriginalCustomerToEventMock($originalCustomer);
+
+ $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL);
+ $this->setCustomerEmail($customer, self::ORIGINAL_CUSTOMER_EMAIL);
+
+ $this->whenOrderRepositoryGetListIsNotCalled();
+
+ $this->orderCustomerEmailObserver->execute($this->observerMock);
+ }
+
+ /**
+ * Verifying that the order email is updated after the customer updates their email
+ *
+ */
+ public function testUpgradeOrderCustomerEmail(): void
+ {
+ $customer = $this->createCustomerMock();
+ $originalCustomer = $this->createCustomerMock();
+ $orderCollectionMock = $this->createOrderMock();
+
+ $this->setCustomerToEventMock($customer);
+ $this->setOriginalCustomerToEventMock($originalCustomer);
+
+ $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL);
+ $this->setCustomerEmail($customer, self::NEW_CUSTOMER_EMAIL);
+
+ $this->whenOrderRepositoryGetListIsCalled($orderCollectionMock);
+
+ $this->whenOrderCollectionSetDataToAllIsCalled($orderCollectionMock);
+
+ $this->whenOrderCollectionSaveIsCalled($orderCollectionMock);
+
+ $this->orderCustomerEmailObserver->execute($this->observerMock);
+ }
+
+ private function createCustomerMock(): MockObject
+ {
+ $customer = $this->getMockBuilder(CustomerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return $customer;
+ }
+
+ private function createOrderMock(): MockObject
+ {
+ $orderCollectionMock = $this->getMockBuilder(Collection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return $orderCollectionMock;
+ }
+
+ private function setCustomerToEventMock(MockObject $customer): void
+ {
+ $this->eventMock->expects($this->once())
+ ->method('getCustomerDataObject')
+ ->willReturn($customer);
+ }
+
+ private function setOriginalCustomerToEventMock(MockObject $originalCustomer): void
+ {
+ $this->eventMock->expects($this->once())
+ ->method('getOrigCustomerDataObject')
+ ->willReturn($originalCustomer);
+ }
+
+ private function setCustomerEmail(MockObject $originalCustomer, string $email): void
+ {
+ $originalCustomer->expects($this->once())
+ ->method('getEmail')
+ ->willReturn($email);
+ }
+
+ private function whenOrderRepositoryGetListIsCalled(MockObject $orderCollectionMock): void
+ {
+ $searchCriteriaMock = $this->getMockBuilder(SearchCriteria::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->searchCriteriaBuilderMock->expects($this->once())
+ ->method('create')
+ ->willReturn($searchCriteriaMock);
+
+ $this->searchCriteriaBuilderMock->expects($this->once())
+ ->method('addFilter')
+ ->willReturn($this->searchCriteriaBuilderMock);
+
+ $this->orderRepositoryMock->expects($this->once())
+ ->method('getList')
+ ->with($searchCriteriaMock)
+ ->willReturn($orderCollectionMock);
+ }
+
+ private function whenOrderCollectionSetDataToAllIsCalled(MockObject $orderCollectionMock): void
+ {
+ $orderCollectionMock->expects($this->once())
+ ->method('setDataToAll')
+ ->with(OrderInterface::CUSTOMER_EMAIL, self::NEW_CUSTOMER_EMAIL);
+ }
+
+ private function whenOrderCollectionSaveIsCalled(MockObject $orderCollectionMock): void
+ {
+ $orderCollectionMock->expects($this->once())
+ ->method('save');
+ }
+
+ private function whenOrderRepositoryGetListIsNotCalled(): void
+ {
+ $this->searchCriteriaBuilderMock->expects($this->never())
+ ->method('addFilter');
+ $this->searchCriteriaBuilderMock->expects($this->never())
+ ->method('create');
+
+ $this->orderRepositoryMock->expects($this->never())
+ ->method('getList');
+ }
+}
diff --git a/app/code/Magento/Customer/etc/events.xml b/app/code/Magento/Customer/etc/events.xml
index 2a724498a0359..0194f91c591f5 100644
--- a/app/code/Magento/Customer/etc/events.xml
+++ b/app/code/Magento/Customer/etc/events.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml
index f2457963a5f3d..426df2bbaa128 100644
--- a/app/code/Magento/Customer/etc/webapi_rest/di.xml
+++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml
@@ -19,4 +19,7 @@
+
+
+
diff --git a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml
index be201afa8f66c..caa501d48da83 100644
--- a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml
+++ b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml
@@ -6,6 +6,8 @@
* @var $block \Magento\Customer\Block\Account\Forgotpassword
*/
+// phpcs:disable Generic.Files.LineLength.TooLong
+
/** @var \Magento\Customer\Block\Account\Forgotpassword $block */
?>
+
diff --git a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml
index ef74b0062c023..a1d1a0260672a 100644
--- a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml
+++ b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml
@@ -4,6 +4,8 @@
* See COPYING.txt for license details.
*/
+// phpcs:disable Generic.Files.LineLength.TooLong
+
/** @var \Magento\Customer\Block\Form\Login $block */
?>
@@ -22,13 +24,22 @@
= $block->getChildHtml('form_additional_info') ?>
@@ -41,3 +52,12 @@
+
diff --git a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml
index f7d10f6df1728..e84861b9b5cf6 100644
--- a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml
+++ b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml
@@ -301,13 +301,6 @@ require([
ignore: ignore ? ':hidden:not(' + ignore + ')' : ':hidden'
}).find('input:text').attr('autocomplete', 'off');
- dataForm.submit(function () {
- $(this).find(':submit').attr('disabled', 'disabled');
- });
- dataForm.bind("invalid-form.validate", function () {
- $(this).find(':submit').prop('disabled', false);
- });
-
});
getShowAddressFields()): ?>
@@ -337,6 +330,11 @@ require([
"passwordStrengthIndicator": {
"formSelector": "form.form-create-account"
}
+ },
+ "*": {
+ "Magento_Customer/js/block-submit-on-send": {
+ "formId": "form-validate"
+ }
}
}
diff --git a/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js
new file mode 100644
index 0000000000000..b941ec7a254d8
--- /dev/null
+++ b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js
@@ -0,0 +1,22 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'jquery',
+ 'mage/mage'
+], function ($) {
+ 'use strict';
+
+ return function (config) {
+ var dataForm = $('#' + config.formId);
+
+ dataForm.submit(function () {
+ $(this).find(':submit').attr('disabled', 'disabled');
+ });
+ dataForm.bind('invalid-form.validate', function () {
+ $(this).find(':submit').prop('disabled', false);
+ });
+ };
+});
diff --git a/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php
new file mode 100644
index 0000000000000..ef3e86788c43f
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php
@@ -0,0 +1,24 @@
+getAllowedCustomerAttributes = $getAllowedCustomerAttributes;
$this->emailAddressValidator = $emailAddressValidator;
+ $this->validators = $validators;
}
/**
* Validate customer data
*
* @param array $customerData
- *
- * @return void
- *
* @throws GraphQlInputException
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
*/
- public function execute(array $customerData): void
+ public function execute(array $customerData)
{
- $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData));
- $errorInput = [];
-
- foreach ($attributes as $attributeInfo) {
- if ($attributeInfo->getIsRequired()
- && (!isset($customerData[$attributeInfo->getAttributeCode()])
- || $customerData[$attributeInfo->getAttributeCode()] == '')
- ) {
- $errorInput[] = $attributeInfo->getDefaultFrontendLabel();
- }
- }
-
- if ($errorInput) {
- throw new GraphQlInputException(
- __('Required parameters are missing: %1', [implode(', ', $errorInput)])
- );
- }
-
- if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) {
- throw new GraphQlInputException(
- __('"%1" is not a valid email address.', $customerData['email'])
- );
+ /** @var ValidateCustomerDataInterface $validator */
+ foreach ($this->validators as $validator) {
+ $validator->execute($customerData);
}
}
}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php
new file mode 100644
index 0000000000000..87f8831550f04
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php
@@ -0,0 +1,45 @@
+emailAddressValidator = $emailAddressValidator;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(array $customerData): void
+ {
+ if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) {
+ throw new GraphQlInputException(
+ __('"%1" is not a valid email address.', $customerData['email'])
+ );
+ }
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php
new file mode 100644
index 0000000000000..463372a63d8d5
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php
@@ -0,0 +1,58 @@
+customerMetadata = $customerMetadata;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(array $customerData): void
+ {
+ if (isset($customerData['gender']) && $customerData['gender']) {
+ /** @var AttributeMetadata $genderData */
+ $options = $this->customerMetadata->getAttributeMetadata('gender')->getOptions();
+
+ $isValid = false;
+ foreach ($options as $optionData) {
+ if ($optionData->getValue() && $optionData->getValue() == $customerData['gender']) {
+ $isValid = true;
+ }
+ }
+
+ if (!$isValid) {
+ throw new GraphQlInputException(
+ __('"%1" is not a valid gender value.', $customerData['gender'])
+ );
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php
new file mode 100644
index 0000000000000..fdf4fa1c824f2
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php
@@ -0,0 +1,59 @@
+getAllowedCustomerAttributes = $getAllowedCustomerAttributes;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(array $customerData): void
+ {
+ $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData));
+ $errorInput = [];
+
+ foreach ($attributes as $attributeInfo) {
+ if ($attributeInfo->getIsRequired()
+ && (!isset($customerData[$attributeInfo->getAttributeCode()])
+ || $customerData[$attributeInfo->getAttributeCode()] == '')
+ ) {
+ $errorInput[] = $attributeInfo->getDefaultFrontendLabel();
+ }
+ }
+
+ if ($errorInput) {
+ throw new GraphQlInputException(
+ __('Required parameters are missing: %1', [implode(', ', $errorInput)])
+ );
+ }
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml
index 1ba0e457430e0..3ed77a2ad563c 100644
--- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml
@@ -29,4 +29,14 @@
+
+
+
+
+ - Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateRequiredArguments
+ - Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateEmail
+ - Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateGender
+
+
+
diff --git a/app/code/Magento/Developer/Console/Command/patch_template.php.dist b/app/code/Magento/Developer/Console/Command/patch_template.php.dist
index f4fc25abcb29a..8e14b24bdc933 100644
--- a/app/code/Magento/Developer/Console/Command/patch_template.php.dist
+++ b/app/code/Magento/Developer/Console/Command/patch_template.php.dist
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace %moduleName%\Setup\Patch\%patchType%;
@@ -36,7 +37,7 @@ class %class% implements %implementsInterfaces%
}
%revertFunction%
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getAliases()
{
@@ -44,12 +45,10 @@ class %class% implements %implementsInterfaces%
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public static function getDependencies()
{
- return [
-
- ];
+ return [];
}
}
diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php
index ba98524bb665e..fc659c773c0af 100644
--- a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php
+++ b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php
@@ -21,11 +21,6 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug
*/
private $state;
- /**
- * @var ScopeConfigInterface
- */
- private $scopeConfig;
-
/**
* @var DeploymentConfig
*/
@@ -34,7 +29,6 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug
/**
* @param DriverInterface $filesystem
* @param State $state
- * @param ScopeConfigInterface $scopeConfig
* @param DeploymentConfig $deploymentConfig
* @param string $filePath
* @throws \Exception
@@ -42,14 +36,12 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug
public function __construct(
DriverInterface $filesystem,
State $state,
- ScopeConfigInterface $scopeConfig,
DeploymentConfig $deploymentConfig,
$filePath = null
) {
parent::__construct($filesystem, $filePath);
$this->state = $state;
- $this->scopeConfig = $scopeConfig;
$this->deploymentConfig = $deploymentConfig;
}
diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php
index 3f5ff58640313..c6ee70fb9ce40 100644
--- a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php
+++ b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php
@@ -29,13 +29,10 @@ class Syslog extends \Magento\Framework\Logger\Handler\Syslog
private $deploymentConfig;
/**
- * @param ScopeConfigInterface $scopeConfig Scope config
* @param DeploymentConfig $deploymentConfig Deployment config
* @param string $ident The string ident to be added to each message
- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
- ScopeConfigInterface $scopeConfig,
DeploymentConfig $deploymentConfig,
string $ident
) {
diff --git a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php
index 8bb0b1f176313..5e824e43764de 100644
--- a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php
+++ b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php
@@ -44,7 +44,6 @@ protected function setUp(): void
$this->deploymentConfigMock = $this->createMock(DeploymentConfig::class);
$this->model = new Syslog(
- $this->scopeConfigMock,
$this->deploymentConfigMock,
'Magento'
);
diff --git a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php
index c0bc825a8285b..c449f8f54872f 100644
--- a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php
+++ b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php
@@ -7,8 +7,9 @@
namespace Magento\Downloadable\Controller\Download;
-use Magento\Catalog\Model\Product\SalabilityChecker;
use Magento\Downloadable\Helper\Download as DownloadHelper;
+use Magento\Downloadable\Model\Link as LinkModel;
+use Magento\Downloadable\Model\RelatedProductRetriever;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\ResponseInterface;
@@ -20,20 +21,21 @@
class LinkSample extends \Magento\Downloadable\Controller\Download
{
/**
- * @var SalabilityChecker
+ * @var RelatedProductRetriever
*/
- private $salabilityChecker;
+ private $relatedProductRetriever;
/**
* @param Context $context
- * @param SalabilityChecker|null $salabilityChecker
+ * @param RelatedProductRetriever $relatedProductRetriever
*/
public function __construct(
Context $context,
- SalabilityChecker $salabilityChecker = null
+ RelatedProductRetriever $relatedProductRetriever
) {
parent::__construct($context);
- $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class);
+
+ $this->relatedProductRetriever = $relatedProductRetriever;
}
/**
@@ -44,9 +46,10 @@ public function __construct(
public function execute()
{
$linkId = $this->getRequest()->getParam('link_id', 0);
- /** @var \Magento\Downloadable\Model\Link $link */
- $link = $this->_objectManager->create(\Magento\Downloadable\Model\Link::class)->load($linkId);
- if ($link->getId() && $this->salabilityChecker->isSalable($link->getProductId())) {
+ /** @var LinkModel $link */
+ $link = $this->_objectManager->create(LinkModel::class);
+ $link->load($linkId);
+ if ($link->getId() && $this->isProductSalable($link)) {
$resource = '';
$resourceType = '';
if ($link->getSampleType() == DownloadHelper::LINK_TYPE_URL) {
@@ -74,4 +77,16 @@ public function execute()
return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl());
}
+
+ /**
+ * Check is related product salable.
+ *
+ * @param LinkModel $link
+ * @return bool
+ */
+ private function isProductSalable(LinkModel $link): bool
+ {
+ $product = $this->relatedProductRetriever->getProduct((int) $link->getProductId());
+ return $product ? $product->isSalable() : false;
+ }
}
diff --git a/app/code/Magento/Downloadable/Controller/Download/Sample.php b/app/code/Magento/Downloadable/Controller/Download/Sample.php
index b95ec510fdd9b..e2561092a7592 100644
--- a/app/code/Magento/Downloadable/Controller/Download/Sample.php
+++ b/app/code/Magento/Downloadable/Controller/Download/Sample.php
@@ -7,8 +7,9 @@
namespace Magento\Downloadable\Controller\Download;
-use Magento\Catalog\Model\Product\SalabilityChecker;
use Magento\Downloadable\Helper\Download as DownloadHelper;
+use Magento\Downloadable\Model\RelatedProductRetriever;
+use Magento\Downloadable\Model\Sample as SampleModel;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\ResponseInterface;
@@ -20,20 +21,21 @@
class Sample extends \Magento\Downloadable\Controller\Download
{
/**
- * @var SalabilityChecker
+ * @var RelatedProductRetriever
*/
- private $salabilityChecker;
+ private $relatedProductRetriever;
/**
* @param Context $context
- * @param SalabilityChecker|null $salabilityChecker
+ * @param RelatedProductRetriever $relatedProductRetriever
*/
public function __construct(
Context $context,
- SalabilityChecker $salabilityChecker = null
+ RelatedProductRetriever $relatedProductRetriever
) {
parent::__construct($context);
- $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class);
+
+ $this->relatedProductRetriever = $relatedProductRetriever;
}
/**
@@ -44,9 +46,10 @@ public function __construct(
public function execute()
{
$sampleId = $this->getRequest()->getParam('sample_id', 0);
- /** @var \Magento\Downloadable\Model\Sample $sample */
- $sample = $this->_objectManager->create(\Magento\Downloadable\Model\Sample::class)->load($sampleId);
- if ($sample->getId() && $this->salabilityChecker->isSalable($sample->getProductId())) {
+ /** @var SampleModel $sample */
+ $sample = $this->_objectManager->create(SampleModel::class);
+ $sample->load($sampleId);
+ if ($sample->getId() && $this->isProductSalable($sample)) {
$resource = '';
$resourceType = '';
if ($sample->getSampleType() == DownloadHelper::LINK_TYPE_URL) {
@@ -71,4 +74,16 @@ public function execute()
return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl());
}
+
+ /**
+ * Check is related product salable.
+ *
+ * @param SampleModel $sample
+ * @return bool
+ */
+ private function isProductSalable(SampleModel $sample): bool
+ {
+ $product = $this->relatedProductRetriever->getProduct((int) $sample->getProductId());
+ return $product ? $product->isSalable() : false;
+ }
}
diff --git a/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php
new file mode 100644
index 0000000000000..f701f96b910e7
--- /dev/null
+++ b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php
@@ -0,0 +1,68 @@
+productRepository = $productRepository;
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ $this->metadataPool = $metadataPool;
+ }
+
+ /**
+ * Get related product.
+ *
+ * @param int $productId
+ * @return ProductInterface|null
+ */
+ public function getProduct(int $productId): ?ProductInterface
+ {
+ $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class);
+
+ $searchCriteria = $this->searchCriteriaBuilder->addFilter($productMetadata->getLinkField(), $productId)
+ ->create();
+ $items = $this->productRepository->getList($searchCriteria)
+ ->getItems();
+ $product = $items ? array_shift($items) : null;
+
+ return $product;
+ }
+}
diff --git a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php
index 8d30322745b8d..b7b079d208d97 100644
--- a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php
+++ b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php
@@ -24,7 +24,7 @@ class Sample extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
/**
* @param \Magento\Framework\Model\ResourceModel\Db\Context $context
* @param \Magento\Framework\EntityManager\MetadataPool $metadataPool
- * @param null $connectionName
+ * @param string|null $connectionName
*/
public function __construct(
\Magento\Framework\Model\ResourceModel\Db\Context $context,
@@ -126,7 +126,7 @@ public function getSearchableData($productId, $storeId)
)->join(
['cpe' => $this->getTable('catalog_product_entity')],
sprintf(
- 'cpe.entity_id = m.product_id',
+ 'cpe.%s = m.product_id',
$this->metadataPool->getMetadata(ProductInterface::class)->getLinkField()
),
[]
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml
index 9ad20385519d1..34b9701f2dca5 100644
--- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml
+++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml
@@ -37,7 +37,7 @@
-
+
diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php
deleted file mode 100644
index 725c06004f117..0000000000000
--- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php
+++ /dev/null
@@ -1,237 +0,0 @@
-objectManagerHelper = new ObjectManagerHelper($this);
-
- $this->request = $this->getMockForAbstractClass(RequestInterface::class);
- $this->response = $this->getMockBuilder(ResponseInterface::class)
- ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect'])
- ->onlyMethods(['sendResponse'])
- ->getMockForAbstractClass();
-
- $this->helperData = $this->createPartialMock(
- Data::class,
- ['getIsShareable']
- );
- $this->downloadHelper = $this->createPartialMock(
- Download::class,
- [
- 'setResource',
- 'getFilename',
- 'getContentType',
- 'getFileSize',
- 'getContentDisposition',
- 'output'
- ]
- );
- $this->product = $this->getMockBuilder(Product::class)
- ->addMethods(['_wakeup'])
- ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName'])
- ->disableOriginalConstructor()
- ->getMock();
- $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class);
- $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class);
- $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class);
- $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class);
- $this->objectManager = $this->createPartialMock(
- \Magento\Framework\ObjectManager\ObjectManager::class,
- ['create', 'get']
- );
- $this->linkSample = $this->objectManagerHelper->getObject(
- LinkSample::class,
- [
- 'objectManager' => $this->objectManager,
- 'request' => $this->request,
- 'response' => $this->response,
- 'messageManager' => $this->messageManager,
- 'redirect' => $this->redirect,
- 'salabilityChecker' => $this->salabilityCheckerMock,
- ]
- );
- }
-
- /**
- * Execute Download link's sample action with Url link.
- *
- * @return void
- */
- public function testExecuteLinkTypeUrl()
- {
- $linkMock = $this->getMockBuilder(Link::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id');
- $this->objectManager->expects($this->once())
- ->method('create')
- ->with(Link::class)
- ->willReturn($linkMock);
- $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf();
- $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $linkMock->expects($this->once())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_URL
- );
- $linkMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url');
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->linkSample->execute());
- }
-
- /**
- * Execute Download link's sample action with File link.
- *
- * @return void
- */
- public function testExecuteLinkTypeFile()
- {
- $linkMock = $this->getMockBuilder(Link::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath'])
- ->getMock();
- $fileMock = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->setMethods(['getFilePath', 'load', 'getSampleType', 'getSampleUrl'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id');
- $this->objectManager->expects($this->at(0))
- ->method('create')
- ->with(Link::class)
- ->willReturn($linkMock);
- $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf();
- $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $linkMock->expects($this->any())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_FILE
- );
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(File::class)
- ->willReturn($fileMock);
- $this->objectManager->expects($this->at(2))
- ->method('get')
- ->with(Link::class)
- ->willReturn($linkMock);
- $linkMock->expects($this->once())->method('getBaseSamplePath')->willReturn('downloadable/files/link_samples');
- $this->objectManager->expects($this->at(3))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->linkSample->execute());
- }
-}
diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php
deleted file mode 100644
index 6dcd09a91dd2e..0000000000000
--- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php
+++ /dev/null
@@ -1,232 +0,0 @@
-objectManagerHelper = new ObjectManagerHelper($this);
-
- $this->request = $this->getMockForAbstractClass(RequestInterface::class);
- $this->response = $this->getMockBuilder(ResponseInterface::class)
- ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect'])
- ->onlyMethods(['sendResponse'])
- ->getMockForAbstractClass();
-
- $this->helperData = $this->createPartialMock(
- Data::class,
- ['getIsShareable']
- );
- $this->downloadHelper = $this->createPartialMock(
- Download::class,
- [
- 'setResource',
- 'getFilename',
- 'getContentType',
- 'getFileSize',
- 'getContentDisposition',
- 'output'
- ]
- );
- $this->product = $this->getMockBuilder(Product::class)
- ->addMethods(['_wakeup'])
- ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName'])
- ->disableOriginalConstructor()
- ->getMock();
- $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class);
- $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class);
- $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class);
- $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class);
- $this->objectManager = $this->createPartialMock(
- \Magento\Framework\ObjectManager\ObjectManager::class,
- ['create', 'get']
- );
- $this->sample = $this->objectManagerHelper->getObject(
- Sample::class,
- [
- 'objectManager' => $this->objectManager,
- 'request' => $this->request,
- 'response' => $this->response,
- 'messageManager' => $this->messageManager,
- 'redirect' => $this->redirect,
- 'salabilityChecker' => $this->salabilityCheckerMock,
- ]
- );
- }
-
- /**
- * Execute Download sample action with Sample Url.
- *
- * @return void
- */
- public function testExecuteSampleWithUrlType()
- {
- $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id');
- $this->objectManager->expects($this->once())
- ->method('create')
- ->with(\Magento\Downloadable\Model\Sample::class)
- ->willReturn($sampleMock);
- $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf();
- $sampleMock->expects($this->once())->method('getId')->willReturn('some_link_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $sampleMock->expects($this->once())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_URL
- );
- $sampleMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url');
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->sample->execute());
- }
-
- /**
- * Execute Download sample action with Sample File.
- *
- * @return void
- */
- public function testExecuteSampleWithFileType()
- {
- $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath'])
- ->getMock();
- $fileHelperMock = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->setMethods(['getFilePath'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id');
- $this->objectManager->expects($this->at(0))
- ->method('create')
- ->with(\Magento\Downloadable\Model\Sample::class)
- ->willReturn($sampleMock);
- $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf();
- $sampleMock->expects($this->once())->method('getId')->willReturn('some_sample_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $sampleMock->expects($this->any())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_FILE
- );
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(File::class)
- ->willReturn($fileHelperMock);
- $fileHelperMock->expects($this->once())->method('getFilePath')->willReturn('file_path');
- $this->objectManager->expects($this->at(2))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->sample->execute());
- }
-}
diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php
index 15dcea077c887..7e434166a15be 100644
--- a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php
+++ b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php
@@ -23,12 +23,12 @@ class Data extends \Magento\Framework\Validator\AbstractValidator
/**
* @var array
*/
- protected $_attributesWhiteList = [];
+ protected $allowedAttributesList = [];
/**
* @var array
*/
- protected $_attributesBlackList = [];
+ protected $deniedAttributesList = [];
/**
* @var array
@@ -68,9 +68,9 @@ public function setAttributes(array $attributes)
* @param array $attributesCodes
* @return $this
*/
- public function setAttributesWhiteList(array $attributesCodes)
+ public function setAllowedAttributesList(array $attributesCodes)
{
- $this->_attributesWhiteList = $attributesCodes;
+ $this->allowedAttributesList = $attributesCodes;
return $this;
}
@@ -82,9 +82,9 @@ public function setAttributesWhiteList(array $attributesCodes)
* @param array $attributesCodes
* @return $this
*/
- public function setAttributesBlackList(array $attributesCodes)
+ public function setDeniedAttributesList(array $attributesCodes)
{
- $this->_attributesBlackList = $attributesCodes;
+ $this->deniedAttributesList = $attributesCodes;
return $this;
}
@@ -171,11 +171,11 @@ protected function _getAttributes($entity)
$attributesCodes[] = $attributeCode;
}
- $ignoreAttributes = $this->_attributesBlackList;
- if ($this->_attributesWhiteList) {
+ $ignoreAttributes = $this->deniedAttributesList;
+ if ($this->allowedAttributesList) {
$ignoreAttributes = array_merge(
$ignoreAttributes,
- array_diff($attributesCodes, $this->_attributesWhiteList)
+ array_diff($attributesCodes, $this->allowedAttributesList)
);
}
diff --git a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php
index 774b968f1b697..a8ecbb8371ac9 100644
--- a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php
+++ b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php
@@ -249,10 +249,10 @@ public function testIsValidAttributesFromCollection()
}
/**
- * @dataProvider whiteBlackListProvider
+ * @dataProvider allowDenyListProvider
* @param callable $callback
*/
- public function testIsValidBlackListWhiteListChecks($callback)
+ public function testIsValidExclusionInclusionListChecks($callback)
{
$attribute = $this->_getAttributeMock(
[
@@ -302,19 +302,19 @@ public function testIsValidBlackListWhiteListChecks($callback)
/**
* @return array
*/
- public function whiteBlackListProvider()
+ public function allowDenyListProvider()
{
- $whiteCallback = function ($validator) {
- $validator->setAttributesWhiteList(['attribute']);
+ $allowedCallbackList = function ($validator) {
+ $validator->setAllowedAttributesList(['attribute']);
};
- $blackCallback = function ($validator) {
- $validator->setAttributesBlackList(['attribute2']);
+ $deniedCallbackList = function ($validator) {
+ $validator->setDeniedAttributesList(['attribute2']);
};
- return ['white_list' => [$whiteCallback], 'black_list' => [$blackCallback]];
+ return ['allowed' => [$allowedCallbackList], 'denied' => [$deniedCallbackList]];
}
- public function testSetAttributesWhiteList()
+ public function testSetAttributesAllowedList()
{
$this->markTestSkipped('Skipped in #27500 due to testing protected/private methods and properties');
@@ -328,12 +328,14 @@ public function testSetAttributesWhiteList()
)
->getMock();
$validator = new Data($attrDataFactory);
- $result = $validator->setAttributesWhiteList($attributes);
- $this->assertAttributeEquals($attributes, '_attributesWhiteList', $validator);
+ $result = $validator->setIncludedAttributesList($attributes);
+
+ // phpstan:ignore
+ $this->assertAttributeEquals($attributes, '_attributesAllowed', $validator);
$this->assertEquals($validator, $result);
}
- public function testSetAttributesBlackList()
+ public function testSetAttributesDeniedList()
{
$this->markTestSkipped('Skipped in #27500 due to testing protected/private methods and properties');
@@ -347,8 +349,9 @@ public function testSetAttributesBlackList()
)
->getMock();
$validator = new Data($attrDataFactory);
- $result = $validator->setAttributesBlackList($attributes);
- $this->assertAttributeEquals($attributes, '_attributesBlackList', $validator);
+ $result = $validator->setDeniedAttributesList($attributes);
+ // phpstan:ignore
+ $this->assertAttributeEquals($attributes, '_attributesDenied', $validator);
$this->assertEquals($validator, $result);
}
diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
index 1f6e05c9e02fc..8576d8df0cc95 100644
--- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
+++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
@@ -19,7 +19,7 @@ class Converter implements ConverterInterface
*/
private const ES_DATA_TYPE_TEXT = 'text';
private const ES_DATA_TYPE_KEYWORD = 'keyword';
- private const ES_DATA_TYPE_FLOAT = 'float';
+ private const ES_DATA_TYPE_DOUBLE = 'double';
private const ES_DATA_TYPE_INT = 'integer';
private const ES_DATA_TYPE_DATE = 'date';
/**#@-*/
@@ -32,7 +32,7 @@ class Converter implements ConverterInterface
private $mapping = [
self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_TEXT,
self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_KEYWORD,
- self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT,
+ self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE,
self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT,
self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE,
];
diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php
index bd9a380230652..8d8787a5eff72 100644
--- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php
+++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php
@@ -276,7 +276,7 @@ public function addFieldsMapping(array $fields, $index, $entityType)
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
index 88dab83698794..2067dcdc7fe9f 100644
--- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
+++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php
@@ -16,7 +16,7 @@ class Converter implements ConverterInterface
* Text flags for Elasticsearch field types
*/
private const ES_DATA_TYPE_STRING = 'string';
- private const ES_DATA_TYPE_FLOAT = 'float';
+ private const ES_DATA_TYPE_DOUBLE = 'double';
private const ES_DATA_TYPE_INT = 'integer';
private const ES_DATA_TYPE_DATE = 'date';
/**#@-*/
@@ -29,7 +29,7 @@ class Converter implements ConverterInterface
private $mapping = [
self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_STRING,
self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_STRING,
- self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT,
+ self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE,
self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT,
self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE,
];
diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php
index 1e106023ea00d..548a57e55f3e2 100644
--- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php
+++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php
@@ -35,7 +35,7 @@ public function __construct(Repository $algorithmRepository, EntityStorageFactor
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function build(
RequestBucketInterface $bucket,
@@ -46,9 +46,7 @@ public function build(
/** @var DynamicBucket $bucket */
$algorithm = $this->algorithmRepository->get($bucket->getMethod(), ['dataProvider' => $dataProvider]);
$data = $algorithm->getItems($bucket, $dimensions, $this->getEntityStorage($queryResult));
- $resultData = $this->prepareData($data);
-
- return $resultData;
+ return $this->prepareData($data);
}
/**
@@ -77,12 +75,9 @@ private function prepareData($data)
{
$resultData = [];
foreach ($data as $value) {
- $from = is_numeric($value['from']) ? $value['from'] : '*';
- $to = is_numeric($value['to']) ? $value['to'] : '*';
- unset($value['from'], $value['to']);
-
- $rangeName = "{$from}_{$to}";
- $resultData[$rangeName] = array_merge(['value' => $rangeName], $value);
+ $rangeName = "{$value['from']}_{$value['to']}";
+ $value['value'] = $rangeName;
+ $resultData[$rangeName] = $value;
}
return $resultData;
diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php
index 496a77e4c5ac3..7bc64b59ffe78 100644
--- a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php
+++ b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php
@@ -235,11 +235,9 @@ public function prepareData($range, array $dbRanges)
{
$data = [];
if (!empty($dbRanges)) {
- $lastIndex = array_keys($dbRanges);
- $lastIndex = $lastIndex[count($lastIndex) - 1];
foreach ($dbRanges as $index => $count) {
- $fromPrice = $index == 1 ? '' : ($index - 1) * $range;
- $toPrice = $index == $lastIndex ? '' : $index * $range;
+ $fromPrice = $index == 1 ? 0 : ($index - 1) * $range;
+ $toPrice = $index * $range;
$data[] = [
'from' => $fromPrice,
'to' => $toPrice,
diff --git a/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php
new file mode 100644
index 0000000000000..7cd72c322d647
--- /dev/null
+++ b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php
@@ -0,0 +1,66 @@
+moduleDataSetup = $moduleDataSetup;
+ $this->indexerRegistry = $indexerRegistry;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function apply(): PatchInterface
+ {
+ $this->indexerRegistry->get(FulltextIndexer::INDEXER_ID)->invalidate();
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getDependencies(): array
+ {
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getAliases(): array
+ {
+ return [];
+ }
+}
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php
index 49a894f1295c7..575a64dc43abd 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php
@@ -329,7 +329,7 @@ public function testAddFieldsMapping()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
@@ -400,7 +400,7 @@ public function testAddFieldsMappingFailure()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php
index 87f072836544e..a9bcd1a20a1b2 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php
@@ -246,7 +246,7 @@ function ($type) use ($complexType) {
if ($type === 'string') {
return 'string';
} elseif ($type === 'float') {
- return 'float';
+ return 'double';
} elseif ($type === 'integer') {
return 'integer';
} else {
@@ -281,7 +281,7 @@ public function attributeProvider()
'index' => 'no_index'
],
'price_1_1' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true
]
]
@@ -300,7 +300,7 @@ public function attributeProvider()
'index' => 'no_index'
],
'price_1_1' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true
]
],
@@ -319,7 +319,7 @@ public function attributeProvider()
'index' => 'no_index'
],
'price_1_1' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true
]
]
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php
index 75b79bc43e805..718adf255254f 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php
@@ -56,7 +56,7 @@ public function convertProvider()
{
return [
['string', 'string'],
- ['float', 'float'],
+ ['float', 'double'],
['integer', 'integer'],
];
}
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php
index c5b9089acd91c..0595b667f4ee8 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php
@@ -390,13 +390,13 @@ public function testPrepareData()
{
$expectedResult = [
[
- 'from' => '',
+ 'from' => 0,
'to' => 10,
'count' => 1,
],
[
'from' => 10,
- 'to' => '',
+ 'to' => 20,
'count' => 1,
],
];
diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml
index 633889e70591b..633e67dfe698e 100644
--- a/app/code/Magento/Elasticsearch/etc/di.xml
+++ b/app/code/Magento/Elasticsearch/etc/di.xml
@@ -537,7 +537,7 @@
-
+
- Elasticsearch 2
diff --git a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php
index 2c1c283c5b24d..0571b075aff28 100644
--- a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php
+++ b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php
@@ -281,7 +281,7 @@ public function addFieldsMapping(array $fields, $index, $entityType)
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php
index aa0b49400c517..2a7fa2ce8114a 100644
--- a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php
+++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php
@@ -439,7 +439,7 @@ public function testAddFieldsMapping()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
@@ -509,7 +509,7 @@ public function testAddFieldsMappingFailure()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php
index feacca8d62804..4b318f987abfe 100644
--- a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php
+++ b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php
@@ -289,7 +289,7 @@ public function addFieldsMapping(array $fields, string $index, string $entityTyp
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php
index 593bbd7792f46..091387f844d55 100644
--- a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php
+++ b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php
@@ -438,7 +438,7 @@ public function testAddFieldsMapping()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
@@ -509,7 +509,7 @@ public function testAddFieldsMappingFailure()
'match' => 'price_*',
'match_mapping_type' => 'string',
'mapping' => [
- 'type' => 'float',
+ 'type' => 'double',
'store' => true,
],
],
diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl
index 62795f07239a6..3629bb424f207 100644
--- a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl
+++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl
@@ -472,7 +472,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
@@ -983,7 +983,7 @@
- The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the master transaction and all child transactions
+ The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions
@@ -1005,7 +1005,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
@@ -4867,4 +4867,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl
index 17a6f74cc09b8..2f3feecb58084 100644
--- a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl
+++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl
@@ -471,7 +471,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment commitment more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
@@ -983,7 +983,7 @@
- The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the master transaction and all child transactions
+ The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions
@@ -1005,7 +1005,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl
index 54bb57d490c76..439d032a61fd0 100644
--- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl
+++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl
@@ -497,7 +497,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
@@ -724,7 +724,7 @@
- The master tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.
+ The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.
diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl
index d8dc0fdfed4ab..a449bf41dbd68 100644
--- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl
+++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl
@@ -497,7 +497,7 @@
- For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction.
+ For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction.
If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request.
@@ -724,7 +724,7 @@
- The master tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.
+ The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.
diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php
index c317221fb6ef7..2ce51c8bbf19d 100644
--- a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php
+++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php
@@ -66,7 +66,7 @@ public function resolve(
array $args = null
) {
if (!isset($value['model'])) {
- throw new GraphQlInputException(__('"model" value should be specified'));
+ throw new GraphQlInputException(__('"model" value must be specified'));
}
$cart = $value['model'];
diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php
new file mode 100644
index 0000000000000..a9a8e682612cc
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php
@@ -0,0 +1,97 @@
+itemRepository = $itemRepository;
+ $this->giftMessageHelper = $giftMessageHelper;
+ }
+
+ /**
+ * Return information about Gift message for item cart
+ *
+ * @param Field $field
+ * @param ContextInterface $context
+ * @param ResolveInfo $info
+ * @param array|null $value
+ * @param array|null $args
+ *
+ * @return array|Value|mixed
+ * @throws GraphQlInputException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['model'])) {
+ throw new GraphQlInputException(__('"model" value must be specified'));
+ }
+
+ $quoteItem = $value['model'];
+
+ if (!$this->giftMessageHelper->isMessagesAllowed('items', $quoteItem)) {
+ return null;
+ }
+
+ if (!$this->giftMessageHelper->isMessagesAllowed('item', $quoteItem)) {
+ return null;
+ }
+
+ try {
+ $giftItemMessage = $this->itemRepository->get($quoteItem->getQuoteId(), $quoteItem->getItemId());
+ } catch (LocalizedException $e) {
+ throw new GraphQlInputException(__('Can\'t load cart item'));
+ }
+
+ if (!isset($giftItemMessage)) {
+ return null;
+ }
+
+ return [
+ 'to' => $giftItemMessage->getRecipient() ?? '',
+ 'from' => $giftItemMessage->getSender() ?? '',
+ 'message'=> $giftItemMessage->getMessage() ?? ''
+ ];
+ }
+}
diff --git a/app/code/Magento/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md
index fa2e02116b66c..d73a058e0db9c 100644
--- a/app/code/Magento/GiftMessageGraphQl/README.md
+++ b/app/code/Magento/GiftMessageGraphQl/README.md
@@ -1,3 +1,3 @@
# GiftMessageGraphQl
-**GiftMessageGraphQl** provides information about gift messages for cart, cart items, order and order items.
+**GiftMessageGraphQl** provides information about gift messages for carts, cart items, orders and order items.
diff --git a/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml
new file mode 100644
index 0000000000000..bce5b7063e6b9
--- /dev/null
+++ b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ - sales/gift_options/allow_order
+ - sales/gift_options/allow_items
+
+
+
+
diff --git a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls
index f14c812a9a5f3..ad18054abca13 100644
--- a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls
@@ -1,20 +1,47 @@
# Copyright © Magento, Inc. All rights reserved.
# See COPYING.txt for license details.
+type StoreConfig {
+ allow_order : String @doc(description: "The value of the Allow Gift Messages on Order Level option")
+ allow_items : String @doc(description: "The value of the Allow Gift Messages for Order Items option")
+}
+
type Cart {
gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\GiftMessage") @doc(description: "The entered gift message for the cart")
}
-type SalesItemInterface {
- gift_message: GiftMessage @doc(description: "The entered gift message for the order item")
+type SimpleCartItem {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item")
}
-type CustomerOrder {
- gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order")
+type ConfigurableCartItem {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item")
+}
+
+type BundleCartItem {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item")
+}
+
+type GiftMessage @doc(description: "Contains the text of a gift message, its sender, and recipient") {
+ to: String! @doc(description: "Recipient name")
+ from: String! @doc(description: "Sender name")
+ message: String! @doc(description: "Gift message text")
+}
+
+input CartItemUpdateInput {
+ gift_message: GiftMessageInput @doc(description: "Gift message details for the cart item")
}
-type GiftMessage {
- to: String! @doc(description: "Recepient name")
+input GiftMessageInput @doc(description: "Contains the text of a gift message, its sender, and recipient") {
+ to: String! @doc(description: "Recipient name")
from: String! @doc(description: "Sender name")
message: String! @doc(description: "Gift message text")
}
+
+type SalesItemInterface {
+ gift_message: GiftMessage @doc(description: "The entered gift message for the order item")
+}
+
+type CustomerOrder {
+ gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order")
+}
diff --git a/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php b/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php
index 135c8c92c6aa9..975788abe52e4 100644
--- a/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php
+++ b/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php
@@ -5,12 +5,16 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\GoogleOptimizer\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
/**
+ * Abstract entity for saving codes
+ *
* @api
* @since 100.0.2
*/
@@ -96,7 +100,9 @@ protected function _processCode()
$this->_initRequestParams();
if ($this->_isNewCode()) {
- $this->_saveCode();
+ if (!$this->_isEmptyCode()) {
+ $this->_saveCode();
+ }
} else {
$this->_loadCode();
if ($this->_isEmptyCode()) {
@@ -185,6 +191,8 @@ protected function _deleteCode()
}
/**
+ * Check data availability
+ *
* @return bool
*/
private function isDataAvailable()
@@ -194,6 +202,8 @@ private function isDataAvailable()
}
/**
+ * Get request data
+ *
* @return mixed
*/
private function getRequestData()
diff --git a/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php b/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php
index 8a5c247369657..c6d02957c4be9 100644
--- a/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php
+++ b/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php
@@ -127,6 +127,39 @@ public function testCreatingCodeIfRequestIsValid()
$this->_modelObserver->execute($this->_eventObserverMock);
}
+ /**
+ * Test that code is not saving when request is empty
+ *
+ * @return void
+ */
+ public function testCreatingCodeIfRequestIsEmpty(): void
+ {
+ $this->_helperMock->expects(
+ $this->once()
+ )->method(
+ 'isGoogleExperimentActive'
+ )->with(
+ $this->_storeId
+ )->willReturn(
+ true
+ );
+
+ $this->_requestMock->expects(
+ $this->exactly(3)
+ )->method(
+ 'getParam'
+ )->with(
+ 'google_experiment'
+ )->willReturn(
+ ['code_id' => '', 'experiment_script' => '']
+ );
+
+ $this->_codeMock->expects($this->never())->method('addData');
+ $this->_codeMock->expects($this->never())->method('save');
+
+ $this->_modelObserver->execute($this->_eventObserverMock);
+ }
+
/**
* @param array $params
* @dataProvider dataProviderWrongRequestForCreating
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php
new file mode 100644
index 0000000000000..ba2e995d4f704
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string
+ */
+ public function getValue(): string
+ {
+ return "1";
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->corsConfiguration->isCredentialsAllowed();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php
new file mode 100644
index 0000000000000..68760de543daa
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return $this->corsConfiguration->getAllowedHeaders();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php
new file mode 100644
index 0000000000000..233839b9deb74
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return $this->corsConfiguration->getAllowedMethods();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php
new file mode 100644
index 0000000000000..21850f18db1f2
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return $this->corsConfiguration->getAllowedOrigins();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php
new file mode 100644
index 0000000000000..e30209ae25e68
--- /dev/null
+++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php
@@ -0,0 +1,71 @@
+corsConfiguration = $corsConfiguration;
+ $this->headerName = $headerName;
+ }
+
+ /**
+ * Get name of header
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->headerName;
+ }
+
+ /**
+ * Check if header can be applied
+ *
+ * @return bool
+ */
+ public function canApply(): bool
+ {
+ return $this->corsConfiguration->isEnabled() && $this->getValue();
+ }
+
+ /**
+ * Get value for header
+ *
+ * @return string|null
+ */
+ public function getValue(): ?string
+ {
+ return (string) $this->corsConfiguration->getMaxAge();
+ }
+}
diff --git a/app/code/Magento/GraphQl/Model/Cors/Configuration.php b/app/code/Magento/GraphQl/Model/Cors/Configuration.php
new file mode 100644
index 0000000000000..dd5a0b426e22d
--- /dev/null
+++ b/app/code/Magento/GraphQl/Model/Cors/Configuration.php
@@ -0,0 +1,96 @@
+scopeConfig = $scopeConfig;
+ }
+
+ /**
+ * Are CORS headers enabled
+ *
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_HEADERS_ENABLED);
+ }
+
+ /**
+ * Get allowed origins or null if stored configuration is empty
+ *
+ * @return string|null
+ */
+ public function getAllowedOrigins(): ?string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_ORIGINS);
+ }
+
+ /**
+ * Get allowed headers or null if stored configuration is empty
+ *
+ * @return string|null
+ */
+ public function getAllowedHeaders(): ?string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_HEADERS);
+ }
+
+ /**
+ * Get allowed methods or null if stored configuration is empty
+ *
+ * @return string|null
+ */
+ public function getAllowedMethods(): ?string
+ {
+ return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_METHODS);
+ }
+
+ /**
+ * Get max age header value
+ *
+ * @return int
+ */
+ public function getMaxAge(): int
+ {
+ return (int) $this->scopeConfig->getValue(self::XML_PATH_CORS_MAX_AGE);
+ }
+
+ /**
+ * Are credentials allowed
+ *
+ * @return bool
+ */
+ public function isCredentialsAllowed(): bool
+ {
+ return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_ALLOW_CREDENTIALS);
+ }
+}
diff --git a/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php
new file mode 100644
index 0000000000000..b40b64f48e51f
--- /dev/null
+++ b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php
@@ -0,0 +1,56 @@
+
+
+
+
+
+ GraphQL
+ service
+ Magento_Integration::config_oauth
+
+ CORS Settings
+
+ CORS Headers Enabled
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Allowed origins
+ The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin. Fill this field with one or more origins (comma separated) or use '*' to allow access from all origins.
+
+ 1
+
+
+
+
+ Allowed methods
+ The Access-Control-Allow-Methods response header specifies the method or methods allowed when accessing the resource in response to a preflight request. Use comma separated methods (e.g. GET,POST)
+
+ 1
+
+
+
+
+
+
+ Max Age
+ validate-digits
+ The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.
+
+ 1
+
+
+
+
+ Credentials Allowed
+ Magento\Config\Model\Config\Source\Yesno
+ The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to frontend code when the request's credentials mode is include.
+
+ 1
+
+
+
+
+
+
diff --git a/app/code/Magento/GraphQl/etc/config.xml b/app/code/Magento/GraphQl/etc/config.xml
new file mode 100644
index 0000000000000..39caacbec42d2
--- /dev/null
+++ b/app/code/Magento/GraphQl/etc/config.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ 0
+
+
+
+ 86400
+ 0
+
+
+
+
diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml
index b356f33c4f4bf..fca6c425e2507 100644
--- a/app/code/Magento/GraphQl/etc/di.xml
+++ b/app/code/Magento/GraphQl/etc/di.xml
@@ -98,4 +98,31 @@
300
+
+
+
+
+ Access-Control-Max-Age
+
+
+
+
+ Access-Control-Allow-Credentials
+
+
+
+
+ Access-Control-Allow-Headers
+
+
+
+
+ Access-Control-Allow-Methods
+
+
+
+
+ Access-Control-Allow-Origin
+
+
diff --git a/app/code/Magento/GraphQl/etc/graphql/di.xml b/app/code/Magento/GraphQl/etc/graphql/di.xml
index 77fce336374dd..23d49124d1a02 100644
--- a/app/code/Magento/GraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/GraphQl/etc/graphql/di.xml
@@ -30,4 +30,15 @@
+
+
+
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider
+ - Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider
+
+
+
diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls
index fccde015c3388..0212d32db0f2f 100644
--- a/app/code/Magento/GraphQl/etc/schema.graphqls
+++ b/app/code/Magento/GraphQl/etc/schema.graphqls
@@ -79,6 +79,12 @@ input FilterMatchTypeInput @doc(description: "Defines a filter that performs a f
match: String @doc(description: "One or more words to filter on")
}
+input FilterStringTypeInput @doc(description: "Defines a filter for an input string.") {
+ in: [String] @doc(description: "Filters items that are exactly the same as entries specified in an array of strings.")
+ eq: String @doc(description: "Filters items that are exactly the same as the specified string.")
+ match: String @doc(description: "Defines a filter that performs a fuzzy search using the specified string.")
+}
+
type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") {
page_size: Int @doc(description: "Specifies the maximum number of items to return")
current_page: Int @doc(description: "Specifies which page of results to return")
diff --git a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php
index 92cfb375fea41..29fa2bffabb3b 100644
--- a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php
+++ b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php
@@ -10,7 +10,7 @@
use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface;
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
class GroupedProductLinksTypeResolver implements TypeResolverInterface
{
@@ -20,14 +20,14 @@ class GroupedProductLinksTypeResolver implements TypeResolverInterface
private $linkTypes = ['associated'];
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
- public function resolveType(array $data) : string
+ public function resolveType(array $data): string
{
if (isset($data['link_type'])) {
$linkType = $data['link_type'];
if (in_array($linkType, $this->linkTypes)) {
- return 'GroupedProductLinks';
+ return 'ProductLinks';
}
}
return '';
diff --git a/app/code/Magento/GroupedProductGraphQl/etc/di.xml b/app/code/Magento/GroupedProductGraphQl/etc/di.xml
index 35b63370baf2f..717bc14826f70 100644
--- a/app/code/Magento/GroupedProductGraphQl/etc/di.xml
+++ b/app/code/Magento/GroupedProductGraphQl/etc/di.xml
@@ -13,4 +13,11 @@
+
+
+
+ - associated
+
+
+
diff --git a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php
index 09b17371ae4e8..f5b62df9aea2c 100644
--- a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php
+++ b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php
@@ -54,6 +54,19 @@ public function destruct()
{
if (is_object($this->_fileHandler)) {
$this->_fileHandler->close();
+ $this->resolveDestination();
+ }
+ }
+
+ /**
+ * Remove temporary destination
+ *
+ * @return void
+ */
+ private function resolveDestination(): void
+ {
+ // only temporary file located directly in var folder
+ if (strpos($this->_destination, '/') === false) {
$this->_directoryHandle->delete($this->_destination);
}
}
diff --git a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php
index 5bd956c1bc322..9bf5b945c8fbd 100644
--- a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php
+++ b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php
@@ -15,6 +15,7 @@
/**
* Import entity abstract model
*
+ * phpcs:disable Magento2.Classes.AbstractApi
* @api
*
* @SuppressWarnings(PHPMD.TooManyFields)
@@ -335,6 +336,8 @@ public function __construct(
}
/**
+ * Returns Error aggregator
+ *
* @return ProcessingErrorAggregatorInterface
*/
public function getErrorAggregator()
@@ -413,7 +416,7 @@ protected function _saveValidatedBunches()
$source->rewind();
$this->_dataSourceModel->cleanBunches();
- $masterAttributeCode = $this->getMasterAttributeCode();
+ $mainAttributeCode = $this->getMasterAttributeCode();
while ($source->valid() || count($bunchRows) || isset($entityGroup)) {
if ($startNewBunch || !$source->valid()) {
@@ -453,7 +456,7 @@ protected function _saveValidatedBunches()
continue;
}
- if (isset($rowData[$masterAttributeCode]) && trim($rowData[$masterAttributeCode])) {
+ if (isset($rowData[$mainAttributeCode]) && trim($rowData[$mainAttributeCode])) {
/* Add entity group that passed validation to bunch */
if (isset($entityGroup)) {
foreach ($entityGroup as $key => $value) {
@@ -590,6 +593,7 @@ public function getBehavior(array $rowData = null)
* Get default import behavior
*
* @return string
+ * phpcs:disable Magento2.Functions.StaticFunction
*/
public static function getDefaultBehavior()
{
@@ -652,7 +656,9 @@ public function isAttributeParticular($attributeCode)
}
/**
- * @return string the master attribute code to use in an import
+ * Returns the master attribute code to use in an import
+ *
+ * @return string
*/
public function getMasterAttributeCode()
{
diff --git a/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php
new file mode 100644
index 0000000000000..1565d455cc43f
--- /dev/null
+++ b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php
@@ -0,0 +1,120 @@
+extractAssetsFromContent = $extractAssetsFromContent;
+ $this->getContent = $getContent;
+ $this->deleteContentAssetLinks = $deleteContentAssetLinks;
+ $this->contentAssetLinkFactory = $contentAssetLinkFactory;
+ $this->contentIdentityFactory = $contentIdentityFactory;
+ $this->fields = $fields;
+ }
+
+ /**
+ * Retrieve the deleted category and remove relation betwen category and asset
+ *
+ * @param Observer $observer
+ * @throws \Exception
+ */
+ public function execute(Observer $observer): void
+ {
+ $category = $observer->getEvent()->getData('category');
+ $contentAssetLinks = [];
+
+ if ($category instanceof CatalogCategory) {
+ foreach ($this->fields as $field) {
+ $contentIdentity = $this->contentIdentityFactory->create(
+ [
+ self::TYPE => self::CONTENT_TYPE,
+ self::FIELD => $field,
+ self::ENTITY_ID => (string) $category->getEntityId(),
+ ]
+ );
+ $content = implode(PHP_EOL, $this->getContent->execute($contentIdentity));
+ $assets = $this->extractAssetsFromContent->execute($content);
+
+ foreach ($assets as $asset) {
+ $contentAssetLinks[] = $this->contentAssetLinkFactory->create(
+ [
+ 'assetId' => $asset->getId(),
+ 'contentIdentity' => $contentIdentity
+ ]
+ );
+ }
+ }
+ if (!empty($contentAssetLinks)) {
+ $this->deleteContentAssetLinks->execute($contentAssetLinks);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php
new file mode 100644
index 0000000000000..421bb5a33fa1d
--- /dev/null
+++ b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php
@@ -0,0 +1,120 @@
+extractAssetsFromContent = $extractAssetsFromContent;
+ $this->getContent = $getContent;
+ $this->deleteContentAssetLinks = $deleteContentAssetLinks;
+ $this->contentAssetLinkFactory = $contentAssetLinkFactory;
+ $this->contentIdentityFactory = $contentIdentityFactory;
+ $this->fields = $fields;
+ }
+
+ /**
+ * Retrieve the deleted product and remove relation betwen product and asset
+ *
+ * @param Observer $observer
+ * @throws \Exception
+ */
+ public function execute(Observer $observer): void
+ {
+ $product = $observer->getEvent()->getData('product');
+ $contentAssetLinks = [];
+
+ if ($product instanceof CatalogProduct) {
+ foreach ($this->fields as $field) {
+ $contentIdentity = $this->contentIdentityFactory->create(
+ [
+ self::TYPE => self::CONTENT_TYPE,
+ self::FIELD => $field,
+ self::ENTITY_ID => (string) $product->getEntityId(),
+ ]
+ );
+ $productContent = implode(PHP_EOL, $this->getContent->execute($contentIdentity));
+ $assets = $this->extractAssetsFromContent->execute($productContent);
+
+ foreach ($assets as $asset) {
+ $contentAssetLinks[] = $this->contentAssetLinkFactory->create(
+ [
+ 'assetId' => $asset->getId(),
+ 'contentIdentity' => $contentIdentity
+ ]
+ );
+ }
+ }
+ if (!empty($contentAssetLinks)) {
+ $this->deleteContentAssetLinks->execute($contentAssetLinks);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/MediaContentCatalog/etc/di.xml b/app/code/Magento/MediaContentCatalog/etc/di.xml
index a2d300a2bb208..6b0ee83b30788 100644
--- a/app/code/Magento/MediaContentCatalog/etc/di.xml
+++ b/app/code/Magento/MediaContentCatalog/etc/di.xml
@@ -14,6 +14,22 @@
+
+
+
+ - description
+ - short_description
+
+
+
+
+
+
+ - image
+ - description
+
+
+
diff --git a/app/code/Magento/MediaContentCatalog/etc/events.xml b/app/code/Magento/MediaContentCatalog/etc/events.xml
index f68d66eb3cc40..8ec7a30b961ba 100644
--- a/app/code/Magento/MediaContentCatalog/etc/events.xml
+++ b/app/code/Magento/MediaContentCatalog/etc/events.xml
@@ -9,6 +9,12 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaContentCms/Observer/BlockDelete.php b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php
new file mode 100644
index 0000000000000..582f0a9ec6701
--- /dev/null
+++ b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php
@@ -0,0 +1,119 @@
+extractAssetsFromContent = $extractAssetsFromContent;
+ $this->getContent = $getContent;
+ $this->deleteContentAssetLinks = $deleteContentAssetLinks;
+ $this->contentAssetLinkFactory = $contentAssetLinkFactory;
+ $this->contentIdentityFactory = $contentIdentityFactory;
+ $this->fields = $fields;
+ }
+
+ /**
+ * Retrieve the deleted category and remove relation betwen category and asset
+ *
+ * @param Observer $observer
+ * @throws \Exception
+ */
+ public function execute(Observer $observer): void
+ {
+ $block = $observer->getEvent()->getData('object');
+ $contentAssetLinks = [];
+
+ if ($block instanceof CmsBlock) {
+ foreach ($this->fields as $field) {
+ $contentIdentity = $this->contentIdentityFactory->create(
+ [
+ self::TYPE => self::CONTENT_TYPE,
+ self::FIELD => $field,
+ self::ENTITY_ID => (string) $block->getId(),
+ ]
+ );
+ $assets = $this->extractAssetsFromContent->execute((string) $block->getData($field));
+
+ foreach ($assets as $asset) {
+ $contentAssetLinks[] = $this->contentAssetLinkFactory->create(
+ [
+ 'assetId' => $asset->getId(),
+ 'contentIdentity' => $contentIdentity
+ ]
+ );
+ }
+ }
+ if (!empty($contentAssetLinks)) {
+ $this->deleteContentAssetLinks->execute($contentAssetLinks);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/MediaContentCms/Observer/PageDelete.php b/app/code/Magento/MediaContentCms/Observer/PageDelete.php
new file mode 100644
index 0000000000000..96d2bf89873bd
--- /dev/null
+++ b/app/code/Magento/MediaContentCms/Observer/PageDelete.php
@@ -0,0 +1,120 @@
+extractAssetsFromContent = $extractAssetsFromContent;
+ $this->getContent = $getContent;
+ $this->deleteContentAssetLinks = $deleteContentAssetLinks;
+ $this->contentAssetLinkFactory = $contentAssetLinkFactory;
+ $this->contentIdentityFactory = $contentIdentityFactory;
+ $this->fields = $fields;
+ }
+
+ /**
+ * Retrieve the deleted category and remove relation betwen category and asset
+ *
+ * @param Observer $observer
+ * @throws \Exception
+ */
+ public function execute(Observer $observer): void
+ {
+ $page = $observer->getEvent()->getData('object');
+ $contentAssetLinks = [];
+
+ if ($page instanceof CmsPage) {
+ foreach ($this->fields as $field) {
+ $contentIdentity = $this->contentIdentityFactory->create(
+ [
+ self::TYPE => self::CONTENT_TYPE,
+ self::FIELD => $field,
+ self::ENTITY_ID => (string) $page->getId(),
+ ]
+ );
+
+ $assets = $this->extractAssetsFromContent->execute((string) $page->getData($field));
+
+ foreach ($assets as $asset) {
+ $contentAssetLinks[] = $this->contentAssetLinkFactory->create(
+ [
+ 'assetId' => $asset->getId(),
+ 'contentIdentity' => $contentIdentity
+ ]
+ );
+ }
+ }
+ if (!empty($contentAssetLinks)) {
+ $this->deleteContentAssetLinks->execute($contentAssetLinks);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/MediaContentCms/etc/di.xml b/app/code/Magento/MediaContentCms/etc/di.xml
index f980936465faf..6f196889540af 100644
--- a/app/code/Magento/MediaContentCms/etc/di.xml
+++ b/app/code/Magento/MediaContentCms/etc/di.xml
@@ -20,4 +20,18 @@
+
+
+
+ - content
+
+
+
+
+
+
+ - content
+
+
+
diff --git a/app/code/Magento/MediaContentCms/etc/events.xml b/app/code/Magento/MediaContentCms/etc/events.xml
index 7e9abe3bf19c4..94f963f40be15 100644
--- a/app/code/Magento/MediaContentCms/etc/events.xml
+++ b/app/code/Magento/MediaContentCms/etc/events.xml
@@ -6,8 +6,14 @@
*/
-->
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGallery/Model/Asset.php b/app/code/Magento/MediaGallery/Model/Asset.php
index 78b9477a70b08..7a4e51709dc0a 100644
--- a/app/code/Magento/MediaGallery/Model/Asset.php
+++ b/app/code/Magento/MediaGallery/Model/Asset.php
@@ -32,11 +32,21 @@ class Asset implements AssetInterface
*/
private $title;
+ /**
+ * @var string|null
+ */
+ private $description;
+
/**
* @var string|null
*/
private $source;
+ /**
+ * @var string|null
+ */
+ private $hash;
+
/**
* @var string
*/
@@ -80,7 +90,9 @@ class Asset implements AssetInterface
* @param int $size
* @param int|null $id
* @param string|null $title
+ * @param string|null $description
* @param string|null $source
+ * @param string|null $hash
* @param string|null $createdAt
* @param string|null $updatedAt
* @param AssetExtensionInterface|null $extensionAttributes
@@ -93,7 +105,9 @@ public function __construct(
int $size,
?int $id = null,
?string $title = null,
+ ?string $description = null,
?string $source = null,
+ ?string $hash = null,
?string $createdAt = null,
?string $updatedAt = null,
?AssetExtensionInterface $extensionAttributes = null
@@ -105,7 +119,9 @@ public function __construct(
$this->size = $size;
$this->id = $id;
$this->title = $title;
+ $this->description = $description;
$this->source = $source;
+ $this->hash = $hash;
$this->createdAt = $createdAt;
$this->updatedAt = $updatedAt;
$this->extensionAttributes = $extensionAttributes;
@@ -135,6 +151,14 @@ public function getTitle(): ?string
return $this->title;
}
+ /**
+ * @inheritdoc
+ */
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
/**
* @inheritdoc
*/
@@ -143,6 +167,14 @@ public function getSource(): ?string
return $this->source;
}
+ /**
+ * @inheritdoc
+ */
+ public function getHash(): ?string
+ {
+ return $this->hash;
+ }
+
/**
* @inheritdoc
*/
diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php
index b2f900233e46a..71e2cb70663f3 100644
--- a/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php
+++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php
@@ -94,7 +94,9 @@ public function execute(int $mediaAssetId): AssetInterface
'id' => $mediaAssetData['id'],
'path' => $mediaAssetData['path'],
'title' => $mediaAssetData['title'],
+ 'description' => $mediaAssetData['description'],
'source' => $mediaAssetData['source'],
+ 'hash' => $mediaAssetData['hash'],
'contentType' => $mediaAssetData['content_type'],
'width' => $mediaAssetData['width'],
'height' => $mediaAssetData['height'],
diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php
index d9faad62b2cd1..02512a12f9d07 100644
--- a/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php
+++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php
@@ -86,7 +86,9 @@ public function execute(string $path): AssetInterface
'id' => $data['id'],
'path' => $data['path'],
'title' => $data['title'],
+ 'description' => $data['description'],
'source' => $data['source'],
+ 'hash' => $data['hash'],
'contentType' => $data['content_type'],
'width' => $data['width'],
'height' => $data['height'],
diff --git a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php
index 4d87c1aa95285..f33c22a18b4b8 100644
--- a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php
+++ b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php
@@ -10,7 +10,7 @@
use Magento\Cms\Model\Wysiwyg\Images\Storage;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface;
-use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface;
+use Magento\MediaGalleryApi\Api\IsPathExcludedInterface;
use Psr\Log\LoggerInterface;
/**
@@ -29,23 +29,23 @@ class CreateByPaths implements CreateDirectoriesByPathsInterface
private $storage;
/**
- * @var IsPathBlacklistedInterface
+ * @var IsPathExcludedInterface
*/
- private $isPathBlacklisted;
+ private $isPathExcluded;
/**
* @param LoggerInterface $logger
* @param Storage $storage
- * @param IsPathBlacklistedInterface $isPathBlacklisted
+ * @param IsPathExcludedInterface $isPathExcluded
*/
public function __construct(
LoggerInterface $logger,
Storage $storage,
- IsPathBlacklistedInterface $isPathBlacklisted
+ IsPathExcludedInterface $isPathExcluded
) {
$this->logger = $logger;
$this->storage = $storage;
- $this->isPathBlacklisted = $isPathBlacklisted;
+ $this->isPathExcluded = $isPathExcluded;
}
/**
@@ -55,7 +55,7 @@ public function execute(array $paths): void
{
$failedPaths = [];
foreach ($paths as $path) {
- if ($this->isPathBlacklisted->execute($path)) {
+ if ($this->isPathExcluded->execute($path)) {
$failedPaths[] = $path;
continue;
}
diff --git a/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php
index d46fb854fff22..2e45000c07225 100644
--- a/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php
+++ b/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php
@@ -10,7 +10,7 @@
use Magento\Cms\Model\Wysiwyg\Images\Storage;
use Magento\Framework\Exception\CouldNotDeleteException;
use Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface;
-use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface;
+use Magento\MediaGalleryApi\Api\IsPathExcludedInterface;
use Psr\Log\LoggerInterface;
/**
@@ -29,23 +29,23 @@ class DeleteByPaths implements DeleteDirectoriesByPathsInterface
private $storage;
/**
- * @var IsPathBlacklistedInterface
+ * @var IsPathExcludedInterface
*/
- private $isPathBlacklisted;
+ private $isPathExcluded;
/**
* @param LoggerInterface $logger
* @param Storage $storage
- * @param IsPathBlacklistedInterface $isPathBlacklisted
+ * @param IsPathExcludedInterface $isPathExcluded
*/
public function __construct(
LoggerInterface $logger,
Storage $storage,
- IsPathBlacklistedInterface $isPathBlacklisted
+ IsPathExcludedInterface $isPathExcluded
) {
$this->logger = $logger;
$this->storage = $storage;
- $this->isPathBlacklisted = $isPathBlacklisted;
+ $this->isPathExcluded = $isPathExcluded;
}
/**
@@ -55,7 +55,7 @@ public function execute(array $paths): void
{
$failedPaths = [];
foreach ($paths as $path) {
- if ($this->isPathBlacklisted->execute($path)) {
+ if ($this->isPathExcluded->execute($path)) {
$failedPaths[] = $path;
continue;
}
diff --git a/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php b/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php
index 91f16d246f636..3d9911c805efb 100644
--- a/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php
+++ b/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php
@@ -15,9 +15,9 @@
class Converter implements ConverterInterface
{
/**
- * Blacklist tag name
+ * Excluded list tag name
*/
- private const BLACKLIST_TAG_NAME = 'blacklist';
+ private const EXCLUDED_LIST_TAG_NAME = 'exclude';
/**
* Patterns tag name
@@ -43,12 +43,12 @@ public function convert($source): array
throw new \InvalidArgumentException('The source should be instance of DOMDocument');
}
- foreach ($source->getElementsByTagName(self::BLACKLIST_TAG_NAME) as $blacklist) {
- $result[self::BLACKLIST_TAG_NAME] = [];
- foreach ($blacklist->getElementsByTagName(self::PATTERNS_TAG_NAME) as $patterns) {
- $result[self::BLACKLIST_TAG_NAME][self::PATTERNS_TAG_NAME] = [];
+ foreach ($source->getElementsByTagName(self::EXCLUDED_LIST_TAG_NAME) as $excludedList) {
+ $result[self::EXCLUDED_LIST_TAG_NAME] = [];
+ foreach ($excludedList->getElementsByTagName(self::PATTERNS_TAG_NAME) as $patterns) {
+ $result[self::EXCLUDED_LIST_TAG_NAME][self::PATTERNS_TAG_NAME] = [];
foreach ($patterns->getElementsByTagName(self::PATTERN_TAG_NAME) as $pattern) {
- $result[self::BLACKLIST_TAG_NAME][self::PATTERNS_TAG_NAME]
+ $result[self::EXCLUDED_LIST_TAG_NAME][self::PATTERNS_TAG_NAME]
[$pattern->attributes->getNamedItem('name')->nodeValue] = $pattern->nodeValue;
}
}
diff --git a/app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php b/app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php
similarity index 68%
rename from app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php
rename to app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php
index 8fdd4f70d5060..29ed5fbf04ecd 100644
--- a/app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php
+++ b/app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php
@@ -8,14 +8,14 @@
namespace Magento\MediaGallery\Model\Directory;
use Magento\Framework\Config\DataInterface;
-use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface;
+use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface;
/**
* Media gallery directory config
*/
-class BlacklistPatternsConfig implements BlacklistPatternsConfigInterface
+class ExcludedPatternsConfig implements ExcludedPatternsConfigInterface
{
- private const XML_PATH_BLACKLIST_PATTERNS = 'blacklist/patterns';
+ private const XML_PATH_EXCLUDED_PATTERNS = 'exclude/patterns';
/**
* @var DataInterface
@@ -37,6 +37,6 @@ public function __construct(DataInterface $data)
*/
public function get() : array
{
- return $this->data->get(self::XML_PATH_BLACKLIST_PATTERNS);
+ return $this->data->get(self::XML_PATH_EXCLUDED_PATTERNS);
}
}
diff --git a/app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php b/app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php
similarity index 61%
rename from app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php
rename to app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php
index 0191b357aaefa..8fb0e03b76548 100644
--- a/app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php
+++ b/app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php
@@ -7,23 +7,23 @@
namespace Magento\MediaGallery\Model\Directory;
-use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface;
-use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface;
+use Magento\MediaGalleryApi\Api\IsPathExcludedInterface;
+use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface;
/**
- * Check if the path is blacklisted for media gallery. Directory path may be blacklisted if it's reserved by the system
+ * Check if the path is excluded for media gallery. Directory path may be blacklisted if it's reserved by the system
*/
-class IsBlacklisted implements IsPathBlacklistedInterface
+class IsExcluded implements IsPathExcludedInterface
{
/**
- * @var BlacklistPatternsConfigInterface
+ * @var ExcludedPatternsConfigInterface
*/
private $config;
/**
- * @param BlacklistPatternsConfigInterface $config
+ * @param ExcludedPatternsConfigInterface $config
*/
- public function __construct(BlacklistPatternsConfigInterface $config)
+ public function __construct(ExcludedPatternsConfigInterface $config)
{
$this->config = $config;
}
diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php
index 53185939b2283..f73162b775683 100644
--- a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php
+++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php
@@ -65,7 +65,9 @@ public function execute(array $ids): array
'id' => $assetData['id'],
'path' => $assetData['path'],
'title' => $assetData['title'],
+ 'description' => $assetData['description'],
'source' => $assetData['source'],
+ 'hash' => $assetData['hash'],
'contentType' => $assetData['content_type'],
'width' => $assetData['width'],
'height' => $assetData['height'],
diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php
index 5593083d9673a..b25d2e22aabd4 100644
--- a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php
+++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php
@@ -66,7 +66,9 @@ public function execute(array $paths): array
'id' => $assetData['id'],
'path' => $assetData['path'],
'title' => $assetData['title'],
+ 'description' => $assetData['description'],
'source' => $assetData['source'],
+ 'hash' => $assetData['hash'],
'contentType' => $assetData['content_type'],
'width' => $assetData['width'],
'height' => $assetData['height'],
diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php b/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php
index ec08addf93462..801279aa7fd7d 100644
--- a/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php
+++ b/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php
@@ -60,7 +60,9 @@ public function execute(array $assets): void
'id' => $asset->getId(),
'path' => $asset->getPath(),
'title' => $asset->getTitle(),
+ 'description' => $asset->getDescription(),
'source' => $asset->getSource(),
+ 'hash' => $asset->getHash(),
'content_type' => $asset->getContentType(),
'width' => $asset->getWidth(),
'height' => $asset->getHeight(),
diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php
index 09ce7ffe8ff20..5f99163db8f12 100644
--- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php
+++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php
@@ -28,7 +28,9 @@ class GetByIdExceptionDuringMediaAssetInitializationTest extends TestCase
'id' => 45,
'path' => 'img.jpg',
'title' => 'Img',
+ 'description' => 'Img Description',
'source' => 'Adobe Stock',
+ 'hash' => 'hash',
'content_type' => 'image/jpeg',
'width' => 420,
'height' => 240,
diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php
index 89efae07360b4..3b47b0036224b 100644
--- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php
+++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php
@@ -29,7 +29,9 @@ class GetByIdExceptionOnGetDataTest extends TestCase
'id' => 45,
'path' => 'img.jpg',
'title' => 'Img',
+ 'description' => 'Img Description',
'source' => 'Adobe Stock',
+ 'hash' => 'hash',
'content_type' => 'image/jpeg',
'width' => 420,
'height' => 240,
diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php
index 8b805d0256e37..2c24899746473 100644
--- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php
+++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php
@@ -29,7 +29,9 @@ class GetByIdSuccessfulTest extends TestCase
'id' => 45,
'path' => 'img.jpg',
'title' => 'Img',
+ 'description' => 'Img Description',
'source' => 'Adobe Stock',
+ 'hash' => 'hash',
'content_type' => 'image/jpeg',
'width' => 420,
'height' => 240,
diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php
similarity index 70%
rename from app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php
rename to app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php
index c96fd2ee54512..cc57b043954d7 100644
--- a/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php
+++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php
@@ -8,45 +8,45 @@
namespace Magento\MediaGallery\Test\Unit\Model\Directory;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
-use Magento\MediaGallery\Model\Directory\IsBlacklisted;
-use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface;
+use Magento\MediaGallery\Model\Directory\IsExcluded;
+use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
/**
- * Test for IsBlacklisted
+ * Test for IsExcluded
*/
-class IsBlacklistedTest extends TestCase
+class IsExcludedTest extends TestCase
{
/**
- * @var IsBlacklisted
+ * @var IsExcluded
*/
private $object;
/**
- * @var BlacklistPatternsConfigInterface|MockObject
+ * @var ExcludedPatternsConfigInterface|MockObject
*/
- private $config;
+ private $configMock;
/**
* Initialize basic test class mocks
*/
protected function setUp(): void
{
- $this->config = $this->getMockBuilder(BlacklistPatternsConfigInterface::class)
+ $this->configMock = $this->getMockBuilder(ExcludedPatternsConfigInterface::class)
->disableOriginalConstructor()
->getMockForAbstractClass();
- $this->config->expects($this->at(0))->method('get')->willReturn([
+ $this->configMock->expects($this->at(0))->method('get')->willReturn([
'tmp' => '/pub\/media\/tmp/',
'captcha' => '/pub\/media\/captcha/'
]);
- $this->object = (new ObjectManager($this))->getObject(IsBlacklisted::class, [
- 'config' => $this->config
+ $this->object = (new ObjectManager($this))->getObject(IsExcluded::class, [
+ 'config' => $this->configMock
]);
}
/**
- * Test if the directory path is blacklisted
+ * Test if the directory path is excluded
*
* @param string $path
* @param bool $isExcluded
diff --git a/app/code/Magento/MediaGallery/etc/db_schema.xml b/app/code/Magento/MediaGallery/etc/db_schema.xml
index 31a764ef00c4d..1001737daa8a7 100644
--- a/app/code/Magento/MediaGallery/etc/db_schema.xml
+++ b/app/code/Magento/MediaGallery/etc/db_schema.xml
@@ -10,7 +10,9 @@
+
+
diff --git a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json
index 8f5098caa9753..b32dfbf082175 100644
--- a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json
+++ b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json
@@ -4,7 +4,9 @@
"id": true,
"path": true,
"title": true,
+ "description": true,
"source": true,
+ "hash": true,
"content_type": true,
"width": true,
"height": true,
diff --git a/app/code/Magento/MediaGallery/etc/di.xml b/app/code/Magento/MediaGallery/etc/di.xml
index a85c26e275226..bedb78758786b 100644
--- a/app/code/Magento/MediaGallery/etc/di.xml
+++ b/app/code/Magento/MediaGallery/etc/di.xml
@@ -21,7 +21,7 @@
-
+
@@ -40,7 +40,7 @@
Magento\MediaGallery\Model\Directory\Config\Converter
Magento\MediaGallery\Model\Directory\Config\SchemaLocator
- - name
+ - name
@@ -50,11 +50,10 @@
Media_Gallery_Patterns_CacheId
-
+
Magento\MediaGallery\Model\Directory\Config\Data
-
-
+
diff --git a/app/code/Magento/MediaGallery/etc/directory.xml b/app/code/Magento/MediaGallery/etc/directory.xml
index 92f50b2dd0a30..42094aff72640 100644
--- a/app/code/Magento/MediaGallery/etc/directory.xml
+++ b/app/code/Magento/MediaGallery/etc/directory.xml
@@ -6,7 +6,7 @@
*/
-->
-
+
/^captcha/
/^customer/
@@ -17,5 +17,5 @@
/^tmp/
/^\./
-
+
diff --git a/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php
index 5df420a274933..a747cb963baab 100644
--- a/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php
+++ b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php
@@ -38,6 +38,13 @@ public function getPath(): string;
*/
public function getTitle(): ?string;
+ /**
+ * Get description
+ *
+ * @return string|null
+ */
+ public function getDescription(): ?string;
+
/**
* Get the name of the channel/stock/integration file was retrieved from. null if not identified.
*
@@ -45,6 +52,13 @@ public function getTitle(): ?string;
*/
public function getSource(): ?string;
+ /**
+ * Get file hash
+ *
+ * @return string|null
+ */
+ public function getHash(): ?string;
+
/**
* Get content type
*
diff --git a/app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php b/app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php
similarity index 71%
rename from app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php
rename to app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php
index cbd23ec3fbde7..1e41debb1b1c5 100644
--- a/app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php
+++ b/app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php
@@ -8,12 +8,12 @@
namespace Magento\MediaGalleryApi\Api;
/**
- * Check if the path is blacklisted for media gallery.
+ * Check if the path is excluded for media gallery.
*
- * Directory path may be blacklisted if it's reserved by the system.
+ * Directory path may be excluded if it's reserved by the system.
* @api
*/
-interface IsPathBlacklistedInterface
+interface IsPathExcludedInterface
{
/**
* Check if the path is excluded from displaying and processing in the media gallery
diff --git a/app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php b/app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php
similarity index 75%
rename from app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php
rename to app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php
index b4710f32e0c46..dd82f87780a49 100644
--- a/app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php
+++ b/app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php
@@ -7,9 +7,9 @@
namespace Magento\MediaGalleryApi\Model;
/**
- * Returns list of blacklist regexp patterns
+ * Returns list of excluded regexp patterns
*/
-interface BlacklistPatternsConfigInterface
+interface ExcludedPatternsConfigInterface
{
/**
* Get regexp patterns
diff --git a/app/code/Magento/MediaGalleryApi/etc/directory.xsd b/app/code/Magento/MediaGalleryApi/etc/directory.xsd
index 2ad76c8fcc9f2..2fb4fed028469 100644
--- a/app/code/Magento/MediaGalleryApi/etc/directory.xsd
+++ b/app/code/Magento/MediaGalleryApi/etc/directory.xsd
@@ -11,14 +11,14 @@
-
+
-
+
- Blacklist used for excluding directories from media gallery rendering and operations
+ List used for excluding directories from media gallery rendering and operations
diff --git a/app/code/Magento/MediaGalleryCatalog/etc/directory.xml b/app/code/Magento/MediaGalleryCatalog/etc/directory.xml
index eaced3f642f70..f1ec76a877368 100644
--- a/app/code/Magento/MediaGalleryCatalog/etc/directory.xml
+++ b/app/code/Magento/MediaGalleryCatalog/etc/directory.xml
@@ -6,9 +6,9 @@
*/
-->
-
+
/^catalog\/product/
-
+
diff --git a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml
index 429482e5795bf..768c97ef316f7 100644
--- a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml
+++ b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml
@@ -40,3 +40,12 @@
+
diff --git a/app/code/Magento/Payment/Block/Transparent/Redirect.php b/app/code/Magento/Payment/Block/Transparent/Redirect.php
index 1be6dec4cc1d8..97a09df38d120 100644
--- a/app/code/Magento/Payment/Block/Transparent/Redirect.php
+++ b/app/code/Magento/Payment/Block/Transparent/Redirect.php
@@ -53,10 +53,21 @@ public function getRedirectUrl(): string
/**
* Returns params to be redirected.
*
+ * Encodes invalid UTF-8 values to UTF-8 to prevent character escape error.
+ * Some payment methods like PayPal, send data in merchant defined language encoding
+ * which can be different from the system character encoding (UTF-8).
+ *
* @return array
*/
public function getPostParams(): array
{
- return (array)$this->_request->getPostValue();
+ $params = [];
+ foreach ($this->_request->getPostValue() as $name => $value) {
+ if (!empty($value) && mb_detect_encoding($value, 'UTF-8', true) === false) {
+ $value = utf8_encode($value);
+ }
+ $params[$name] = $value;
+ }
+ return $params;
}
}
diff --git a/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php
new file mode 100644
index 0000000000000..1cd1230a14634
--- /dev/null
+++ b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php
@@ -0,0 +1,102 @@
+context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class);
+ $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class);
+ $this->context->method('getRequest')
+ ->willReturn($this->request);
+ $this->url = $this->createMock(\Magento\Framework\UrlInterface::class);
+ $this->model = new Redirect(
+ $this->context,
+ $this->url
+ );
+ }
+
+ /**
+ * @param array $postData
+ * @param array $expected
+ * @dataProvider getPostParamsDataProvider
+ */
+ public function testGetPostParams(array $postData, array $expected): void
+ {
+ $this->request->method('getPostValue')
+ ->willReturn($postData);
+ $this->assertEquals($expected, $this->model->getPostParams());
+ }
+
+ /**
+ * @return array
+ */
+ public function getPostParamsDataProvider(): array
+ {
+ return [
+ [
+ [
+ 'BILLTOEMAIL' => 'john.doe@magento.lo',
+ 'BILLTOSTREET' => '3640 Holdrege Ave',
+ 'BILLTOZIP' => '90016',
+ 'BILLTOLASTNAME' => 'Ãtienne',
+ 'BILLTOFIRSTNAME' => 'Ãillin',
+ ],
+ [
+ 'BILLTOEMAIL' => 'john.doe@magento.lo',
+ 'BILLTOSTREET' => '3640 Holdrege Ave',
+ 'BILLTOZIP' => '90016',
+ 'BILLTOLASTNAME' => 'Ãtienne',
+ 'BILLTOFIRSTNAME' => 'Ãillin',
+ ]
+ ],
+ [
+ [
+ 'BILLTOEMAIL' => 'john.doe@magento.lo',
+ 'BILLTOSTREET' => '3640 Holdrege Ave',
+ 'BILLTOZIP' => '90016',
+ 'BILLTOLASTNAME' => mb_convert_encoding('Ãtienne', 'ISO-8859-1'),
+ 'BILLTOFIRSTNAME' => mb_convert_encoding('Ãillin', 'ISO-8859-1'),
+ ],
+ [
+ 'BILLTOEMAIL' => 'john.doe@magento.lo',
+ 'BILLTOSTREET' => '3640 Holdrege Ave',
+ 'BILLTOZIP' => '90016',
+ 'BILLTOLASTNAME' => 'Ãtienne',
+ 'BILLTOFIRSTNAME' => 'Ãillin',
+ ]
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php
index 39148f990b714..4366ef7aaf969 100644
--- a/app/code/Magento/Quote/Model/Quote/Address.php
+++ b/app/code/Magento/Quote/Model/Quote/Address.php
@@ -1019,6 +1019,13 @@ public function collectShippingRates()
*/
public function requestShippingRates(AbstractItem $item = null)
{
+ $storeId = $this->getQuote()->getStoreId() ?: $this->storeManager->getStore()->getId();
+ $taxInclude = $this->_scopeConfig->getValue(
+ 'tax/calculation/price_includes_tax',
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+
/** @var $request RateRequest */
$request = $this->_rateRequestFactory->create();
$request->setAllItems($item ? [$item] : $this->getAllItems());
@@ -1028,9 +1035,11 @@ public function requestShippingRates(AbstractItem $item = null)
$request->setDestStreet($this->getStreetFull());
$request->setDestCity($this->getCity());
$request->setDestPostcode($this->getPostcode());
- $request->setPackageValue($item ? $item->getBaseRowTotal() : $this->getBaseSubtotal());
+ $baseSubtotal = $taxInclude ? $this->getBaseSubtotalTotalInclTax() : $this->getBaseSubtotal();
+ $request->setPackageValue($item ? $item->getBaseRowTotal() : $baseSubtotal);
+ $baseSubtotalWithDiscount = $baseSubtotal + $this->getBaseDiscountAmount();
$packageWithDiscount = $item ? $item->getBaseRowTotal() -
- $item->getBaseDiscountAmount() : $this->getBaseSubtotalWithDiscount();
+ $item->getBaseDiscountAmount() : $baseSubtotalWithDiscount;
$request->setPackageValueWithDiscount($packageWithDiscount);
$request->setPackageWeight($item ? $item->getRowWeight() : $this->getWeight());
$request->setPackageQty($item ? $item->getQty() : $this->getItemQty());
@@ -1038,8 +1047,7 @@ public function requestShippingRates(AbstractItem $item = null)
/**
* Need for shipping methods that use insurance based on price of physical products
*/
- $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $this->getBaseSubtotal() -
- $this->getBaseVirtualAmount();
+ $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $baseSubtotal - $this->getBaseVirtualAmount();
$request->setPackagePhysicalValue($packagePhysicalValue);
$request->setFreeMethodWeight($item ? 0 : $this->getFreeMethodWeight());
@@ -1047,12 +1055,10 @@ public function requestShippingRates(AbstractItem $item = null)
/**
* Store and website identifiers specified from StoreManager
*/
+ $request->setStoreId($storeId);
if ($this->getQuote()->getStoreId()) {
- $storeId = $this->getQuote()->getStoreId();
- $request->setStoreId($storeId);
$request->setWebsiteId($this->storeManager->getStore($storeId)->getWebsiteId());
} else {
- $request->setStoreId($this->storeManager->getStore()->getId());
$request->setWebsiteId($this->storeManager->getWebsite()->getId());
}
$request->setFreeShipping($this->getFreeShipping());
diff --git a/app/code/Magento/Quote/Model/Quote/Item/Processor.php b/app/code/Magento/Quote/Model/Quote/Item/Processor.php
index ef4b853862681..c6bef1cc80bfb 100644
--- a/app/code/Magento/Quote/Model/Quote/Item/Processor.php
+++ b/app/code/Magento/Quote/Model/Quote/Item/Processor.php
@@ -97,7 +97,9 @@ public function prepare(Item $item, DataObject $request, Product $candidate): vo
$item->addQty($candidate->getCartQty());
$customPrice = $request->getCustomPrice();
- $item->setPrice($candidate->getFinalPrice());
+ if (!$item->getParentItem() || $item->getParentItem()->isChildrenCalculated()) {
+ $item->setPrice($candidate->getFinalPrice());
+ }
if (!empty($customPrice)) {
$item->setCustomPrice($customPrice);
$item->setOriginalCustomPrice($customPrice);
diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote.php b/app/code/Magento/Quote/Model/ResourceModel/Quote.php
index 48945dacd1738..749e9944a6ad3 100644
--- a/app/code/Magento/Quote/Model/ResourceModel/Quote.php
+++ b/app/code/Magento/Quote/Model/ResourceModel/Quote.php
@@ -230,7 +230,8 @@ public function subtractProductFromQuotes($product)
'items_qty' => new \Zend_Db_Expr(
$connection->quoteIdentifier('q.items_qty') . ' - ' . $connection->quoteIdentifier('qi.qty')
),
- 'items_count' => new \Zend_Db_Expr($ifSql)
+ 'items_count' => new \Zend_Db_Expr($ifSql),
+ 'updated_at' => 'q.updated_at',
]
)->join(
['qi' => $this->getTable('quote_item')],
@@ -277,21 +278,27 @@ public function markQuotesRecollect($productIds)
{
$tableQuote = $this->getTable('quote');
$tableItem = $this->getTable('quote_item');
- $subSelect = $this->getConnection()->select()->from(
- $tableItem,
- ['entity_id' => 'quote_id']
- )->where(
- 'product_id IN ( ? )',
- $productIds
- )->group(
- 'quote_id'
- );
-
- $select = $this->getConnection()->select()->join(
- ['t2' => $subSelect],
- 't1.entity_id = t2.entity_id',
- ['trigger_recollect' => new \Zend_Db_Expr('1')]
- );
+ $subSelect = $this->getConnection()
+ ->select()
+ ->from(
+ $tableItem,
+ ['entity_id' => 'quote_id']
+ )->where(
+ 'product_id IN ( ? )',
+ $productIds
+ )->group(
+ 'quote_id'
+ );
+ $select = $this->getConnection()
+ ->select()
+ ->join(
+ ['t2' => $subSelect],
+ 't1.entity_id = t2.entity_id',
+ [
+ 'trigger_recollect' => new \Zend_Db_Expr('1'),
+ 'updated_at' => 't1.updated_at',
+ ]
+ );
$updateQuery = $select->crossUpdateFromSelect(['t1' => $tableQuote]);
$this->getConnection()->query($updateQuery);
diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml
new file mode 100755
index 0000000000000..a14be3b533fa8
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+ ShippingAddressTX
+ BillingAddressTX
+ flatrate
+ flatrate
+
+
+
+
+ PaymentMethodCheckMoneyOrder
+ BillingAddressTX
+
+
diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml
new file mode 100644
index 0000000000000..3681245311188
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ 1
+
+
diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml
new file mode 100644
index 0000000000000..f5555394f8d4d
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ application/json
+
+ string
+ string
+ integer
+
+
+
diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml
new file mode 100644
index 0000000000000..f233954f2cdcf
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+ application/json
+ string
+
+
+
+ application/json
+ string
+
+
+ string
+ string
+ string
+ integer
+ string
+
+ string
+
+ string
+ string
+ string
+ string
+ string
+
+
+ string
+ string
+ string
+ integer
+ string
+
+ string
+
+ string
+ string
+ string
+ string
+ string
+
+ string
+ string
+
+
+
+
+ application/json
+ string
+
+ string
+
+
+
diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
index a8fd794c08757..d4f6778a2ccb8 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
@@ -352,10 +352,40 @@ public function testRequestShippingRates()
$currentCurrencyCode = 'UAH';
+ $this->quote->expects($this->any())
+ ->method('getStoreId')
+ ->willReturn($storeId);
+
+ $this->storeManager->expects($this->at(0))
+ ->method('getStore')
+ ->with($storeId)
+ ->willReturn($this->store);
+ $this->store->expects($this->any())
+ ->method('getWebsiteId')
+ ->willReturn($webSiteId);
+
+ $this->scopeConfig->expects($this->exactly(1))
+ ->method('getValue')
+ ->with(
+ 'tax/calculation/price_includes_tax',
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ )
+ ->willReturn(1);
+
/** @var RateRequest */
$request = $this->getMockBuilder(RateRequest::class)
->disableOriginalConstructor()
- ->setMethods(['setStoreId', 'setWebsiteId', 'setBaseCurrency', 'setPackageCurrency'])
+ ->setMethods(
+ [
+ 'setStoreId',
+ 'setWebsiteId',
+ 'setBaseCurrency',
+ 'setPackageCurrency',
+ 'getBaseSubtotalTotalInclTax',
+ 'getBaseSubtotal'
+ ]
+ )
->getMock();
/** @var Collection */
@@ -434,13 +464,6 @@ public function testRequestShippingRates()
$this->storeManager->method('getStore')
->willReturn($this->store);
- $this->storeManager->expects($this->once())
- ->method('getWebsite')
- ->willReturn($this->website);
-
- $this->store->method('getId')
- ->willReturn($storeId);
-
$this->store->method('getBaseCurrency')
->willReturn($baseCurrency);
@@ -452,10 +475,6 @@ public function testRequestShippingRates()
->method('getCurrentCurrencyCode')
->willReturn($currentCurrencyCode);
- $this->website->expects($this->once())
- ->method('getId')
- ->willReturn($webSiteId);
-
$this->addressRateFactory->expects($this->once())
->method('create')
->willReturn($rate);
diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php
index cbcb7dd0adc3c..3025a72410671 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php
@@ -77,7 +77,16 @@ protected function setUp(): void
$this->itemMock = $this->getMockBuilder(Item::class)
->addMethods(['setOriginalCustomPrice'])
- ->onlyMethods(['getId', 'setOptions', 'setProduct', 'addQty', 'setCustomPrice', 'setData', 'setPrice'])
+ ->onlyMethods([
+ 'getId',
+ 'setOptions',
+ 'setProduct',
+ 'addQty',
+ 'setCustomPrice',
+ 'setData',
+ 'setPrice',
+ 'getParentItem'
+ ])
->disableOriginalConstructor()
->getMock();
$this->quoteItemFactoryMock->expects($this->any())
@@ -438,4 +447,41 @@ public function testPrepareWithResetCountAndNotStickAndSameItemId()
$this->processor->prepare($this->itemMock, $this->objectMock, $this->productMock);
}
+
+ /**
+ * @param bool $isChildrenCalculated
+ * @dataProvider prepareChildProductDataProvider
+ */
+ public function testPrepareChildProduct(bool $isChildrenCalculated): void
+ {
+ $finalPrice = 10;
+ $this->objectMock->method('getResetCount')
+ ->willReturn(false);
+ $this->productMock->method('getFinalPrice')
+ ->willReturn($finalPrice);
+ $this->itemMock->expects($isChildrenCalculated ? $this->once() : $this->never())
+ ->method('setPrice')
+ ->with($finalPrice)
+ ->willReturnSelf();
+ $parentItem = $this->createConfiguredMock(
+ \Magento\Quote\Model\Quote\Item::class,
+ [
+ 'isChildrenCalculated' => $isChildrenCalculated
+ ]
+ );
+ $this->itemMock->method('getParentItem')
+ ->willReturn($parentItem);
+ $this->processor->prepare($this->itemMock, $this->objectMock, $this->productMock);
+ }
+
+ /**
+ * @return array
+ */
+ public function prepareChildProductDataProvider(): array
+ {
+ return [
+ [false],
+ [true]
+ ];
+ }
}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php
index f73daa715c1df..e959c19a7cbe4 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php
@@ -51,7 +51,10 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s
$shippingAddressInput = current($shippingAddressesInput) ?? [];
$customerAddressId = $shippingAddressInput['customer_address_id'] ?? null;
- if (!$customerAddressId && !isset($shippingAddressInput['address']['save_in_address_book'])) {
+ if (!$customerAddressId
+ && isset($shippingAddressInput['address'])
+ && !isset($shippingAddressInput['address']['save_in_address_book'])
+ ) {
$shippingAddressInput['address']['save_in_address_book'] = true;
}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php
index b2526bdc04e98..654a4bb558632 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php
@@ -42,12 +42,12 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s
}
$shippingMethodInput = current($shippingMethodsInput);
- if (!isset($shippingMethodInput['carrier_code']) || empty($shippingMethodInput['carrier_code'])) {
+ if (empty($shippingMethodInput['carrier_code'])) {
throw new GraphQlInputException(__('Required parameter "carrier_code" is missing.'));
}
$carrierCode = $shippingMethodInput['carrier_code'];
- if (!isset($shippingMethodInput['method_code']) || empty($shippingMethodInput['method_code'])) {
+ if (empty($shippingMethodInput['method_code'])) {
throw new GraphQlInputException(__('Required parameter "method_code" is missing.'));
}
$methodCode = $shippingMethodInput['method_code'];
diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php
new file mode 100644
index 0000000000000..c2e94b215956e
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php
@@ -0,0 +1,157 @@
+cartItemRepository = $cartItemRepository;
+ $this->updateCartItem = $updateCartItem;
+ $this->itemRepository = $itemRepository;
+ $this->giftMessageHelper = $giftMessageHelper;
+ $this->giftMessageFactory = $giftMessageFactory;
+ }
+
+ /**
+ * Process cart items
+ *
+ * @param Quote $cart
+ * @param array $items
+ *
+ * @throws GraphQlInputException
+ * @throws LocalizedException
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
+ */
+ public function processCartItems(Quote $cart, array $items): void
+ {
+ foreach ($items as $item) {
+ if (empty($item['cart_item_id'])) {
+ throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.'));
+ }
+
+ $itemId = (int)$item['cart_item_id'];
+ $customizableOptions = $item['customizable_options'] ?? [];
+ $cartItem = $cart->getItemById($itemId);
+
+ if ($cartItem && $cartItem->getParentItemId()) {
+ throw new GraphQlInputException(__('Child items may not be updated.'));
+ }
+
+ if (count($customizableOptions) === 0 && !isset($item['quantity'])) {
+ throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.'));
+ }
+
+ $quantity = (float)$item['quantity'];
+
+ if ($quantity <= 0.0) {
+ $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId);
+ } else {
+ $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions);
+ }
+
+ if (!empty($item['gift_message'])) {
+ try {
+ if (!$this->giftMessageHelper->isMessagesAllowed('items', $cartItem)) {
+ continue;
+ }
+ if (!$this->giftMessageHelper->isMessagesAllowed('item', $cartItem)) {
+ continue;
+ }
+
+ /** @var MessageInterface $giftItemMessage */
+ $giftItemMessage = $this->itemRepository->get($cart->getEntityId(), $itemId);
+
+ if (empty($giftItemMessage)) {
+ /** @var MessageInterface $giftMessage */
+ $giftMessage = $this->giftMessageFactory->create();
+ $this->updateGiftMessageForItem($cart, $giftMessage, $item, $itemId);
+ continue;
+ }
+ } catch (LocalizedException $exception) {
+ throw new GraphQlInputException(__('Gift Message cannot be updated.'));
+ }
+
+ $this->updateGiftMessageForItem($cart, $giftItemMessage, $item, $itemId);
+ }
+ }
+ }
+
+ /**
+ * Update Gift Message for Quote item
+ *
+ * @param Quote $cart
+ * @param MessageInterface $giftItemMessage
+ * @param array $item
+ * @param int $itemId
+ *
+ * @throws GraphQlInputException
+ */
+ private function updateGiftMessageForItem(Quote $cart, MessageInterface $giftItemMessage, array $item, int $itemId)
+ {
+ try {
+ $giftItemMessage->setRecipient($item['gift_message']['to']);
+ $giftItemMessage->setSender($item['gift_message']['from']);
+ $giftItemMessage->setMessage($item['gift_message']['message']);
+ $this->itemRepository->save($cart->getEntityId(), $giftItemMessage, $itemId);
+ } catch (LocalizedException $exception) {
+ throw new GraphQlInputException(__('Gift Message cannot be updated'));
+ }
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php
index 0be95eccc39e5..e8aa8d612c670 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php
@@ -7,17 +7,12 @@
namespace Magento\QuoteGraphQl\Model\Resolver;
-use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
-use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForCustomer;
use Magento\GraphQl\Model\Query\ContextInterface;
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
-use Magento\Quote\Api\CartManagementInterface;
-use Magento\Quote\Model\QuoteIdMaskFactory;
-use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface;
-use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel;
+use Magento\Quote\Model\Cart\CustomerCartResolver;
/**
* Get cart for the customer
@@ -25,48 +20,19 @@
class CustomerCart implements ResolverInterface
{
/**
- * @var CreateEmptyCartForCustomer
+ * @var CustomerCartResolver
*/
- private $createEmptyCartForCustomer;
+ private $customerCartResolver;
/**
- * @var CartManagementInterface
- */
- private $cartManagement;
-
- /**
- * @var QuoteIdMaskFactory
- */
- private $quoteIdMaskFactory;
-
- /**
- * @var QuoteIdMaskResourceModel
- */
- private $quoteIdMaskResourceModel;
- /**
- * @var QuoteIdToMaskedQuoteIdInterface
- */
- private $quoteIdToMaskedQuoteId;
-
- /**
- * @param CreateEmptyCartForCustomer $createEmptyCartForCustomer
- * @param CartManagementInterface $cartManagement
- * @param QuoteIdMaskFactory $quoteIdMaskFactory
- * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel
- * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId
+ * CustomerCart constructor.
+ *
+ * @param CustomerCartResolver $customerCartResolver
*/
public function __construct(
- CreateEmptyCartForCustomer $createEmptyCartForCustomer,
- CartManagementInterface $cartManagement,
- QuoteIdMaskFactory $quoteIdMaskFactory,
- QuoteIdMaskResourceModel $quoteIdMaskResourceModel,
- QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId
+ CustomerCartResolver $customerCartResolver
) {
- $this->createEmptyCartForCustomer = $createEmptyCartForCustomer;
- $this->cartManagement = $cartManagement;
- $this->quoteIdMaskFactory = $quoteIdMaskFactory;
- $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel;
- $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId;
+ $this->customerCartResolver = $customerCartResolver;
}
/**
@@ -76,22 +42,17 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
{
$currentUserId = $context->getUserId();
- /** @var ContextInterface $context */
+ /**
+ * @var ContextInterface $context
+ */
if (false === $context->getExtensionAttributes()->getIsCustomer()) {
throw new GraphQlAuthorizationException(__('The request is allowed for logged in customer'));
}
- try {
- $cart = $this->cartManagement->getCartForCustomer($currentUserId);
- } catch (NoSuchEntityException $e) {
- $this->createEmptyCartForCustomer->execute($currentUserId, null);
- $cart = $this->cartManagement->getCartForCustomer($currentUserId);
- }
- $maskedId = $this->quoteIdToMaskedQuoteId->execute((int) $cart->getId());
- if (empty($maskedId)) {
- $quoteIdMask = $this->quoteIdMaskFactory->create();
- $quoteIdMask->setQuoteId((int) $cart->getId());
- $this->quoteIdMaskResourceModel->save($quoteIdMask);
+ try {
+ $cart = $this->customerCartResolver->resolve($currentUserId);
+ } catch (\Exception $e) {
+ $cart = null;
}
return [
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php
index dd4ce8fe7f7a6..c2e4bfa44c9bb 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php
@@ -71,14 +71,15 @@ public function __construct(
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
{
- if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) {
+ if (empty($args['input']['cart_id'])) {
throw new GraphQlInputException(__('Required parameter "cart_id" is missing'));
}
- $maskedCartId = $args['input']['cart_id'];
- if (!isset($args['input']['payment_method']['code']) || empty($args['input']['payment_method']['code'])) {
+ if (empty($args['input']['payment_method']['code'])) {
throw new GraphQlInputException(__('Required parameter "code" for "payment_method" is missing.'));
}
+
+ $maskedCartId = $args['input']['cart_id'];
$paymentData = $args['input']['payment_method'];
$storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php
index fa90f08e4b553..005baaad0e1e5 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php
@@ -14,53 +14,43 @@
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
-use Magento\Quote\Api\CartItemRepositoryInterface;
use Magento\Quote\Api\CartRepositoryInterface;
-use Magento\Quote\Model\Quote;
use Magento\QuoteGraphQl\Model\Cart\GetCartForUser;
-use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem;
+use Magento\QuoteGraphQl\Model\CartItem\DataProvider\UpdateCartItems as UpdateCartItemsProvider;
/**
* @inheritdoc
*/
class UpdateCartItems implements ResolverInterface
{
- /**
- * @var UpdateCartItem
- */
- private $updateCartItem;
-
/**
* @var GetCartForUser
*/
private $getCartForUser;
/**
- * @var CartItemRepositoryInterface
+ * @var CartRepositoryInterface
*/
- private $cartItemRepository;
+ private $cartRepository;
/**
- * @var CartRepositoryInterface
+ * @var UpdateCartItemsProvider
*/
- private $cartRepository;
+ private $updateCartItems;
/**
- * @param GetCartForUser $getCartForUser
- * @param CartItemRepositoryInterface $cartItemRepository
- * @param UpdateCartItem $updateCartItem
+ * @param GetCartForUser $getCartForUser
* @param CartRepositoryInterface $cartRepository
+ * @param UpdateCartItemsProvider $updateCartItems
*/
public function __construct(
GetCartForUser $getCartForUser,
- CartItemRepositoryInterface $cartItemRepository,
- UpdateCartItem $updateCartItem,
- CartRepositoryInterface $cartRepository
+ CartRepositoryInterface $cartRepository,
+ UpdateCartItemsProvider $updateCartItems
) {
$this->getCartForUser = $getCartForUser;
- $this->cartItemRepository = $cartItemRepository;
- $this->updateCartItem = $updateCartItem;
$this->cartRepository = $cartRepository;
+ $this->updateCartItems = $updateCartItems;
}
/**
@@ -71,6 +61,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
if (empty($args['input']['cart_id'])) {
throw new GraphQlInputException(__('Required parameter "cart_id" is missing.'));
}
+
$maskedCartId = $args['input']['cart_id'];
if (empty($args['input']['cart_items'])
@@ -78,13 +69,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
) {
throw new GraphQlInputException(__('Required parameter "cart_items" is missing.'));
}
- $cartItems = $args['input']['cart_items'];
+ $cartItems = $args['input']['cart_items'];
$storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
$cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId);
try {
- $this->processCartItems($cart, $cartItems);
+ $this->updateCartItems->processCartItems($cart, $cartItems);
$this->cartRepository->save($cart);
} catch (NoSuchEntityException $e) {
throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e);
@@ -98,39 +89,4 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
],
];
}
-
- /**
- * Process cart items
- *
- * @param Quote $cart
- * @param array $items
- * @throws GraphQlInputException
- * @throws LocalizedException
- */
- private function processCartItems(Quote $cart, array $items): void
- {
- foreach ($items as $item) {
- if (empty($item['cart_item_id'])) {
- throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.'));
- }
- $itemId = (int)$item['cart_item_id'];
- $customizableOptions = $item['customizable_options'] ?? [];
-
- $cartItem = $cart->getItemById($itemId);
- if ($cartItem && $cartItem->getParentItemId()) {
- throw new GraphQlInputException(__('Child items may not be updated.'));
- }
-
- if (count($customizableOptions) === 0 && !isset($item['quantity'])) {
- throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.'));
- }
- $quantity = (float)$item['quantity'];
-
- if ($quantity <= 0.0) {
- $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId);
- } else {
- $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions);
- }
- }
- }
}
diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json
index 0652d39b5f426..25f089cf75a62 100644
--- a/app/code/Magento/QuoteGraphQl/composer.json
+++ b/app/code/Magento/QuoteGraphQl/composer.json
@@ -13,7 +13,8 @@
"magento/module-customer-graph-ql": "*",
"magento/module-sales": "*",
"magento/module-directory": "*",
- "magento/module-graph-ql": "*"
+ "magento/module-graph-ql": "*",
+ "magento/module-gift-message": "*"
},
"suggest": {
"magento/module-graph-ql-cache": "*"
diff --git a/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php b/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php
new file mode 100644
index 0000000000000..a8a9f78df7f28
--- /dev/null
+++ b/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php
@@ -0,0 +1,62 @@
+getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver');
+ /** @var FacebookWebDriver $webDriver */
+ $webDriver = $magentoWebDriver->webDriver;
+ $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow));
+ while (!empty($rows)) {
+ $rows[0]->click();
+ $magentoWebDriver->waitForPageLoad(30);
+ $magentoWebDriver->click($deleteButton);
+ $magentoWebDriver->waitForPageLoad(30);
+ $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10);
+ $magentoWebDriver->waitForPageLoad(60);
+ $magentoWebDriver->click($modalAcceptButton);
+ $magentoWebDriver->waitForPageLoad(60);
+ $magentoWebDriver->waitForLoadingMaskToDisappear();
+ $magentoWebDriver->waitForElementVisible($successMessageContainer, 10);
+ $magentoWebDriver->see($successMessage, $successMessageContainer);
+ $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow));
+ }
+ } catch (\Exception $e) {
+ $this->fail($e->getMessage());
+ }
+ }
+}
diff --git a/app/code/Magento/Sales/Model/Order/ItemRepository.php b/app/code/Magento/Sales/Model/Order/ItemRepository.php
index 6e029ac468370..345fffc414fbc 100644
--- a/app/code/Magento/Sales/Model/Order/ItemRepository.php
+++ b/app/code/Magento/Sales/Model/Order/ItemRepository.php
@@ -167,10 +167,7 @@ public function deleteById($id)
public function save(OrderItemInterface $entity)
{
if ($entity->getProductOption()) {
- $request = $this->getBuyRequest($entity);
- $productOptions = $entity->getProductOptions();
- $productOptions['info_buyRequest'] = $request->toArray();
- $entity->setProductOptions($productOptions);
+ $entity->setProductOptions($this->getItemProductOptions($entity));
}
$this->metadata->getMapper()->save($entity);
@@ -178,6 +175,23 @@ public function save(OrderItemInterface $entity)
return $this->registry[$entity->getEntityId()];
}
+ /**
+ * Return product options
+ *
+ * @param OrderItemInterface $entity
+ * @return array
+ */
+ private function getItemProductOptions(OrderItemInterface $entity): array
+ {
+ $request = $this->getBuyRequest($entity);
+ $productOptions = $entity->getProductOptions();
+ $productOptions['info_buyRequest'] = $productOptions && !empty($productOptions['info_buyRequest'])
+ ? array_merge($productOptions['info_buyRequest'], $request->toArray())
+ : $request->toArray();
+
+ return $productOptions;
+ }
+
/**
* Set parent item.
*
diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php
index 253dbd43fa580..6ddbce49829eb 100644
--- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php
+++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php
@@ -80,11 +80,9 @@ public function draw()
$lines = [];
// draw Product name
- $lines[0] = [
- [
+ $lines[0][] = [
'text' => $this->string->split($this->prepareText((string)$item->getName()), 35, true, true),
'feed' => 35
- ]
];
// draw SKU
diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php
index 19d9b6f300eba..b1d2deb248ba1 100644
--- a/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php
+++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php
@@ -43,13 +43,13 @@ public function getAllCommentCollection($orderId)
$commentSelects = [];
foreach (['invoice', 'shipment', 'creditmemo'] as $entityTypeCode) {
$mainTable = $resource->getTableName('sales_' . $entityTypeCode);
- $slaveTable = $resource->getTableName('sales_' . $entityTypeCode . '_comment');
+ $commentTable = $resource->getTableName('sales_' . $entityTypeCode . '_comment');
$select = $read->select()->from(
['main' => $mainTable],
['entity_id' => 'order_id', 'entity_type_code' => new \Zend_Db_Expr("'{$entityTypeCode}'")]
)->join(
- ['slave' => $slaveTable],
- 'main.entity_id = slave.parent_id',
+ ['comment' => $commentTable],
+ 'main.entity_id = comment.parent_id',
$fields
)->where(
'main.order_id = ?',
diff --git a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html
index dc3a8e9f69aca..0529c66a04d8c 100644
--- a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html
+++ b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html
@@ -8,7 +8,7 @@
+
+
+
+
+ - Magento\SalesGraphQl\Model\OrderItemTypeResolver
+
+
+
+
+
+
+ - Magento\SalesGraphQl\Model\InvoiceItemTypeResolver
+
+
+
+
diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls
index f823c25cf2d9f..099a3ffb959c4 100644
--- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls
@@ -2,20 +2,7 @@
# See COPYING.txt for license details.
type Query {
- customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @doc(description: "List of customer orders") @cache(cacheable: false)
-}
-
-type CustomerOrder @doc(description: "Order mapping fields") {
- id: Int
- increment_id: String @deprecated(reason: "Use the order_number instead.")
- order_number: String! @doc(description: "The order number")
- created_at: String
- grand_total: Float
- status: String
-}
-
-type CustomerOrders {
- items: [CustomerOrder] @doc(description: "Array of orders")
+ customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @deprecated(reason: "Use orders from customer instead") @cache(cacheable: false)
}
type Mutation {
@@ -33,6 +20,190 @@ type CheckoutUserInputError @doc(description:"An error encountered while adding
code: CheckoutUserInputErrorCodes! @doc(description: "Checkout-specific error code")
}
+type Customer {
+ orders (
+ filter: CustomerOrdersFilterInput @doc(description: "Defines the filter to use for searching customer orders"),
+ currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1"),
+ pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. The default value is 20"),
+ ): CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CustomerOrders") @cache(cacheable: false)
+}
+
+input CustomerOrdersFilterInput @doc(description: "Identifies the filter to use for filtering orders.") {
+ number: FilterStringTypeInput @doc(description: "Filters by order number.")
+}
+
+type CustomerOrders @doc(description: "The collection of orders that match the conditions defined in the filter") {
+ items: [CustomerOrder]! @doc(description: "An array of customer orders")
+ page_info: SearchResultPageInfo @doc(description: "An object that includes the current_page, page_info, and page_size values specified in the query")
+ total_count: Int @doc(description: "The total count of customer orders")
+}
+
+type CustomerOrder @doc(description: "Contains details about each of the customer's orders") {
+ id: ID! @doc(description: "Unique identifier for the order")
+ order_date: String! @doc(description: "The date the order was placed")
+ status: String! @doc(description: "The current status of the order")
+ number: String! @doc(description: "The order number")
+ items: [OrderItemInterface] @doc(description: "An array containing the items purchased in this order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItems")
+ total: OrderTotal @doc(description: "Contains details about the calculated totals for this order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderTotal")
+ invoices: [Invoice]! @doc(description: "A list of invoices for the order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoices")
+ shipments: [OrderShipment] @doc(description: "A list of shipments for the order")
+ payment_methods: [PaymentMethod] @doc(description: "Payment details for the order")
+ shipping_address: CustomerAddress @doc(description: "The shipping address for the order")
+ billing_address: CustomerAddress @doc(description: "The billing address for the order")
+ carrier: String @doc(description: "The shipping carrier for the order delivery")
+ shipping_method: String @doc(description: "The delivery method for the order")
+ comments: [CommentItem] @doc(description: "Comments about the order")
+ increment_id: String @deprecated(reason: "Use the id attribute instead")
+ order_number: String! @deprecated(reason: "Use the number attribute instead")
+ created_at: String @deprecated(reason: "Use the order_date attribute instead")
+ grand_total: Float @deprecated(reason: "Use the totals.grand_total attribute instead")
+}
+
+interface OrderItemInterface @doc(description: "Order item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\OrderItemTypeResolver") {
+ id: ID! @doc(description: "The unique identifier of the order item")
+ product_name: String @doc(description: "The name of the base product")
+ product_sku: String! @doc(description: "The SKU of the base product")
+ product_url_key: String @doc(description: "URL key of the base product")
+ product_type: String @doc(description: "The type of product, such as simple, configurable, or bundle")
+ status: String @doc(description: "The status of the order item")
+ product_sale_price: Money! @doc(description: "The sale price of the base product, including selected options")
+ discounts: [Discount] @doc(description: "The final discount information for the product")
+ selected_options: [OrderItemOption] @doc(description: "The selected options for the base product, such as color or size")
+ entered_options: [OrderItemOption] @doc(description: "The entered option for the base product, such as a logo or image")
+ quantity_ordered: Float @doc(description: "The number of units ordered for this item")
+ quantity_shipped: Float @doc(description: "The number of shipped items")
+ quantity_refunded: Float @doc(description: "The number of refunded items")
+ quantity_invoiced: Float @doc(description: "The number of invoiced items")
+ quantity_canceled: Float @doc(description: "The number of canceled items")
+ quantity_returned: Float @doc(description: "The number of returned items")
+}
+
+type OrderItem implements OrderItemInterface {
+}
+
+type BundleOrderItem implements OrderItemInterface {
+ bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\BundleOptions")
+}
+
+type ItemSelectedBundleOption @doc(description: "A list of options of the selected bundle product") {
+ id: ID! @doc(description: "The unique identifier of the option")
+ label: String! @doc(description: "The label of the option")
+ values: [ItemSelectedBundleOptionValue] @doc(description: "A list of products that represent the values of the parent option")
+}
+
+type ItemSelectedBundleOptionValue @doc(description: "A list of values for the selected bundle product") {
+ id: ID! @doc(description: "The unique identifier of the value")
+ product_name: String! @doc(description: "The name of the child bundle product")
+ product_sku: String! @doc(description: "The SKU of the child bundle product")
+ quantity: Float! @doc(description: "Indicates how many of this bundle product were ordered")
+ price: Money! @doc(description: "The price of the child bundle product")
+}
+
+type OrderItemOption @doc(description: "Represents order item options like selected or entered") {
+ id: String! @doc(description: "The name of the option")
+ value: String! @doc(description: "The value of the option")
+}
+
+type TaxItem @doc(description: "The tax item details") {
+ amount: Money! @doc(description: "The amount of tax applied to the item")
+ title: String! @doc(description: "A title that describes the tax")
+ rate: Float! @doc(description: "The rate used to calculate the tax")
+}
+
+type OrderTotal @doc(description: "Contains details about the sales total amounts used to calculate the final price") {
+ subtotal: Money! @doc(description: "The subtotal of the order, excluding shipping, discounts, and taxes")
+ discounts: [Discount] @doc(description: "The applied discounts to the order")
+ total_tax: Money! @doc(description: "The amount of tax applied to the order")
+ taxes: [TaxItem] @doc(description: "The order tax details")
+ grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes")
+ base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency")
+ total_shipping: Money! @doc(description: "The shipping amount for the order")
+ shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the order")
+}
+
+type Invoice @doc(description: "Invoice details") {
+ id: ID! @doc(description: "The ID of the invoice, used for API purposes")
+ number: String! @doc(description: "Sequential invoice number")
+ total: InvoiceTotal @doc(description: "Invoice total amount details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\InvoiceTotal")
+ items: [InvoiceItemInterface] @doc(description: "Invoiced product details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\InvoiceItems")
+ comments: [CommentItem] @doc(description: "Comments on the invoice")
+}
+
+interface InvoiceItemInterface @doc(description: "Invoice item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\InvoiceItemTypeResolver") {
+ id: ID! @doc(description: "The unique ID of the invoice item")
+ order_item: OrderItemInterface @doc(description: "Contains details about an individual order item")
+ product_name: String @doc(description: "The name of the base product")
+ product_sku: String! @doc(description: "The SKU of the base product")
+ product_sale_price: Money! @doc(description: "The sale price for the base product including selected options")
+ discounts: [Discount] @doc(description: "Contains information about the final discount amount for the base product, including discounts on options")
+ quantity_invoiced: Float @doc(description: "The number of invoiced items")
+}
+
+type InvoiceItem implements InvoiceItemInterface {
+}
+
+type BundleInvoiceItem implements InvoiceItemInterface{
+ bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\BundleOptions")
+}
+
+type InvoiceTotal @doc(description: "Contains price details from an invoice"){
+ subtotal: Money! @doc(description: "The subtotal of the invoice, excluding shipping, discounts, and taxes")
+ discounts: [Discount] @doc(description: "The applied discounts to the invoice")
+ total_tax: Money! @doc(description: "The amount of tax applied to the invoice")
+ taxes: [TaxItem] @doc(description: "The order tax details")
+ grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes")
+ base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency")
+ total_shipping: Money! @doc(description: "The shipping amount for the invoice")
+ shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the invoice")
+}
+
+type ShippingHandling @doc(description: "The Shipping handling details") {
+ total_amount: Money! @doc(description: "The total amount for shipping")
+ amount_including_tax: Money @doc(description: "The shipping amount, including tax")
+ amount_excluding_tax: Money @doc(description: "The shipping amount, excluding tax")
+ taxes: [TaxItem] @doc(description: "Contains details about taxes applied for shipping")
+ discounts: [Discount] @doc(description: "The applied discounts to the shipping")
+}
+
+type OrderShipment @doc(description: "Order shipment details") {
+ id: ID! @doc(description: "The unique ID of the shipment")
+ number: String! @doc(description: "The sequential credit shipment number")
+ tracking: [ShipmentTracking] @doc(description: "Contains shipment tracking details")
+ items: [ShipmentItem] @doc(description: "Contains items included in the shipment")
+ comments: [CommentItem] @doc(description: "Comments added to the shipment")
+}
+
+type CommentItem @doc(description: "Comment item details") {
+ timestamp: String! @doc(description: "The timestamp of the comment")
+ message: String! @doc(description: "The text of the message")
+}
+
+type ShipmentItem @doc(description: "Order shipment item details") {
+ id: ID! @doc(description: "The unique ID of the shipment item")
+ order_item: OrderItemInterface @doc(description: "The shipped order item")
+ product_name: String @doc(description: "The name of the base product")
+ product_sku: String! @doc(description: "The SKU of the base product")
+ product_sale_price: Money! @doc(description: "The sale price for the base product")
+ quantity_shipped: Float! @doc(description: "The number of shipped items")
+}
+
+type ShipmentTracking @doc(description: "Order shipment tracking details") {
+ title: String! @doc(description: "The shipment tracking title")
+ carrier: String! @doc(description: "The shipping carrier for the order delivery")
+ number: String @doc(description: "The tracking number of the order shipment")
+}
+
+type PaymentMethod @doc(description: "Contains details about the payment method used to pay for the order") {
+ name: String! @doc(description: "The label that describes the payment method")
+ type: String! @doc(description: "The payment method code that indicates how the order was paid for")
+ additional_data: [KeyValue] @doc(description: "Additional data per payment method type")
+}
+
+type KeyValue @doc(description: "The key-value type") {
+ name: String @doc(description: "The name part of the name/value pair")
+ value: String @doc(description: "The value part of the name/value pair")
+}
+
enum CheckoutUserInputErrorCodes {
REORDER_NOT_AVAILABLE
PRODUCT_NOT_FOUND
diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml
new file mode 100644
index 0000000000000..85437650efc35
--- /dev/null
+++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+ Open Cart Price Rule grid and delete all rules one by one. Need to avoid interference with other tests that test cart price rules.
+
+
+
+
+
+
+
+ {{AdminDataGridTableSection.firstNotEmptyRow}}
+ {{AdminConfirmationModalSection.ok}}
+ {{AdminMainActionsSection.delete}}
+ {{AdminMessagesSection.success}}
+ You deleted the rule.
+
+
+
+
+
diff --git a/app/code/Magento/SalesRule/view/frontend/requirejs-config.js b/app/code/Magento/SalesRule/view/frontend/requirejs-config.js
index 13b701c6fe65a..21f49fb3080fc 100644
--- a/app/code/Magento/SalesRule/view/frontend/requirejs-config.js
+++ b/app/code/Magento/SalesRule/view/frontend/requirejs-config.js
@@ -8,6 +8,9 @@ var config = {
mixins: {
'Magento_Checkout/js/action/select-payment-method': {
'Magento_SalesRule/js/action/select-payment-method-mixin': true
+ },
+ 'Magento_Checkout/js/model/shipping-save-processor': {
+ 'Magento_SalesRule/js/model/shipping-save-processor-mixin': true
}
}
}
diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js b/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js
new file mode 100644
index 0000000000000..193acb8eed2f4
--- /dev/null
+++ b/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js
@@ -0,0 +1,34 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+define([
+ 'mage/utils/wrapper',
+ 'Magento_Checkout/js/model/quote',
+ 'Magento_SalesRule/js/model/coupon'
+], function (wrapper, quote, coupon) {
+ 'use strict';
+
+ return function (shippingSaveProcessor) {
+ shippingSaveProcessor.saveShippingInformation = wrapper.wrapSuper(
+ shippingSaveProcessor.saveShippingInformation,
+ function (type) {
+ var updateCouponCallback;
+
+ /**
+ * Update coupon form
+ */
+ updateCouponCallback = function () {
+ if (quote.totals() && !quote.totals()['coupon_code']) {
+ coupon.setCouponCode('');
+ coupon.setIsApplied(false);
+ }
+ };
+
+ return this._super(type).done(updateCouponCallback);
+ }
+ );
+
+ return shippingSaveProcessor;
+ };
+});
diff --git a/app/code/Magento/SampleData/README.md b/app/code/Magento/SampleData/README.md
index c71439b929013..e0666ba73fe24 100644
--- a/app/code/Magento/SampleData/README.md
+++ b/app/code/Magento/SampleData/README.md
@@ -11,7 +11,7 @@ You can deploy sample data from one of the following sources:
* From the Magento composer repository, optionally using Magento CLI
* From the Magento GitHub repository
-If your Magento code base was cloned from the `master` branch, you can use either source of the sample data. If it was cloned from the `develop` branch, use the GitHub repository and choose to get sample data modules from the `develop` branch.
+If your Magento code base was cloned from the mainline branch, you can use either source of the sample data. If it was cloned from the `develop` branch, use the GitHub repository and choose to get sample data modules from the `develop` branch.
### Deploy Sample Data from Composer Repository
@@ -46,7 +46,7 @@ Each package corresponds to a sample data module. The complete list of available
To deploy sample data from the GitHub repository:
-1. Clone sample data from `https://github.com/magento/magento2-sample-data`. If your Magento instance was cloned from the `master` branch, choose the `master` branch when cloning sample data; choose the `develop` branch if Magento was cloned from `develop`.
+1. Clone sample data from `https://github.com/magento/magento2-sample-data`. If your Magento instance was cloned from the mainline branch, choose the mainline branch when cloning sample data; choose the `develop` branch if Magento was cloned from `develop`.
2. Link the sample data and your Magento instance by running: `# php -f /dev/tools/build-sample-data.php -- --ce-source=""`
## Install Sample Data
diff --git a/app/code/Magento/Search/Model/SearchEngine/Validator.php b/app/code/Magento/Search/Model/SearchEngine/Validator.php
index f4fc8a9a62e0e..264e7c69dd520 100644
--- a/app/code/Magento/Search/Model/SearchEngine/Validator.php
+++ b/app/code/Magento/Search/Model/SearchEngine/Validator.php
@@ -22,7 +22,7 @@ class Validator implements ValidatorInterface
/**
* @var array
*/
- private $engineBlacklist = ['mysql' => 'MySQL'];
+ private $excludedEngineList = ['mysql' => 'MySQL'];
/**
* @var ValidatorInterface[]
@@ -32,16 +32,16 @@ class Validator implements ValidatorInterface
/**
* @param ScopeConfigInterface $scopeConfig
* @param array $engineValidators
- * @param array $engineBlacklist
+ * @param array $excludedEngineList
*/
public function __construct(
ScopeConfigInterface $scopeConfig,
array $engineValidators = [],
- array $engineBlacklist = []
+ array $excludedEngineList = []
) {
$this->scopeConfig = $scopeConfig;
$this->engineValidators = $engineValidators;
- $this->engineBlacklist = array_merge($this->engineBlacklist, $engineBlacklist);
+ $this->excludedEngineList = array_merge($this->excludedEngineList, $excludedEngineList);
}
/**
@@ -51,9 +51,9 @@ public function validate(): array
{
$errors = [];
$currentEngine = $this->scopeConfig->getValue('catalog/search/engine');
- if (isset($this->engineBlacklist[$currentEngine])) {
- $blacklistedEngine = $this->engineBlacklist[$currentEngine];
- $errors[] = "Your current search engine, '{$blacklistedEngine}', is not supported."
+ if (isset($this->excludedEngineList[$currentEngine])) {
+ $excludedEngine = $this->excludedEngineList[$currentEngine];
+ $errors[] = "Your current search engine, '{$excludedEngine}', is not supported."
. " You must install a supported search engine before upgrading."
. " See the System Upgrade Guide for more information.";
}
diff --git a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php
index c91c0fce9dd47..cc272ccb60162 100644
--- a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php
+++ b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php
@@ -34,7 +34,7 @@ protected function setUp(): void
[
'scopeConfig' => $this->scopeConfigMock,
'engineValidators' => ['otherEngine' => $this->otherEngineValidatorMock],
- 'engineBlacklist' => ['badEngine' => 'Bad Engine']
+ 'excludedEngineList' => ['badEngine' => 'Bad Engine']
]
);
}
@@ -54,7 +54,7 @@ public function testValidateValid()
$this->assertEquals($expectedErrors, $this->validator->validate());
}
- public function testValidateBlacklist()
+ public function testValidateExcludedList()
{
$this->scopeConfigMock
->expects($this->once())
diff --git a/app/code/Magento/Security/Model/Plugin/Auth.php b/app/code/Magento/Security/Model/Plugin/Auth.php
index 833b4e4c1b774..b388ef6115867 100644
--- a/app/code/Magento/Security/Model/Plugin/Auth.php
+++ b/app/code/Magento/Security/Model/Plugin/Auth.php
@@ -35,6 +35,8 @@ public function __construct(
}
/**
+ * Add warning message if other sessions terminated
+ *
* @param \Magento\Backend\Model\Auth $authModel
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
@@ -43,11 +45,13 @@ public function afterLogin(\Magento\Backend\Model\Auth $authModel)
{
$this->sessionsManager->processLogin();
if ($this->sessionsManager->getCurrentSession()->isOtherSessionsTerminated()) {
- $this->messageManager->addWarning(__('All other open sessions for this account were terminated.'));
+ $this->messageManager->addWarningMessage(__('All other open sessions for this account were terminated.'));
}
}
/**
+ * Handle logout process
+ *
* @param \Magento\Backend\Model\Auth $authModel
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php
index c431f1ecda332..dd86b3b574ead 100644
--- a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php
+++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php
@@ -64,7 +64,7 @@ protected function setUp(): void
$this->messageManager = $this->getMockForAbstractClass(
ManagerInterface::class,
- ['addWarning'],
+ ['addWarningMessage'],
'',
false
);
@@ -100,7 +100,7 @@ public function testAfterLogin()
->method('isOtherSessionsTerminated')
->willReturn(true);
$this->messageManager->expects($this->once())
- ->method('addWarning')
+ ->method('addWarningMessage')
->with($warningMessage);
$this->model->afterLogin($this->authMock);
diff --git a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php
index 537fec4c75df6..8f6011f1ae56f 100644
--- a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php
+++ b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php
@@ -6,7 +6,7 @@
namespace Magento\Store\Api\Data;
/**
- * StoreConfig interface
+ * Interface for store config
*
* @api
* @since 100.0.2
@@ -141,7 +141,7 @@ public function setWeightUnit($weightUnit);
public function getBaseUrl();
/**
- * set base URL
+ * Set base URL
*
* @param string $baseUrl
* @return $this
@@ -201,7 +201,7 @@ public function setBaseMediaUrl($baseMediaUrl);
public function getSecureBaseUrl();
/**
- * set secure base URL
+ * Set secure base URL
*
* @param string $secureBaseUrl
* @return $this
diff --git a/app/code/Magento/Store/Model/Data/StoreConfig.php b/app/code/Magento/Store/Model/Data/StoreConfig.php
index 6634e2cb05bd9..e68d98b162613 100644
--- a/app/code/Magento/Store/Model/Data/StoreConfig.php
+++ b/app/code/Magento/Store/Model/Data/StoreConfig.php
@@ -6,7 +6,7 @@
namespace Magento\Store\Model\Data;
/**
- * Class StoreConfig
+ * Allows to get and set store config values
*
* @codeCoverageIgnore
*/
@@ -188,7 +188,7 @@ public function getBaseUrl()
}
/**
- * set base URL
+ * Set base URL
*
* @param string $baseUrl
* @return $this
@@ -293,7 +293,7 @@ public function getSecureBaseUrl()
}
/**
- * set secure base URL
+ * Set secure base URL
*
* @param string $secureBaseUrl
* @return $this
@@ -367,7 +367,7 @@ public function setSecureBaseMediaUrl($secureBaseMediaUrl)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*
* @return \Magento\Store\Api\Data\StoreConfigExtensionInterface|null
*/
@@ -377,7 +377,7 @@ public function getExtensionAttributes()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*
* @param \Magento\Store\Api\Data\StoreConfigExtensionInterface $extensionAttributes
* @return $this
diff --git a/app/code/Magento/Store/Model/Service/StoreConfigManager.php b/app/code/Magento/Store/Model/Service/StoreConfigManager.php
index b3c2208a58361..debb08438a3b4 100644
--- a/app/code/Magento/Store/Model/Service/StoreConfigManager.php
+++ b/app/code/Magento/Store/Model/Service/StoreConfigManager.php
@@ -5,6 +5,9 @@
*/
namespace Magento\Store\Model\Service;
+/**
+ * Allows to get store config
+ */
class StoreConfigManager implements \Magento\Store\Api\StoreConfigManagerInterface
{
/**
@@ -53,6 +56,8 @@ public function __construct(
}
/**
+ * Get store configs
+ *
* @param string[] $storeCodes list of stores by store codes, will return all if storeCodes is not set
* @return \Magento\Store\Api\Data\StoreConfigInterface[]
*/
@@ -71,6 +76,8 @@ public function getStoreConfigs(array $storeCodes = null)
}
/**
+ * Get store config
+ *
* @param \Magento\Store\Model\Store $store
* @return \Magento\Store\Api\Data\StoreConfigInterface
*/
diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml
new file mode 100644
index 0000000000000..4a403364a91e3
--- /dev/null
+++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Switch the Storefront to the provided Store.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml
index 5bd8f6e2349fc..2da9e91e1fddd 100644
--- a/app/code/Magento/Store/etc/di.xml
+++ b/app/code/Magento/Store/etc/di.xml
@@ -65,7 +65,6 @@
-
diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php
index 59f9831789a35..0baee00f468a0 100644
--- a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php
+++ b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php
@@ -55,11 +55,10 @@ public function __construct(
*/
public function getStoreConfigData(StoreInterface $store): array
{
- $storeConfigData = array_merge(
+ return array_merge(
$this->getBaseConfigData($store),
$this->getExtendedConfigData((int)$store->getId())
);
- return $storeConfigData;
}
/**
@@ -72,7 +71,7 @@ private function getBaseConfigData(StoreInterface $store) : array
{
$storeConfig = current($this->storeConfigManager->getStoreConfigs([$store->getCode()]));
- $storeConfigData = [
+ return [
'id' => $storeConfig->getId(),
'code' => $storeConfig->getCode(),
'website_id' => $storeConfig->getWebsiteId(),
@@ -88,9 +87,9 @@ private function getBaseConfigData(StoreInterface $store) : array
'secure_base_url' => $storeConfig->getSecureBaseUrl(),
'secure_base_link_url' => $storeConfig->getSecureBaseLinkUrl(),
'secure_base_static_url' => $storeConfig->getSecureBaseStaticUrl(),
- 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl()
+ 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl(),
+ 'store_name' => $store->getName()
];
- return $storeConfigData;
}
/**
diff --git a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml
index f3771b704c3e9..3a0143821d8b9 100644
--- a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml
@@ -23,11 +23,4 @@
-
-
-
- - store/information/name
-
-
-
diff --git a/app/code/Magento/Theme/Block/Html/Pager.php b/app/code/Magento/Theme/Block/Html/Pager.php
index 5798b94e31a70..764b2e9ca42f0 100644
--- a/app/code/Magento/Theme/Block/Html/Pager.php
+++ b/app/code/Magento/Theme/Block/Html/Pager.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Theme\Block\Html;
/**
@@ -466,7 +467,26 @@ public function getPageUrl($page)
*/
public function getLimitUrl($limit)
{
- return $this->getPagerUrl([$this->getLimitVarName() => $limit]);
+ return $this->getPagerUrl($this->getPageLimitParams($limit));
+ }
+
+ /**
+ * Return page limit params
+ *
+ * @param int $limit
+ * @return array
+ */
+ private function getPageLimitParams(int $limit): array
+ {
+ $data = [$this->getLimitVarName() => $limit];
+
+ $currentPage = $this->getCurrentPage();
+ $availableCount = (int) ceil($this->getTotalNum() / $limit);
+ if ($currentPage !== 1 && $availableCount < $currentPage) {
+ $data = array_merge($data, [$this->getPageVarName() => $availableCount === 1 ? null : $availableCount]);
+ }
+
+ return $data;
}
/**
diff --git a/app/code/Magento/Theme/Model/Config/Customization.php b/app/code/Magento/Theme/Model/Config/Customization.php
index 6a6872d794b1b..7430730451110 100644
--- a/app/code/Magento/Theme/Model/Config/Customization.php
+++ b/app/code/Magento/Theme/Model/Config/Customization.php
@@ -5,23 +5,34 @@
*/
namespace Magento\Theme\Model\Config;
+use Magento\Framework\App\Area;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\View\Design\Theme\ThemeProviderInterface;
+use Magento\Framework\View\Design\ThemeInterface;
+use Magento\Framework\View\DesignInterface;
+use Magento\Store\Model\Store;
+use Magento\Store\Model\StoreManagerInterface;
+use Magento\Theme\Model\ResourceModel\Theme\Collection;
+use Magento\Theme\Model\Theme\StoreThemesResolverInterface;
+use Magento\Theme\Model\Theme\StoreUserAgentThemeResolver;
+
/**
* Theme customization config model
*/
class Customization
{
/**
- * @var \Magento\Store\Model\StoreManagerInterface
+ * @var StoreManagerInterface
*/
protected $_storeManager;
/**
- * @var \Magento\Framework\View\DesignInterface
+ * @var DesignInterface
*/
protected $_design;
/**
- * @var \Magento\Framework\View\Design\Theme\ThemeProviderInterface
+ * @var ThemeProviderInterface
*/
protected $themeProvider;
@@ -40,20 +51,28 @@ class Customization
* @see self::_prepareThemeCustomizations()
*/
protected $_unassignedTheme;
+ /**
+ * @var StoreUserAgentThemeResolver|mixed|null
+ */
+ private $storeThemesResolver;
/**
- * @param \Magento\Store\Model\StoreManagerInterface $storeManager
- * @param \Magento\Framework\View\DesignInterface $design
- * @param \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider
+ * @param StoreManagerInterface $storeManager
+ * @param DesignInterface $design
+ * @param ThemeProviderInterface $themeProvider
+ * @param StoreThemesResolverInterface|null $storeThemesResolver
*/
public function __construct(
- \Magento\Store\Model\StoreManagerInterface $storeManager,
- \Magento\Framework\View\DesignInterface $design,
- \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider
+ StoreManagerInterface $storeManager,
+ DesignInterface $design,
+ ThemeProviderInterface $themeProvider,
+ ?StoreThemesResolverInterface $storeThemesResolver = null
) {
$this->_storeManager = $storeManager;
$this->_design = $design;
$this->themeProvider = $themeProvider;
+ $this->storeThemesResolver = $storeThemesResolver
+ ?? ObjectManager::getInstance()->get(StoreThemesResolverInterface::class);
}
/**
@@ -93,13 +112,14 @@ public function getStoresByThemes()
{
$storesByThemes = [];
$stores = $this->_storeManager->getStores();
- /** @var $store \Magento\Store\Model\Store */
+ /** @var $store Store */
foreach ($stores as $store) {
- $themeId = $this->_getConfigurationThemeId($store);
- if (!isset($storesByThemes[$themeId])) {
- $storesByThemes[$themeId] = [];
+ foreach ($this->storeThemesResolver->getThemes($store) as $themeId) {
+ if (!isset($storesByThemes[$themeId])) {
+ $storesByThemes[$themeId] = [];
+ }
+ $storesByThemes[$themeId][] = $store;
}
- $storesByThemes[$themeId][] = $store;
}
return $storesByThemes;
}
@@ -107,8 +127,8 @@ public function getStoresByThemes()
/**
* Check if current theme has assigned to any store
*
- * @param \Magento\Framework\View\Design\ThemeInterface $theme
- * @param null|\Magento\Store\Model\Store $store
+ * @param ThemeInterface $theme
+ * @param null|Store $store
* @return bool
*/
public function isThemeAssignedToStore($theme, $store = null)
@@ -133,8 +153,8 @@ public function hasThemeAssigned()
/**
* Is theme assigned to specific store
*
- * @param \Magento\Framework\View\Design\ThemeInterface $theme
- * @param \Magento\Store\Model\Store $store
+ * @param ThemeInterface $theme
+ * @param Store $store
* @return bool
*/
protected function _isThemeAssignedToSpecificStore($theme, $store)
@@ -145,21 +165,21 @@ protected function _isThemeAssignedToSpecificStore($theme, $store)
/**
* Get configuration theme id
*
- * @param \Magento\Store\Model\Store $store
+ * @param Store $store
* @return int
*/
protected function _getConfigurationThemeId($store)
{
return $this->_design->getConfigurationDesignTheme(
- \Magento\Framework\App\Area::AREA_FRONTEND,
+ Area::AREA_FRONTEND,
['store' => $store]
);
}
/**
* Fetch theme customization and sort them out to arrays:
- * self::_assignedTheme and self::_unassignedTheme.
*
+ * Set self::_assignedTheme and self::_unassignedTheme.
* NOTE: To get into "assigned" list theme customization not necessary should be assigned to store-view directly.
* It can be set to website or as default theme and be used by store-view via config fallback mechanism.
*
@@ -167,15 +187,15 @@ protected function _getConfigurationThemeId($store)
*/
protected function _prepareThemeCustomizations()
{
- /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection */
- $themeCollection = $this->themeProvider->getThemeCustomizations(\Magento\Framework\App\Area::AREA_FRONTEND);
+ /** @var Collection $themeCollection */
+ $themeCollection = $this->themeProvider->getThemeCustomizations(Area::AREA_FRONTEND);
$assignedThemes = $this->getStoresByThemes();
$this->_assignedTheme = [];
$this->_unassignedTheme = [];
- /** @var $theme \Magento\Framework\View\Design\ThemeInterface */
+ /** @var $theme ThemeInterface */
foreach ($themeCollection as $theme) {
if (isset($assignedThemes[$theme->getId()])) {
$theme->setAssignedStores($assignedThemes[$theme->getId()]);
diff --git a/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php
new file mode 100644
index 0000000000000..26bd5604294d1
--- /dev/null
+++ b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php
@@ -0,0 +1,90 @@
+design = $design;
+ $this->themeCollectionFactory = $themeCollectionFactory;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getThemes(StoreInterface $store): array
+ {
+ $theme = $this->design->getConfigurationDesignTheme(
+ Area::AREA_FRONTEND,
+ ['store' => $store]
+ );
+ $themes = [];
+ if ($theme) {
+ if (!is_numeric($theme)) {
+ $registeredThemes = $this->getRegisteredThemes();
+ if (isset($registeredThemes[$theme])) {
+ $themes[] = $registeredThemes[$theme]->getId();
+ }
+ } else {
+ $themes[] = $theme;
+ }
+ }
+ return $themes;
+ }
+
+ /**
+ * Get system registered themes.
+ *
+ * @return ThemeInterface[]
+ */
+ private function getRegisteredThemes(): array
+ {
+ if ($this->registeredThemes === null) {
+ $this->registeredThemes = [];
+ /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $collection */
+ $collection = $this->themeCollectionFactory->create();
+ $themes = $collection->loadRegisteredThemes();
+ /** @var ThemeInterface $theme */
+ foreach ($themes as $theme) {
+ $this->registeredThemes[$theme->getCode()] = $theme;
+ }
+ }
+ return $this->registeredThemes;
+ }
+}
diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php
new file mode 100644
index 0000000000000..5be86c08f7c51
--- /dev/null
+++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php
@@ -0,0 +1,57 @@
+resolvers = $resolvers;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getThemes(StoreInterface $store): array
+ {
+ $themes = [];
+ foreach ($this->resolvers as $resolver) {
+ foreach ($resolver->getThemes($store) as $theme) {
+ $themes[] = $theme;
+ }
+ }
+ return array_values(array_unique($themes));
+ }
+}
diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php
new file mode 100644
index 0000000000000..bb2cd73300c02
--- /dev/null
+++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php
@@ -0,0 +1,24 @@
+scopeConfig = $scopeConfig;
+ $this->serializer = $serializer;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getThemes(StoreInterface $store): array
+ {
+ $config = $this->scopeConfig->getValue(
+ self::XML_PATH_THEME_USER_AGENT,
+ ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ $rules = $config ? $this->serializer->unserialize($config) : [];
+ $themes = [];
+ if ($rules) {
+ $themes = array_values(array_unique(array_column($rules, 'value')));
+ }
+ return $themes;
+ }
+}
diff --git a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php
similarity index 88%
rename from lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php
rename to app/code/Magento/Theme/Plugin/LoadDesignPlugin.php
index 2cda49c43c2ce..c4f8d3a905d0f 100644
--- a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php
+++ b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php
@@ -4,7 +4,7 @@
* See COPYING.txt for license details.
*/
-namespace Magento\Framework\App\Action\Plugin;
+namespace Magento\Theme\Plugin;
use Magento\Framework\App\ActionInterface;
use Magento\Framework\Config\Dom\ValidationException;
@@ -21,12 +21,12 @@ class LoadDesignPlugin
/**
* @var DesignLoader
*/
- protected $_designLoader;
+ private $designLoader;
/**
* @var MessageManagerInterface
*/
- protected $messageManager;
+ private $messageManager;
/**
* @param DesignLoader $designLoader
@@ -36,7 +36,7 @@ public function __construct(
DesignLoader $designLoader,
MessageManagerInterface $messageManager
) {
- $this->_designLoader = $designLoader;
+ $this->designLoader = $designLoader;
$this->messageManager = $messageManager;
}
@@ -50,7 +50,7 @@ public function __construct(
public function beforeExecute(ActionInterface $subject)
{
try {
- $this->_designLoader->load();
+ $this->designLoader->load();
} catch (LocalizedException $e) {
if ($e->getPrevious() instanceof ValidationException) {
/** @var MessageInterface $message */
diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php
index ac16c56b17f1b..fd0ef1db0219a 100644
--- a/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php
+++ b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php
@@ -91,6 +91,60 @@ public function testGetPages(): void
$this->assertEquals($expectedPages, $this->pager->getPages());
}
+ /**
+ * Test get limit url.
+ *
+ * @dataProvider limitUrlDataProvider
+ *
+ * @param int $page
+ * @param int $size
+ * @param int $limit
+ * @param array $expectedParams
+ * @return void
+ */
+ public function testGetLimitUrl(int $page, int $size, int $limit, array $expectedParams): void
+ {
+ $expectedArray = [
+ '_current' => true,
+ '_escape' => true,
+ '_use_rewrite' => true,
+ '_fragment' => null,
+ '_query' => $expectedParams,
+ ];
+
+ $collectionMock = $this->createMock(Collection::class);
+ $collectionMock->expects($this->once())
+ ->method('getCurPage')
+ ->willReturn($page);
+ $collectionMock->expects($this->once())
+ ->method('getSize')
+ ->willReturn($size);
+ $this->setCollectionProperty($collectionMock);
+
+ $this->urlBuilderMock->expects($this->once())
+ ->method('getUrl')
+ ->with('*/*/*', $expectedArray);
+
+ $this->pager->getLimitUrl($limit);
+ }
+
+ /**
+ * DataProvider for testGetLimitUrl
+ *
+ * @return array
+ */
+ public function limitUrlDataProvider(): array
+ {
+ return [
+ [2, 21, 10, ['limit' => 10]],
+ [3, 21, 10, ['limit' => 10]],
+ [2, 21, 20, ['limit' => 20]],
+ [3, 21, 50, ['limit' => 50, 'p' => null]],
+ [2, 11, 20, ['limit' => 20, 'p' => null]],
+ [4, 40, 20, ['limit' => 20, 'p' => 2]],
+ ];
+ }
+
/**
* Set Collection
*
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php
index 82678d4b4277d..438853b9935e6 100644
--- a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php
+++ b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php
@@ -13,9 +13,10 @@
use Magento\Framework\App\Area;
use Magento\Framework\DataObject;
use Magento\Framework\View\DesignInterface;
+use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Theme\Model\Config\Customization;
-use Magento\Theme\Model\ResourceModel\Theme\Collection;
+use Magento\Theme\Model\Theme\StoreThemesResolverInterface;
use Magento\Theme\Model\Theme\ThemeProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -32,47 +33,37 @@ class CustomizationTest extends TestCase
*/
protected $designPackage;
- /**
- * @var Collection
- */
- protected $themeCollection;
-
/**
* @var Customization
*/
protected $model;
/**
- * @var ThemeProvider|\PHPUnit\Framework\MockObject_MockBuilder
+ * @var ThemeProvider|MockObject
*/
protected $themeProviderMock;
+ /**
+ * @var StoreThemesResolverInterface|MockObject
+ */
+ private $storeThemesResolver;
protected function setUp(): void
{
- $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)
- ->getMock();
- $this->designPackage = $this->getMockBuilder(DesignInterface::class)
- ->getMock();
- $this->themeCollection = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $collectionFactory = $this->getMockBuilder(\Magento\Theme\Model\ResourceModel\Theme\CollectionFactory::class)
- ->disableOriginalConstructor()
- ->setMethods(['create'])
- ->getMock();
-
- $collectionFactory->expects($this->any())->method('create')->willReturn($this->themeCollection);
+ $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)->getMock();
+ $this->designPackage = $this->getMockBuilder(DesignInterface::class)->getMock();
$this->themeProviderMock = $this->getMockBuilder(ThemeProvider::class)
->disableOriginalConstructor()
->setMethods(['getThemeCustomizations', 'getThemeByFullPath'])
->getMock();
+ $this->storeThemesResolver = $this->createMock(StoreThemesResolverInterface::class);
+
$this->model = new Customization(
$this->storeManager,
$this->designPackage,
- $this->themeProviderMock
+ $this->themeProviderMock,
+ $this->storeThemesResolver
);
}
@@ -84,13 +75,15 @@ protected function setUp(): void
*/
public function testGetAssignedThemeCustomizations()
{
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
-
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
+
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$this->themeProviderMock->expects($this->once())
->method('getThemeCustomizations')
@@ -108,13 +101,15 @@ public function testGetAssignedThemeCustomizations()
*/
public function testGetUnassignedThemeCustomizations()
{
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$this->themeProviderMock->expects($this->once())
->method('getThemeCustomizations')
@@ -131,13 +126,15 @@ public function testGetUnassignedThemeCustomizations()
*/
public function testGetStoresByThemes()
{
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$stores = $this->model->getStoresByThemes();
$this->assertArrayHasKey($this->getAssignedTheme()->getId(), $stores);
@@ -148,15 +145,17 @@ public function testGetStoresByThemes()
* @covers \Magento\Theme\Model\Config\Customization::_getConfigurationThemeId
* @covers \Magento\Theme\Model\Config\Customization::__construct
*/
- public function testIsThemeAssignedToDefaultStore()
+ public function testIsThemeAssignedToAnyStore()
{
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$this->themeProviderMock->expects($this->once())
->method('getThemeCustomizations')
@@ -198,10 +197,10 @@ protected function getUnassignedTheme()
}
/**
- * @return DataObject
+ * @return StoreInterface|MockObject
*/
protected function getStore()
{
- return new DataObject(['id' => 55]);
+ return $this->createConfiguredMock(StoreInterface::class, ['getId' => 55]);
}
}
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php
new file mode 100644
index 0000000000000..939b47a42ce85
--- /dev/null
+++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php
@@ -0,0 +1,115 @@
+createMock(CollectionFactory::class);
+ $this->design = $this->createMock(DesignInterface::class);
+ $this->model = new StoreDefaultThemeResolver(
+ $themeCollectionFactory,
+ $this->design
+ );
+ $registeredThemes = [];
+ $registeredThemes[] = $this->createConfiguredMock(
+ ThemeInterface::class,
+ [
+ 'getId' => 1,
+ 'getCode' => 'Magento/luma',
+ ]
+ );
+ $registeredThemes[] = $this->createConfiguredMock(
+ ThemeInterface::class,
+ [
+ 'getId' => 2,
+ 'getCode' => 'Magento/blank',
+ ]
+ );
+ $collection = $this->createMock(Collection::class);
+ $collection->method('getIterator')
+ ->willReturn(new ArrayIterator($registeredThemes));
+ $collection->method('loadRegisteredThemes')
+ ->willReturnSelf();
+ $themeCollectionFactory->method('create')
+ ->willReturn($collection);
+ }
+
+ /**
+ * Test that method returns default theme associated to given store.
+ *
+ * @param string|null $defaultTheme
+ * @param array $expected
+ * @dataProvider getThemesDataProvider
+ */
+ public function testGetThemes(?string $defaultTheme, array $expected): void
+ {
+ $store = $this->createMock(StoreInterface::class);
+ $this->design->expects($this->once())
+ ->method('getConfigurationDesignTheme')
+ ->with(
+ Area::AREA_FRONTEND,
+ ['store' => $store]
+ )
+ ->willReturn($defaultTheme);
+ $this->assertEquals($expected, $this->model->getThemes($store));
+ }
+
+ /**
+ * @return array
+ */
+ public function getThemesDataProvider(): array
+ {
+ return [
+ [
+ null,
+ []
+ ],
+ [
+ '1',
+ [1]
+ ],
+ [
+ 'Magento/blank',
+ [2]
+ ],
+ [
+ 'Magento/theme',
+ []
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php
new file mode 100644
index 0000000000000..b80ec4ae83887
--- /dev/null
+++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php
@@ -0,0 +1,115 @@
+resolvers = [];
+ $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class);
+ $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class);
+ $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class);
+ $this->model = new StoreThemesResolver($this->resolvers);
+ }
+
+ /**
+ * Test that constructor SHOULD throw an exception when resolver is not instance of StoreThemesResolverInterface.
+ */
+ public function testInvalidConstructorArguments(): void
+ {
+ $resolver = $this->createMock(StoreInterface::class);
+ $this->expectExceptionObject(
+ new \InvalidArgumentException(
+ sprintf(
+ 'Instance of %s is expected, got %s instead.',
+ StoreThemesResolverInterface::class,
+ get_class($resolver)
+ )
+ )
+ );
+ $this->model = new StoreThemesResolver(
+ [
+ $resolver
+ ]
+ );
+ }
+
+ /**
+ * Test that method returns aggregated themes from resolvers
+ *
+ * @param array $themes
+ * @param array $expected
+ * @dataProvider getThemesDataProvider
+ */
+ public function testGetThemes(array $themes, array $expected): void
+ {
+ $store = $this->createMock(StoreInterface::class);
+ foreach ($this->resolvers as $key => $resolver) {
+ $resolver->expects($this->once())
+ ->method('getThemes')
+ ->willReturn($themes[$key]);
+ }
+ $this->assertEquals($expected, $this->model->getThemes($store));
+ }
+
+ /**
+ * @return array
+ */
+ public function getThemesDataProvider(): array
+ {
+ return [
+ [
+ [
+ [],
+ [],
+ []
+ ],
+ []
+ ],
+ [
+ [
+ ['1'],
+ [],
+ ['1']
+ ],
+ ['1']
+ ],
+ [
+ [
+ ['1'],
+ ['2'],
+ ['1']
+ ],
+ ['1', '2']
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php
new file mode 100644
index 0000000000000..1ef4b17ca6562
--- /dev/null
+++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php
@@ -0,0 +1,105 @@
+scopeConfig = $this->createMock(ScopeConfigInterface::class);
+ $this->serializer = new Json();
+ $this->model = new StoreUserAgentThemeResolver(
+ $this->scopeConfig,
+ $this->serializer
+ );
+ }
+
+ /**
+ * Test that method returns user-agent rules associated themes.
+ *
+ * @param array|null $config
+ * @param array $expected
+ * @dataProvider getThemesDataProvider
+ */
+ public function testGetThemes(?array $config, array $expected): void
+ {
+ $store = $this->createMock(StoreInterface::class);
+ $this->scopeConfig->expects($this->once())
+ ->method('getValue')
+ ->with('design/theme/ua_regexp', ScopeInterface::SCOPE_STORE, $store)
+ ->willReturn($config !== null ? $this->serializer->serialize($config) : $config);
+ $this->assertEquals($expected, $this->model->getThemes($store));
+ }
+
+ /**
+ * @return array
+ */
+ public function getThemesDataProvider(): array
+ {
+ return [
+ [
+ null,
+ []
+ ],
+ [
+ [],
+ []
+ ],
+ [
+ [
+ [
+ 'search' => '\/Chrome\/i',
+ 'regexp' => '\/Chrome\/i',
+ 'value' => '1',
+ ],
+ ],
+ ['1']
+ ],
+ [
+ [
+ [
+ 'search' => '\/Chrome\/i',
+ 'regexp' => '\/Chrome\/i',
+ 'value' => '1',
+ ],
+ [
+ 'search' => '\/mozila\/i',
+ 'regexp' => '\/mozila\/i',
+ 'value' => '2',
+ ],
+ ],
+ ['1', '2']
+ ]
+ ];
+ }
+}
diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php
similarity index 80%
rename from lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php
rename to app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php
index 549d45a986cf0..4efcc584986d1 100644
--- a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php
+++ b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php
@@ -3,15 +3,13 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-declare(strict_types=1);
-
-namespace Magento\Framework\App\Test\Unit\Action\Plugin;
+namespace Magento\Theme\Test\Unit\Plugin;
use Magento\Framework\App\Action\Action;
-use Magento\Framework\App\Action\Plugin\LoadDesignPlugin;
use Magento\Framework\App\ActionInterface;
use Magento\Framework\Message\ManagerInterface;
use Magento\Framework\View\DesignLoader;
+use Magento\Theme\Plugin\LoadDesignPlugin;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -26,7 +24,7 @@ public function testBeforeExecute()
$designLoaderMock = $this->createMock(DesignLoader::class);
/** @var MockObject|ManagerInterface $messageManagerMock */
- $messageManagerMock = $this->getMockForAbstractClass(ManagerInterface::class);
+ $messageManagerMock = $this->createMock(ManagerInterface::class);
$plugin = new LoadDesignPlugin($designLoaderMock, $messageManagerMock);
diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml
index 921e6bfc6ecf1..15107adb931c9 100644
--- a/app/code/Magento/Theme/etc/di.xml
+++ b/app/code/Magento/Theme/etc/di.xml
@@ -18,6 +18,7 @@
+
Magento\Framework\App\Cache\Type\Config
@@ -104,6 +105,9 @@
Magento\Store\Model\ScopeInterface::SCOPE_STORE
+
+
+
@@ -309,4 +313,12 @@
configured_design_cache
+
+
+
+ - Magento\Theme\Model\Theme\StoreDefaultThemeResolver
+ - Magento\Theme\Model\Theme\StoreUserAgentThemeResolver
+
+
+
diff --git a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml
index 55798169cdf75..b42cabde6cd85 100644
--- a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml
+++ b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml
@@ -14,7 +14,6 @@