-
+
getFileThumbUrl($file)):?>
diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml
index 9f886f6f1345e..793fc7d26cb4a 100644
--- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml
+++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml
@@ -146,7 +146,6 @@
true
- true
text
diff --git a/app/code/Magento/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php
index 83c61f90f789a..c07872a630830 100644
--- a/app/code/Magento/Config/App/Config/Type/System.php
+++ b/app/code/Magento/Config/App/Config/Type/System.php
@@ -11,6 +11,7 @@
use Magento\Framework\App\Config\Spi\PreProcessorInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Config\App\Config\Type\System\Reader;
+use Magento\Framework\Lock\LockManagerInterface;
use Magento\Framework\Serialize\Serializer\Sensitive as SensitiveSerializer;
use Magento\Framework\Serialize\Serializer\SensitiveFactory as SensitiveSerializerFactory;
use Magento\Framework\App\ScopeInterface;
@@ -24,12 +25,43 @@
*
* @api
* @since 100.1.2
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class System implements ConfigTypeInterface
{
+ /**
+ * Config cache tag.
+ */
const CACHE_TAG = 'config_scopes';
+
+ /**
+ * System config type.
+ */
const CONFIG_TYPE = 'system';
+ /**
+ * @var string
+ */
+ private static $lockName = 'SYSTEM_CONFIG';
+
+ /**
+ * Timeout between retrieves to load the configuration from the cache.
+ *
+ * Value of the variable in microseconds.
+ *
+ * @var int
+ */
+ private static $delayTimeout = 50000;
+
+ /**
+ * Lifetime of the lock for write in cache.
+ *
+ * Value of the variable in seconds.
+ *
+ * @var int
+ */
+ private static $lockTimeout = 8;
+
/**
* @var array
*/
@@ -71,6 +103,11 @@ class System implements ConfigTypeInterface
*/
private $availableDataScopes;
+ /**
+ * @var LockManagerInterface
+ */
+ private $locker;
+
/**
* @param ConfigSourceInterface $source
* @param PostProcessorInterface $postProcessor
@@ -82,7 +119,7 @@ class System implements ConfigTypeInterface
* @param string $configType
* @param Reader $reader
* @param SensitiveSerializerFactory|null $sensitiveFactory
- *
+ * @param LockManagerInterface|null $locker
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
@@ -96,7 +133,8 @@ public function __construct(
$cachingNestedLevel = 1,
$configType = self::CONFIG_TYPE,
Reader $reader = null,
- SensitiveSerializerFactory $sensitiveFactory = null
+ SensitiveSerializerFactory $sensitiveFactory = null,
+ LockManagerInterface $locker = null
) {
$this->postProcessor = $postProcessor;
$this->cache = $cache;
@@ -110,6 +148,7 @@ public function __construct(
$this->serializer = $sensitiveFactory->create(
['serializer' => $serializer]
);
+ $this->locker = $locker ?: ObjectManager::getInstance()->get(LockManagerInterface::class);
}
/**
@@ -153,7 +192,7 @@ private function getWithParts($path)
if (count($pathParts) === 1 && $pathParts[0] !== ScopeInterface::SCOPE_DEFAULT) {
if (!isset($this->data[$pathParts[0]])) {
- $data = $this->readData();
+ $data = $this->loadAllData();
$this->data = array_replace_recursive($data, $this->data);
}
@@ -186,21 +225,60 @@ private function getWithParts($path)
}
/**
- * Load configuration data for all scopes
+ * Make lock on data load.
*
+ * @param callable $dataLoader
+ * @param bool $flush
* @return array
*/
- private function loadAllData()
+ private function lockedLoadData(callable $dataLoader, bool $flush = false): array
{
- $cachedData = $this->cache->load($this->configType);
+ $cachedData = $dataLoader(); //optimistic read
- if ($cachedData === false) {
- $data = $this->readData();
- } else {
- $data = $this->serializer->unserialize($cachedData);
+ while ($cachedData === false && $this->locker->isLocked(self::$lockName)) {
+ usleep(self::$delayTimeout);
+ $cachedData = $dataLoader();
}
- return $data;
+ while ($cachedData === false) {
+ try {
+ if ($this->locker->lock(self::$lockName, self::$lockTimeout)) {
+ if (!$flush) {
+ $data = $this->readData();
+ $this->cacheData($data);
+ $cachedData = $data;
+ } else {
+ $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]);
+ $cachedData = [];
+ }
+ }
+ } finally {
+ $this->locker->unlock(self::$lockName);
+ }
+
+ if ($cachedData === false) {
+ usleep(self::$delayTimeout);
+ $cachedData = $dataLoader();
+ }
+ }
+
+ return $cachedData;
+ }
+
+ /**
+ * Load configuration data for all scopes
+ *
+ * @return array
+ */
+ private function loadAllData()
+ {
+ return $this->lockedLoadData(function () {
+ $cachedData = $this->cache->load($this->configType);
+ if ($cachedData === false) {
+ return $cachedData;
+ }
+ return $this->serializer->unserialize($cachedData);
+ });
}
/**
@@ -211,16 +289,13 @@ private function loadAllData()
*/
private function loadDefaultScopeData($scopeType)
{
- $cachedData = $this->cache->load($this->configType . '_' . $scopeType);
-
- if ($cachedData === false) {
- $data = $this->readData();
- $this->cacheData($data);
- } else {
- $data = [$scopeType => $this->serializer->unserialize($cachedData)];
- }
-
- return $data;
+ return $this->lockedLoadData(function () use ($scopeType) {
+ $cachedData = $this->cache->load($this->configType . '_' . $scopeType);
+ if ($cachedData === false) {
+ return $cachedData;
+ }
+ return [$scopeType => $this->serializer->unserialize($cachedData)];
+ });
}
/**
@@ -232,25 +307,22 @@ private function loadDefaultScopeData($scopeType)
*/
private function loadScopeData($scopeType, $scopeId)
{
- $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId);
-
- if ($cachedData === false) {
- if ($this->availableDataScopes === null) {
- $cachedScopeData = $this->cache->load($this->configType . '_scopes');
- if ($cachedScopeData !== false) {
- $this->availableDataScopes = $this->serializer->unserialize($cachedScopeData);
+ return $this->lockedLoadData(function () use ($scopeType, $scopeId) {
+ $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId);
+ if ($cachedData === false) {
+ if ($this->availableDataScopes === null) {
+ $cachedScopeData = $this->cache->load($this->configType . '_scopes');
+ if ($cachedScopeData !== false) {
+ $this->availableDataScopes = $this->serializer->unserialize($cachedScopeData);
+ }
}
+ if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) {
+ return [$scopeType => [$scopeId => []]];
+ }
+ return false;
}
- if (is_array($this->availableDataScopes) && !isset($this->availableDataScopes[$scopeType][$scopeId])) {
- return [$scopeType => [$scopeId => []]];
- }
- $data = $this->readData();
- $this->cacheData($data);
- } else {
- $data = [$scopeType => [$scopeId => $this->serializer->unserialize($cachedData)]];
- }
-
- return $data;
+ return [$scopeType => [$scopeId => $this->serializer->unserialize($cachedData)]];
+ });
}
/**
@@ -340,6 +412,11 @@ private function readData(): array
public function clean()
{
$this->data = [];
- $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]);
+ $this->lockedLoadData(
+ function () {
+ return false;
+ },
+ true
+ );
}
}
diff --git a/app/code/Magento/Config/Block/System/Config/Form.php b/app/code/Magento/Config/Block/System/Config/Form.php
index 81e39a83296d7..63a2b811f93ec 100644
--- a/app/code/Magento/Config/Block/System/Config/Form.php
+++ b/app/code/Magento/Config/Block/System/Config/Form.php
@@ -143,13 +143,15 @@ public function __construct(
\Magento\Config\Model\Config\Structure $configStructure,
\Magento\Config\Block\System\Config\Form\Fieldset\Factory $fieldsetFactory,
\Magento\Config\Block\System\Config\Form\Field\Factory $fieldFactory,
- array $data = []
+ array $data = [],
+ SettingChecker $settingChecker = null
) {
parent::__construct($context, $registry, $formFactory, $data);
$this->_configFactory = $configFactory;
$this->_configStructure = $configStructure;
$this->_fieldsetFactory = $fieldsetFactory;
$this->_fieldFactory = $fieldFactory;
+ $this->settingChecker = $settingChecker ?: ObjectManager::getInstance()->get(SettingChecker::class);
$this->_scopeLabels = [
self::SCOPE_DEFAULT => __('[GLOBAL]'),
@@ -158,18 +160,6 @@ public function __construct(
];
}
- /**
- * @deprecated 100.1.2
- * @return SettingChecker
- */
- private function getSettingChecker()
- {
- if ($this->settingChecker === null) {
- $this->settingChecker = ObjectManager::getInstance()->get(SettingChecker::class);
- }
- return $this->settingChecker;
- }
-
/**
* Initialize objects required to render config form
*
@@ -366,9 +356,8 @@ protected function _initElement(
$sharedClass = $this->_getSharedCssClass($field);
$requiresClass = $this->_getRequiresCssClass($field, $fieldPrefix);
+ $isReadOnly = $this->isReadOnly($field, $path);
- $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath())
- ?: $this->getSettingChecker()->isReadOnly($path, $this->getScope(), $this->getStringScopeCode());
$formField = $fieldset->addField(
$elementId,
$field->getType(),
@@ -417,7 +406,7 @@ private function getFieldData(\Magento\Config\Model\Config\Structure\Element\Fie
{
$data = $this->getAppConfigDataValue($path);
- $placeholderValue = $this->getSettingChecker()->getPlaceholderValue(
+ $placeholderValue = $this->settingChecker->getPlaceholderValue(
$path,
$this->getScope(),
$this->getStringScopeCode()
@@ -434,6 +423,10 @@ private function getFieldData(\Magento\Config\Model\Config\Structure\Element\Fie
$backendModel = $field->getBackendModel();
// Backend models which implement ProcessorInterface are processed by ScopeConfigInterface
if (!$backendModel instanceof ProcessorInterface) {
+ if (array_key_exists($path, $this->_configData)) {
+ $data = $this->_configData[$path];
+ }
+
$backendModel->setPath($path)
->setValue($data)
->setWebsite($this->getWebsiteCode())
@@ -718,6 +711,7 @@ protected function _getAdditionalElementTypes()
*
* @TODO delete this methods when {^see above^} is done
* @return string
+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod)
*/
public function getSectionCode()
{
@@ -729,6 +723,7 @@ public function getSectionCode()
*
* @TODO delete this methods when {^see above^} is done
* @return string
+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod)
*/
public function getWebsiteCode()
{
@@ -740,6 +735,7 @@ public function getWebsiteCode()
*
* @TODO delete this methods when {^see above^} is done
* @return string
+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod)
*/
public function getStoreCode()
{
@@ -797,6 +793,26 @@ private function getAppConfig()
return $this->appConfig;
}
+ /**
+ * Check Path is Readonly
+ *
+ * @param \Magento\Config\Model\Config\Structure\Element\Field $field
+ * @param string $path
+ * @return boolean
+ */
+ private function isReadOnly(\Magento\Config\Model\Config\Structure\Element\Field $field, $path)
+ {
+ $isReadOnly = $this->settingChecker->isReadOnly(
+ $path,
+ ScopeConfigInterface::SCOPE_TYPE_DEFAULT
+ );
+ if (!$isReadOnly) {
+ $isReadOnly = $this->getElementVisibility()->isDisabled($field->getPath())
+ ?: $this->settingChecker->isReadOnly($path, $this->getScope(), $this->getStringScopeCode());
+ }
+ return $isReadOnly;
+ }
+
/**
* Retrieve deployment config data value by path
*
diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php
index d7d513bfad423..86ae1f96749df 100644
--- a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php
+++ b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php
@@ -7,17 +7,19 @@
use Magento\Config\App\Config\Type\System;
use Magento\Config\Console\Command\ConfigSetCommand;
+use Magento\Config\Model\Config\Factory as ConfigFactory;
use Magento\Framework\App\Config\ConfigPathResolver;
use Magento\Framework\App\DeploymentConfig;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Config\Model\PreparedValueFactory;
-use Magento\Framework\App\Config\Value;
/**
* Processes default flow of config:set command.
+ *
* This processor saves the value of configuration into database.
*
- * {@inheritdoc}
+ * @inheritdoc
* @api
* @since 100.2.0
*/
@@ -44,26 +46,36 @@ class DefaultProcessor implements ConfigSetProcessorInterface
*/
private $preparedValueFactory;
+ /**
+ * @var ConfigFactory
+ */
+ private $configFactory;
+
/**
* @param PreparedValueFactory $preparedValueFactory The factory for prepared value
* @param DeploymentConfig $deploymentConfig The deployment configuration reader
* @param ConfigPathResolver $configPathResolver The resolver for configuration paths according to source type
+ * @param ConfigFactory|null $configFactory
*/
public function __construct(
PreparedValueFactory $preparedValueFactory,
DeploymentConfig $deploymentConfig,
- ConfigPathResolver $configPathResolver
+ ConfigPathResolver $configPathResolver,
+ ConfigFactory $configFactory = null
) {
$this->preparedValueFactory = $preparedValueFactory;
$this->deploymentConfig = $deploymentConfig;
$this->configPathResolver = $configPathResolver;
+
+ $this->configFactory = $configFactory ?? ObjectManager::getInstance()->get(ConfigFactory::class);
}
/**
* Processes database flow of config:set command.
+ *
* Requires installed application.
*
- * {@inheritdoc}
+ * @inheritdoc
* @since 100.2.0
*/
public function process($path, $value, $scope, $scopeCode)
@@ -78,12 +90,12 @@ public function process($path, $value, $scope, $scopeCode)
}
try {
- /** @var Value $backendModel */
- $backendModel = $this->preparedValueFactory->create($path, $value, $scope, $scopeCode);
- if ($backendModel instanceof Value) {
- $resourceModel = $backendModel->getResource();
- $resourceModel->save($backendModel);
- }
+ $config = $this->configFactory->create([
+ 'scope' => $scope,
+ 'scope_code' => $scopeCode,
+ ]);
+ $config->setDataByPath($path, $value);
+ $config->save();
} catch (\Exception $exception) {
throw new CouldNotSaveException(__('%1', $exception->getMessage()), $exception);
}
diff --git a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php
index cb79daddbf5f9..999d8e41af5bc 100644
--- a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php
+++ b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php
@@ -114,13 +114,13 @@ protected function configure()
),
new InputOption(
static::OPTION_LOCK_ENV,
- 'le',
+ 'e',
InputOption::VALUE_NONE,
'Lock value which prevents modification in the Admin (will be saved in app/etc/env.php)'
),
new InputOption(
static::OPTION_LOCK_CONFIG,
- 'lc',
+ 'c',
InputOption::VALUE_NONE,
'Lock and share value with other installations, prevents modification in the Admin '
. '(will be saved in app/etc/config.php)'
@@ -139,8 +139,10 @@ protected function configure()
/**
* Creates and run appropriate processor, depending on input options.
*
- * {@inheritdoc}
+ * @param InputInterface $input
+ * @param OutputInterface $output
* @since 100.2.0
+ * @return int|null
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php
index 7f7d461ea090d..893a73654137e 100644
--- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php
+++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php
@@ -6,6 +6,7 @@
namespace Magento\Config\Controller\Adminhtml\System\Config;
use Magento\Config\Controller\Adminhtml\System\AbstractConfig;
+use Magento\Framework\Exception\NotFoundException;
/**
* System Configuration Save Controller
@@ -140,9 +141,14 @@ protected function _saveAdvanced()
* Save configuration
*
* @return \Magento\Backend\Model\View\Result\Redirect
+ * @throws NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found'));
+ }
+
try {
// custom save logic
$this->_saveSection();
diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php
index 0472c5daa276f..6bf191c20a844 100644
--- a/app/code/Magento/Config/Model/Config.php
+++ b/app/code/Magento/Config/Model/Config.php
@@ -9,15 +9,32 @@
use Magento\Config\Model\Config\Structure\Element\Group;
use Magento\Config\Model\Config\Structure\Element\Field;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\App\ScopeInterface;
+use Magento\Framework\App\ScopeResolverPool;
+use Magento\Store\Model\ScopeInterface as StoreScopeInterface;
+use Magento\Store\Model\ScopeTypeNormalizer;
/**
* Backend config model
+ *
* Used to save configuration
*
* @author Magento Core Team
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @api
* @since 100.0.2
+ * @method string getSection()
+ * @method void setSection(string $section)
+ * @method string getWebsite()
+ * @method void setWebsite(string $website)
+ * @method string getStore()
+ * @method void setStore(string $store)
+ * @method string getScope()
+ * @method void setScope(string $scope)
+ * @method int getScopeId()
+ * @method void setScopeId(int $scopeId)
+ * @method string getScopeCode()
+ * @method void setScopeCode(string $scopeCode)
*/
class Config extends \Magento\Framework\DataObject
{
@@ -87,6 +104,16 @@ class Config extends \Magento\Framework\DataObject
*/
private $settingChecker;
+ /**
+ * @var ScopeResolverPool
+ */
+ private $scopeResolverPool;
+
+ /**
+ * @var ScopeTypeNormalizer
+ */
+ private $scopeTypeNormalizer;
+
/**
* @param \Magento\Framework\App\Config\ReinitableConfigInterface $config
* @param \Magento\Framework\Event\ManagerInterface $eventManager
@@ -97,6 +124,9 @@ class Config extends \Magento\Framework\DataObject
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param Config\Reader\Source\Deployed\SettingChecker|null $settingChecker
* @param array $data
+ * @param ScopeResolverPool|null $scopeResolverPool
+ * @param ScopeTypeNormalizer|null $scopeTypeNormalizer
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
\Magento\Framework\App\Config\ReinitableConfigInterface $config,
@@ -107,7 +137,9 @@ public function __construct(
\Magento\Framework\App\Config\ValueFactory $configValueFactory,
\Magento\Store\Model\StoreManagerInterface $storeManager,
SettingChecker $settingChecker = null,
- array $data = []
+ array $data = [],
+ ScopeResolverPool $scopeResolverPool = null,
+ ScopeTypeNormalizer $scopeTypeNormalizer = null
) {
parent::__construct($data);
$this->_eventManager = $eventManager;
@@ -117,11 +149,17 @@ public function __construct(
$this->_configLoader = $configLoader;
$this->_configValueFactory = $configValueFactory;
$this->_storeManager = $storeManager;
- $this->settingChecker = $settingChecker ?: ObjectManager::getInstance()->get(SettingChecker::class);
+ $this->settingChecker = $settingChecker
+ ?? ObjectManager::getInstance()->get(SettingChecker::class);
+ $this->scopeResolverPool = $scopeResolverPool
+ ?? ObjectManager::getInstance()->get(ScopeResolverPool::class);
+ $this->scopeTypeNormalizer = $scopeTypeNormalizer
+ ?? ObjectManager::getInstance()->get(ScopeTypeNormalizer::class);
}
/**
* Save config section
+ *
* Require set: section, website, store and groups
*
* @throws \Exception
@@ -386,6 +424,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,
@@ -482,30 +525,35 @@ public function setDataByPath($path, $value)
if ($path === '') {
throw new \UnexpectedValueException('Path must not be empty');
}
+
$pathParts = explode('/', $path);
$keyDepth = count($pathParts);
- if ($keyDepth !== 3) {
+ if ($keyDepth < 3) {
throw new \UnexpectedValueException(
- "Allowed depth of configuration is 3 (//). Your configuration depth is "
- . $keyDepth . " for path '$path'"
+ 'Minimal depth of configuration is 3. Your configuration depth is ' . $keyDepth
);
}
+
+ $section = array_shift($pathParts);
$data = [
- 'section' => $pathParts[0],
- 'groups' => [
- $pathParts[1] => [
- 'fields' => [
- $pathParts[2] => ['value' => $value],
- ],
- ],
+ 'fields' => [
+ array_pop($pathParts) => ['value' => $value],
],
];
+ while ($pathParts) {
+ $data = [
+ 'groups' => [
+ array_pop($pathParts) => $data,
+ ],
+ ];
+ }
+ $data['section'] = $section;
$this->addData($data);
}
/**
- * Get scope name and scopeId
- * @todo refactor to scope resolver
+ * Set scope data
+ *
* @return void
*/
private function initScope()
@@ -513,31 +561,66 @@ private function initScope()
if ($this->getSection() === null) {
$this->setSection('');
}
+
+ $scope = $this->retrieveScope();
+ $this->setScope($this->scopeTypeNormalizer->normalize($scope->getScopeType()));
+ $this->setScopeCode($scope->getCode());
+ $this->setScopeId($scope->getId());
+
if ($this->getWebsite() === null) {
- $this->setWebsite('');
+ $this->setWebsite(StoreScopeInterface::SCOPE_WEBSITES === $this->getScope() ? $scope->getId() : '');
}
if ($this->getStore() === null) {
- $this->setStore('');
+ $this->setStore(StoreScopeInterface::SCOPE_STORES === $this->getScope() ? $scope->getId() : '');
}
+ }
- if ($this->getStore()) {
- $scope = 'stores';
- $store = $this->_storeManager->getStore($this->getStore());
- $scopeId = (int)$store->getId();
- $scopeCode = $store->getCode();
- } elseif ($this->getWebsite()) {
- $scope = 'websites';
- $website = $this->_storeManager->getWebsite($this->getWebsite());
- $scopeId = (int)$website->getId();
- $scopeCode = $website->getCode();
+ /**
+ * Retrieve scope from initial data
+ *
+ * @return ScopeInterface
+ */
+ private function retrieveScope(): ScopeInterface
+ {
+ $scopeType = $this->getScope();
+ if (!$scopeType) {
+ switch (true) {
+ case $this->getStore():
+ $scopeType = StoreScopeInterface::SCOPE_STORES;
+ $scopeIdentifier = $this->getStore();
+ break;
+ case $this->getWebsite():
+ $scopeType = StoreScopeInterface::SCOPE_WEBSITES;
+ $scopeIdentifier = $this->getWebsite();
+ break;
+ default:
+ $scopeType = ScopeInterface::SCOPE_DEFAULT;
+ $scopeIdentifier = null;
+ break;
+ }
} else {
- $scope = 'default';
- $scopeId = 0;
- $scopeCode = '';
+ switch (true) {
+ case $this->getScopeId() !== null:
+ $scopeIdentifier = $this->getScopeId();
+ break;
+ case $this->getScopeCode() !== null:
+ $scopeIdentifier = $this->getScopeCode();
+ break;
+ case $this->getStore() !== null:
+ $scopeIdentifier = $this->getStore();
+ break;
+ case $this->getWebsite() !== null:
+ $scopeIdentifier = $this->getWebsite();
+ break;
+ default:
+ $scopeIdentifier = null;
+ break;
+ }
}
- $this->setScope($scope);
- $this->setScopeId($scopeId);
- $this->setScopeCode($scopeCode);
+ $scope = $this->scopeResolverPool->get($scopeType)
+ ->getScope($scopeIdentifier);
+
+ return $scope;
}
/**
diff --git a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php
index 9a483de6a695b..d12569eebe5b2 100644
--- a/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php
+++ b/app/code/Magento/Config/Model/Config/Backend/Admin/Usecustom.php
@@ -56,8 +56,9 @@ public function beforeSave()
{
$value = $this->getValue();
if ($value == 1) {
- $customUrl = $this->getData('groups/url/fields/custom/value');
- if (empty($customUrl)) {
+ $customUrlField = $this->getData('groups/url/fields/custom/value');
+ $customUrlConfig = $this->_config->getValue('admin/url/custom');
+ if (empty($customUrlField) && empty($customUrlConfig)) {
throw new \Magento\Framework\Exception\LocalizedException(__('Please specify the admin custom URL.'));
}
}
diff --git a/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php b/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php
index 4ae66bfd9692b..25303093ace5d 100644
--- a/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php
+++ b/app/code/Magento/Config/Model/Config/Backend/Currency/AbstractCurrency.php
@@ -14,6 +14,8 @@
namespace Magento\Config\Model\Config\Backend\Currency;
/**
+ * Base currency class
+ *
* @api
* @since 100.0.2
*/
@@ -26,18 +28,19 @@ abstract class AbstractCurrency extends \Magento\Framework\App\Config\Value
*/
protected function _getAllowedCurrencies()
{
- if (!$this->isFormData() || $this->getData('groups/options/fields/allow/inherit')) {
- return explode(
+ $allowValue = $this->getData('groups/options/fields/allow/value');
+ $allowedCurrencies = $allowValue === null || $this->getData('groups/options/fields/allow/inherit')
+ ? explode(
',',
(string)$this->_config->getValue(
\Magento\Directory\Model\Currency::XML_PATH_CURRENCY_ALLOW,
$this->getScope(),
$this->getScopeId()
)
- );
- }
+ )
+ : (array) $allowValue;
- return (array)$this->getData('groups/options/fields/allow/value');
+ return $allowedCurrencies;
}
/**
diff --git a/app/code/Magento/Config/Model/Config/Backend/Serialized.php b/app/code/Magento/Config/Model/Config/Backend/Serialized.php
index 3d5713357c39c..4d5da764db470 100644
--- a/app/code/Magento/Config/Model/Config/Backend/Serialized.php
+++ b/app/code/Magento/Config/Model/Config/Backend/Serialized.php
@@ -52,7 +52,18 @@ protected function _afterLoad()
{
$value = $this->getValue();
if (!is_array($value)) {
- $this->setValue(empty($value) ? false : $this->serializer->unserialize($value));
+ try {
+ $this->setValue(empty($value) ? false : $this->serializer->unserialize($value));
+ } catch (\Exception $e) {
+ $this->_logger->critical(
+ sprintf(
+ 'Failed to unserialize %s config value. The error is: %s',
+ $this->getPath(),
+ $e->getMessage()
+ )
+ );
+ $this->setValue(false);
+ }
}
}
diff --git a/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php b/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php
index b3474674cf76d..5beff0d043ade 100644
--- a/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php
+++ b/app/code/Magento/Config/Model/Config/Source/Locale/Currency.php
@@ -4,12 +4,15 @@
* See COPYING.txt for license details.
*/
-/**
- * Locale currency source
- */
namespace Magento\Config\Model\Config\Source\Locale;
+use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Locale\ListsInterface;
+
/**
+ * Locale currency source.
+ *
* @api
* @since 100.0.2
*/
@@ -21,27 +24,70 @@ class Currency implements \Magento\Framework\Option\ArrayInterface
protected $_options;
/**
- * @var \Magento\Framework\Locale\ListsInterface
+ * @var ListsInterface
*/
protected $_localeLists;
/**
- * @param \Magento\Framework\Locale\ListsInterface $localeLists
+ * @var ScopeConfigInterface
*/
- public function __construct(\Magento\Framework\Locale\ListsInterface $localeLists)
- {
+ private $config;
+
+ /**
+ * @var array
+ */
+ private $installedCurrencies;
+
+ /**
+ * @param ListsInterface $localeLists
+ * @param ScopeConfigInterface $config
+ */
+ public function __construct(
+ ListsInterface $localeLists,
+ ScopeConfigInterface $config = null
+ ) {
$this->_localeLists = $localeLists;
+ $this->config = $config ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class);
}
/**
- * @return array
+ * @inheritdoc
*/
public function toOptionArray()
{
if (!$this->_options) {
$this->_options = $this->_localeLists->getOptionCurrencies();
}
- $options = $this->_options;
+
+ $selected = array_flip($this->getInstalledCurrencies());
+
+ $options = array_filter(
+ $this->_options,
+ function ($option) use ($selected) {
+ return isset($selected[$option['value']]);
+ }
+ );
+
return $options;
}
+
+ /**
+ * Retrieve Installed Currencies.
+ *
+ * @return array
+ */
+ private function getInstalledCurrencies()
+ {
+ if (!$this->installedCurrencies) {
+ $this->installedCurrencies = explode(
+ ',',
+ $this->config->getValue(
+ 'system/currency/installed',
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+ )
+ );
+ }
+
+ return $this->installedCurrencies;
+ }
}
diff --git a/app/code/Magento/Config/Setup/ConfigOptionsList.php b/app/code/Magento/Config/Setup/ConfigOptionsList.php
new file mode 100644
index 0000000000000..45e3987d282f1
--- /dev/null
+++ b/app/code/Magento/Config/Setup/ConfigOptionsList.php
@@ -0,0 +1,88 @@
+configDataFactory = $configDataFactory;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getOptions()
+ {
+ return [
+ new SelectConfigOption(
+ self::INPUT_KEY_DEBUG_LOGGING,
+ SelectConfigOption::FRONTEND_WIZARD_RADIO,
+ [true, false, 1, 0],
+ self::CONFIG_PATH_DEBUG_LOGGING,
+ 'Enable debug logging'
+ )
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function createConfig(array $options, DeploymentConfig $deploymentConfig)
+ {
+ $config = [];
+ if (isset($options[self::INPUT_KEY_DEBUG_LOGGING])) {
+ $configData = $this->configDataFactory->create(ConfigFilePool::APP_ENV);
+ if ($options[self::INPUT_KEY_DEBUG_LOGGING] === 'true'
+ || $options[self::INPUT_KEY_DEBUG_LOGGING] === '1') {
+ $value = 1;
+ } else {
+ $value = 0;
+ }
+ $configData->set(self::CONFIG_PATH_DEBUG_LOGGING, $value);
+ $config[] = $configData;
+ }
+
+ return $config;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function validate(array $options, DeploymentConfig $deploymentConfig)
+ {
+ return [];
+ }
+}
diff --git a/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml
new file mode 100644
index 0000000000000..e998730d11ae7
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/Data/LocaleOptionsData.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+ LocaleOptionsFrance
+
+
+ fr_FR
+
+
+
+ LocaleOptionsUSA
+
+
+ en_US
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml
new file mode 100644
index 0000000000000..6398d51cda916
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/Metadata/locale_options_config-meta.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+ string
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml
index c58e77d200bfb..62b5cdcf9ceec 100644
--- a/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml
+++ b/app/code/Magento/Config/Test/Mftf/Section/AdminSalesConfigSection.xml
@@ -7,7 +7,7 @@
-->
+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php
index de18d45d26864..011bcfee64af5 100644
--- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php
+++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/FileTest.php
@@ -11,6 +11,11 @@
class FileTest extends \PHPUnit\Framework\TestCase
{
+ /**
+ * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $escaperMock;
+
/**
* @var \Magento\Config\Block\System\Config\Form\Field\File
*/
@@ -24,6 +29,8 @@ class FileTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
$objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+ $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class);
+ $this->escaperMock->method('escapeHtml')->willReturnArgument(0);
$this->testData = [
'before_element_html' => 'test_before_element_html',
@@ -40,7 +47,10 @@ protected function setUp()
$this->file = $objectManager->getObject(
\Magento\Config\Block\System\Config\Form\Field\File::class,
- ['data' => $this->testData]
+ [
+ 'escaper' => $this->escaperMock,
+ 'data' => $this->testData
+ ]
);
$formMock = new \Magento\Framework\DataObject();
diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php
index e62aa37af47dc..6f771a2e38078 100644
--- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php
+++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php
@@ -13,6 +13,11 @@
class ImageTest extends \PHPUnit\Framework\TestCase
{
+ /**
+ * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $escaperMock;
+
/**
* @var \Magento\Framework\Url|\PHPUnit_Framework_MockObject_MockObject
*/
@@ -31,10 +36,13 @@ class ImageTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
$objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+ $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class);
+ $this->escaperMock->method('escapeHtml')->willReturnArgument(0);
$this->urlBuilderMock = $this->createMock(\Magento\Framework\Url::class);
$this->image = $objectManager->getObject(
\Magento\Config\Block\System\Config\Form\Field\Image::class,
[
+ 'escaper' => $this->escaperMock,
'urlBuilder' => $this->urlBuilderMock,
]
);
diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php
index f5c65e848b3bf..3799136aea9c0 100644
--- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php
+++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php
@@ -7,6 +7,11 @@
class AllowspecificTest extends \PHPUnit\Framework\TestCase
{
+ /**
+ * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $escaperMock;
+
/**
* @var \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific
*/
@@ -20,8 +25,11 @@ class AllowspecificTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
$testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+ $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class);
+ $this->escaperMock->method('escapeHtml')->willReturnArgument(0);
$this->_object = $testHelper->getObject(
- \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class
+ \Magento\Config\Block\System\Config\Form\Field\Select\Allowspecific::class,
+ ['escaper' => $this->escaperMock]
);
$this->_object->setData('html_id', 'spec_element');
$this->_formMock = $this->createPartialMock(
diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php
index 83b7bd5fda42e..528d141306cce 100644
--- a/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php
+++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/FormTest.php
@@ -103,6 +103,9 @@ protected function setUp()
$this->_fieldsetFactoryMock = $this->createMock(\Magento\Config\Block\System\Config\Form\Fieldset\Factory::class);
$this->_fieldFactoryMock = $this->createMock(\Magento\Config\Block\System\Config\Form\Field\Factory::class);
$this->_coreConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class);
+ $settingCheckerMock = $this->getMockBuilder(SettingChecker::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$this->_backendConfigMock = $this->createMock(\Magento\Config\Model\Config::class);
@@ -150,6 +153,7 @@ protected function setUp()
'fieldsetFactory' => $this->_fieldsetFactoryMock,
'fieldFactory' => $this->_fieldFactoryMock,
'context' => $context,
+ 'settingChecker' => $settingCheckerMock,
];
$objectArguments = $helper->getConstructArguments(\Magento\Config\Block\System\Config\Form::class, $data);
@@ -529,7 +533,7 @@ public function testInitFields(
$elementVisibilityMock = $this->getMockBuilder(ElementVisibilityInterface::class)
->getMockForAbstractClass();
- $elementVisibilityMock->expects($this->once())
+ $elementVisibilityMock->expects($this->any())
->method('isDisabled')
->willReturn($isDisabled);
diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php
index 984e0fe842687..3fb7d9ad21cd4 100644
--- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php
+++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSet/DefaultProcessorTest.php
@@ -7,13 +7,14 @@
use Magento\Config\App\Config\Type\System;
use Magento\Config\Console\Command\ConfigSet\DefaultProcessor;
+use Magento\Config\Model\Config;
+use Magento\Config\Model\Config\Factory as ConfigFactory;
use Magento\Framework\App\Config\ConfigPathResolver;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\DeploymentConfig;
use Magento\Store\Model\ScopeInterface;
use Magento\Config\Model\PreparedValueFactory;
use Magento\Framework\App\Config\Value;
-use Magento\Framework\App\Config\ValueInterface;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
use PHPUnit_Framework_MockObject_MockObject as Mock;
@@ -55,17 +56,18 @@ class DefaultProcessorTest extends \PHPUnit\Framework\TestCase
*/
private $resourceModelMock;
+ /**
+ * @var ConfigFactory|Mock
+ */
+ private $configFactory;
+
/**
* @inheritdoc
*/
protected function setUp()
{
- $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->configPathResolverMock = $this->getMockBuilder(ConfigPathResolver::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class);
+ $this->configPathResolverMock = $this->createMock(ConfigPathResolver::class);
$this->resourceModelMock = $this->getMockBuilder(AbstractDb::class)
->disableOriginalConstructor()
->setMethods(['save'])
@@ -74,14 +76,14 @@ protected function setUp()
->disableOriginalConstructor()
->setMethods(['getResource'])
->getMock();
- $this->preparedValueFactoryMock = $this->getMockBuilder(PreparedValueFactory::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->preparedValueFactoryMock = $this->createMock(PreparedValueFactory::class);
+ $this->configFactory = $this->createMock(ConfigFactory::class);
$this->model = new DefaultProcessor(
$this->preparedValueFactoryMock,
$this->deploymentConfigMock,
- $this->configPathResolverMock
+ $this->configPathResolverMock,
+ $this->configFactory
);
}
@@ -98,15 +100,14 @@ public function testProcess($path, $value, $scope, $scopeCode)
{
$this->configMockForProcessTest($path, $scope, $scopeCode);
- $this->preparedValueFactoryMock->expects($this->once())
- ->method('create')
- ->willReturn($this->valueMock);
- $this->valueMock->expects($this->once())
- ->method('getResource')
- ->willReturn($this->resourceModelMock);
- $this->resourceModelMock->expects($this->once())
+ $config = $this->createMock(Config::class);
+ $this->configFactory->method('create')
+ ->with(['scope' => $scope, 'scope_code' => $scopeCode])
+ ->willReturn($config);
+ $config->method('setDataByPath')
+ ->with($path, $value);
+ $config->expects($this->once())
->method('save')
- ->with($this->valueMock)
->willReturnSelf();
$this->model->process($path, $value, $scope, $scopeCode);
@@ -124,28 +125,6 @@ public function processDataProvider()
];
}
- public function testProcessWithWrongValueInstance()
- {
- $path = 'test/test/test';
- $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT;
- $scopeCode = null;
- $value = 'value';
- $valueInterfaceMock = $this->getMockBuilder(ValueInterface::class)
- ->getMockForAbstractClass();
-
- $this->configMockForProcessTest($path, $scope, $scopeCode);
-
- $this->preparedValueFactoryMock->expects($this->once())
- ->method('create')
- ->willReturn($valueInterfaceMock);
- $this->valueMock->expects($this->never())
- ->method('getResource');
- $this->resourceModelMock->expects($this->never())
- ->method('save');
-
- $this->model->process($path, $value, $scope, $scopeCode);
- }
-
/**
* @param string $path
* @param string $scope
@@ -185,6 +164,9 @@ public function testProcessLockedValue()
->method('resolve')
->willReturn('system/default/test/test/test');
+ $this->configFactory->expects($this->never())
+ ->method('create');
+
$this->model->process($path, $value, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null);
}
}
diff --git a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php b/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php
index 069a1c20b2966..980d8355de555 100644
--- a/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php
+++ b/app/code/Magento/Config/Test/Unit/Controller/Adminhtml/System/Config/SaveTest.php
@@ -69,6 +69,7 @@ class SaveTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
$this->_requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class);
+ $this->_requestMock->expects($this->any())->method('isPost')->willReturn(true);
$this->_responseMock = $this->createMock(\Magento\Framework\App\Response\Http::class);
$configStructureMock = $this->createMock(\Magento\Config\Model\Config\Structure::class);
diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php
index bb1e0e0225901..048df95f98649 100644
--- a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php
+++ b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php
@@ -9,6 +9,7 @@
use Magento\Framework\Model\Context;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Psr\Log\LoggerInterface;
class SerializedTest extends \PHPUnit\Framework\TestCase
{
@@ -18,14 +19,20 @@ class SerializedTest extends \PHPUnit\Framework\TestCase
/** @var Json|\PHPUnit_Framework_MockObject_MockObject */
private $serializerMock;
+ /** @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */
+ private $loggerMock;
+
protected function setUp()
{
$objectManager = new ObjectManager($this);
$this->serializerMock = $this->createMock(Json::class);
+ $this->loggerMock = $this->createMock(LoggerInterface::class);
$contextMock = $this->createMock(Context::class);
$eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class);
$contextMock->method('getEventDispatcher')
->willReturn($eventManagerMock);
+ $contextMock->method('getLogger')
+ ->willReturn($this->loggerMock);
$this->serializedConfig = $objectManager->getObject(
Serialized::class,
[
@@ -72,6 +79,20 @@ public function afterLoadDataProvider()
];
}
+ public function testAfterLoadWithException()
+ {
+ $value = '{"key":';
+ $expected = false;
+ $this->serializedConfig->setValue($value);
+ $this->serializerMock->expects($this->once())
+ ->method('unserialize')
+ ->willThrowException(new \Exception());
+ $this->loggerMock->expects($this->once())
+ ->method('critical');
+ $this->serializedConfig->afterLoad();
+ $this->assertEquals($expected, $this->serializedConfig->getValue());
+ }
+
/**
* @param string $expected
* @param int|double|string|array|boolean|null $value
diff --git a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php
index fcc1ff8b9c70c..a731be96af963 100644
--- a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php
+++ b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php
@@ -5,138 +5,183 @@
*/
namespace Magento\Config\Test\Unit\Model;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
+use Magento\Config\Model\Config;
+use Magento\Framework\App\Config\ReinitableConfigInterface;
+use Magento\Framework\Event\ManagerInterface;
+use Magento\Config\Model\Config\Structure\Reader;
+use Magento\Framework\DB\TransactionFactory;
+use Magento\Config\Model\Config\Loader;
+use Magento\Framework\App\Config\ValueFactory;
+use Magento\Store\Model\StoreManagerInterface;
+use Magento\Config\Model\Config\Structure;
+use Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker;
+use Magento\Framework\App\ScopeResolverPool;
+use Magento\Framework\App\ScopeResolverInterface;
+use Magento\Framework\App\ScopeInterface;
+use Magento\Store\Model\ScopeTypeNormalizer;
+use Magento\Framework\DB\Transaction;
+use Magento\Framework\App\Config\Value;
+use Magento\Store\Model\Website;
+use Magento\Config\Model\Config\Structure\Element\Group;
+use Magento\Config\Model\Config\Structure\Element\Field;
+
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class ConfigTest extends \PHPUnit\Framework\TestCase
{
/**
- * @var \Magento\Config\Model\Config
+ * @var Config
+ */
+ private $model;
+
+ /**
+ * @var ManagerInterface|MockObject
+ */
+ private $eventManagerMock;
+
+ /**
+ * @var Reader|MockObject
+ */
+ private $structureReaderMock;
+
+ /**
+ * @var TransactionFactory|MockObject
*/
- protected $_model;
+ private $transFactoryMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var ReinitableConfigInterface|MockObject
*/
- protected $_eventManagerMock;
+ private $appConfigMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var Loader|MockObject
*/
- protected $_structureReaderMock;
+ private $configLoaderMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var ValueFactory|MockObject
*/
- protected $_transFactoryMock;
+ private $dataFactoryMock;
/**
- * @var \Magento\Framework\App\Config\ReinitableConfigInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var StoreManagerInterface|MockObject
*/
- protected $_appConfigMock;
+ private $storeManager;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var Structure|MockObject
*/
- protected $_applicationMock;
+ private $configStructure;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var SettingChecker|MockObject
*/
- protected $_configLoaderMock;
+ private $settingsChecker;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var ScopeResolverPool|MockObject
*/
- protected $_dataFactoryMock;
+ private $scopeResolverPool;
/**
- * @var \Magento\Store\Model\StoreManagerInterface
+ * @var ScopeResolverInterface|MockObject
*/
- protected $_storeManager;
+ private $scopeResolver;
/**
- * @var \Magento\Config\Model\Config\Structure
+ * @var ScopeInterface|MockObject
*/
- protected $_configStructure;
+ private $scope;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var ScopeTypeNormalizer|MockObject
*/
- private $_settingsChecker;
+ private $scopeTypeNormalizer;
protected function setUp()
{
- $this->_eventManagerMock = $this->createMock(\Magento\Framework\Event\ManagerInterface::class);
- $this->_structureReaderMock = $this->createPartialMock(
- \Magento\Config\Model\Config\Structure\Reader::class,
+ $this->eventManagerMock = $this->createMock(ManagerInterface::class);
+ $this->structureReaderMock = $this->createPartialMock(
+ Reader::class,
['getConfiguration']
);
- $this->_configStructure = $this->createMock(\Magento\Config\Model\Config\Structure::class);
+ $this->configStructure = $this->createMock(Structure::class);
- $this->_structureReaderMock->expects(
- $this->any()
- )->method(
- 'getConfiguration'
- )->will(
- $this->returnValue($this->_configStructure)
- );
+ $this->structureReaderMock->method('getConfiguration')
+ ->willReturn($this->configStructure);
- $this->_transFactoryMock = $this->createPartialMock(
- \Magento\Framework\DB\TransactionFactory::class,
+ $this->transFactoryMock = $this->createPartialMock(
+ TransactionFactory::class,
['create', 'addObject']
);
- $this->_appConfigMock = $this->createMock(\Magento\Framework\App\Config\ReinitableConfigInterface::class);
- $this->_configLoaderMock = $this->createPartialMock(
- \Magento\Config\Model\Config\Loader::class,
+ $this->appConfigMock = $this->createMock(ReinitableConfigInterface::class);
+ $this->configLoaderMock = $this->createPartialMock(
+ Loader::class,
['getConfigByPath']
);
- $this->_dataFactoryMock = $this->createMock(\Magento\Framework\App\Config\ValueFactory::class);
-
- $this->_storeManager = $this->getMockForAbstractClass(\Magento\Store\Model\StoreManagerInterface::class);
-
- $this->_settingsChecker = $this
- ->createMock(\Magento\Config\Model\Config\Reader\Source\Deployed\SettingChecker::class);
-
- $this->_model = new \Magento\Config\Model\Config(
- $this->_appConfigMock,
- $this->_eventManagerMock,
- $this->_configStructure,
- $this->_transFactoryMock,
- $this->_configLoaderMock,
- $this->_dataFactoryMock,
- $this->_storeManager,
- $this->_settingsChecker
+ $this->dataFactoryMock = $this->createMock(ValueFactory::class);
+
+ $this->storeManager = $this->createMock(StoreManagerInterface::class);
+
+ $this->settingsChecker = $this->createMock(SettingChecker::class);
+
+ $this->scopeResolverPool = $this->createMock(ScopeResolverPool::class);
+ $this->scopeResolver = $this->createMock(ScopeResolverInterface::class);
+ $this->scopeResolverPool->method('get')
+ ->willReturn($this->scopeResolver);
+ $this->scope = $this->createMock(ScopeInterface::class);
+ $this->scopeResolver->method('getScope')
+ ->willReturn($this->scope);
+
+ $this->scopeTypeNormalizer = $this->createMock(ScopeTypeNormalizer::class);
+
+ $this->model = new Config(
+ $this->appConfigMock,
+ $this->eventManagerMock,
+ $this->configStructure,
+ $this->transFactoryMock,
+ $this->configLoaderMock,
+ $this->dataFactoryMock,
+ $this->storeManager,
+ $this->settingsChecker,
+ [],
+ $this->scopeResolverPool,
+ $this->scopeTypeNormalizer
);
}
public function testSaveDoesNotDoAnythingIfGroupsAreNotPassed()
{
- $this->_configLoaderMock->expects($this->never())->method('getConfigByPath');
- $this->_model->save();
+ $this->configLoaderMock->expects($this->never())->method('getConfigByPath');
+ $this->model->save();
}
public function testSaveEmptiesNonSetArguments()
{
- $this->_structureReaderMock->expects($this->never())->method('getConfiguration');
- $this->assertNull($this->_model->getSection());
- $this->assertNull($this->_model->getWebsite());
- $this->assertNull($this->_model->getStore());
- $this->_model->save();
- $this->assertSame('', $this->_model->getSection());
- $this->assertSame('', $this->_model->getWebsite());
- $this->assertSame('', $this->_model->getStore());
+ $this->structureReaderMock->expects($this->never())->method('getConfiguration');
+ $this->assertNull($this->model->getSection());
+ $this->assertNull($this->model->getWebsite());
+ $this->assertNull($this->model->getStore());
+ $this->model->save();
+ $this->assertSame('', $this->model->getSection());
+ $this->assertSame('', $this->model->getWebsite());
+ $this->assertSame('', $this->model->getStore());
}
public function testSaveToCheckAdminSystemConfigChangedSectionEvent()
{
- $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class);
+ $transactionMock = $this->createMock(Transaction::class);
- $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock));
+ $this->transFactoryMock->method('create')
+ ->willReturn($transactionMock);
- $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([]));
+ $this->configLoaderMock->method('getConfigByPath')
+ ->willReturn([]);
- $this->_eventManagerMock->expects(
+ $this->eventManagerMock->expects(
$this->at(0)
)->method(
'dispatch'
@@ -145,7 +190,7 @@ public function testSaveToCheckAdminSystemConfigChangedSectionEvent()
$this->arrayHasKey('website')
);
- $this->_eventManagerMock->expects(
+ $this->eventManagerMock->expects(
$this->at(0)
)->method(
'dispatch'
@@ -154,123 +199,147 @@ public function testSaveToCheckAdminSystemConfigChangedSectionEvent()
$this->arrayHasKey('store')
);
- $this->_model->setGroups(['1' => ['data']]);
- $this->_model->save();
+ $this->model->setGroups(['1' => ['data']]);
+ $this->model->save();
}
public function testDoNotSaveReadOnlyFields()
{
- $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class);
- $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock));
+ $transactionMock = $this->createMock(Transaction::class);
+ $this->transFactoryMock->method('create')
+ ->willReturn($transactionMock);
- $this->_settingsChecker->expects($this->any())->method('isReadOnly')->will($this->returnValue(true));
- $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([]));
+ $this->settingsChecker->method('isReadOnly')
+ ->willReturn(true);
+ $this->configLoaderMock->method('getConfigByPath')
+ ->willReturn([]);
- $this->_model->setGroups(['1' => ['fields' => ['key' => ['data']]]]);
- $this->_model->setSection('section');
+ $this->model->setGroups(['1' => ['fields' => ['key' => ['data']]]]);
+ $this->model->setSection('section');
- $group = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Group::class);
- $group->method('getPath')->willReturn('section/1');
+ $group = $this->createMock(Group::class);
+ $group->method('getPath')
+ ->willReturn('section/1');
- $field = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Field::class);
- $field->method('getGroupPath')->willReturn('section/1');
- $field->method('getId')->willReturn('key');
+ $field = $this->createMock(Field::class);
+ $field->method('getGroupPath')
+ ->willReturn('section/1');
+ $field->method('getId')
+ ->willReturn('key');
- $this->_configStructure->expects($this->at(0))
+ $this->configStructure->expects($this->at(0))
->method('getElement')
->with('section/1')
- ->will($this->returnValue($group));
- $this->_configStructure->expects($this->at(1))
+ ->willReturn($group);
+ $this->configStructure->expects($this->at(1))
->method('getElement')
->with('section/1')
- ->will($this->returnValue($group));
- $this->_configStructure->expects($this->at(2))
+ ->willReturn($group);
+ $this->configStructure->expects($this->at(2))
->method('getElement')
->with('section/1/key')
- ->will($this->returnValue($field));
+ ->willReturn($field);
$backendModel = $this->createPartialMock(
- \Magento\Framework\App\Config\Value::class,
+ Value::class,
['addData']
);
- $this->_dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel));
+ $this->dataFactoryMock->method('create')
+ ->willReturn($backendModel);
- $this->_transFactoryMock->expects($this->never())->method('addObject');
- $backendModel->expects($this->never())->method('addData');
+ $this->transFactoryMock->expects($this->never())
+ ->method('addObject');
+ $backendModel->expects($this->never())
+ ->method('addData');
- $this->_model->save();
+ $this->model->save();
}
public function testSaveToCheckScopeDataSet()
{
- $transactionMock = $this->createMock(\Magento\Framework\DB\Transaction::class);
- $this->_transFactoryMock->expects($this->any())->method('create')->will($this->returnValue($transactionMock));
+ $transactionMock = $this->createMock(Transaction::class);
+ $this->transFactoryMock->method('create')
+ ->willReturn($transactionMock);
- $this->_configLoaderMock->expects($this->any())->method('getConfigByPath')->will($this->returnValue([]));
+ $this->configLoaderMock->method('getConfigByPath')
+ ->willReturn([]);
- $this->_eventManagerMock->expects($this->at(0))
+ $this->eventManagerMock->expects($this->at(0))
->method('dispatch')
->with(
$this->equalTo('admin_system_config_changed_section_section'),
$this->arrayHasKey('website')
);
- $this->_eventManagerMock->expects($this->at(0))
+ $this->eventManagerMock->expects($this->at(0))
->method('dispatch')
->with(
$this->equalTo('admin_system_config_changed_section_section'),
$this->arrayHasKey('store')
);
- $group = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Group::class);
+ $group = $this->createMock(Group::class);
$group->method('getPath')->willReturn('section/1');
- $field = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Field::class);
+ $field = $this->createMock(Field::class);
$field->method('getGroupPath')->willReturn('section/1');
$field->method('getId')->willReturn('key');
- $this->_configStructure->expects($this->at(0))
+ $this->configStructure->expects($this->at(0))
->method('getElement')
->with('section/1')
- ->will($this->returnValue($group));
- $this->_configStructure->expects($this->at(1))
+ ->willReturn($group);
+ $this->configStructure->expects($this->at(1))
->method('getElement')
->with('section/1')
- ->will($this->returnValue($group));
- $this->_configStructure->expects($this->at(2))
+ ->willReturn($group);
+ $this->configStructure->expects($this->at(2))
->method('getElement')
->with('section/1/key')
- ->will($this->returnValue($field));
- $this->_configStructure->expects($this->at(3))
+ ->willReturn($field);
+ $this->configStructure->expects($this->at(3))
->method('getElement')
->with('section/1')
- ->will($this->returnValue($group));
- $this->_configStructure->expects($this->at(4))
+ ->willReturn($group);
+ $this->configStructure->expects($this->at(4))
->method('getElement')
->with('section/1/key')
- ->will($this->returnValue($field));
-
- $website = $this->createMock(\Magento\Store\Model\Website::class);
- $website->expects($this->any())->method('getCode')->will($this->returnValue('website_code'));
- $this->_storeManager->expects($this->any())->method('getWebsite')->will($this->returnValue($website));
- $this->_storeManager->expects($this->any())->method('getWebsites')->will($this->returnValue([$website]));
- $this->_storeManager->expects($this->any())->method('isSingleStoreMode')->will($this->returnValue(true));
-
- $this->_model->setWebsite('website');
- $this->_model->setSection('section');
- $this->_model->setGroups(['1' => ['fields' => ['key' => ['data']]]]);
+ ->willReturn($field);
+
+ $this->scopeResolver->method('getScope')
+ ->with('1')
+ ->willReturn($this->scope);
+ $this->scope->expects($this->atLeastOnce())
+ ->method('getScopeType')
+ ->willReturn('website');
+ $this->scope->expects($this->atLeastOnce())
+ ->method('getId')
+ ->willReturn(1);
+ $this->scope->expects($this->atLeastOnce())
+ ->method('getCode')
+ ->willReturn('website_code');
+ $this->scopeTypeNormalizer->expects($this->atLeastOnce())
+ ->method('normalize')
+ ->with('website')
+ ->willReturn('websites');
+ $website = $this->createMock(Website::class);
+ $this->storeManager->method('getWebsites')->willReturn([$website]);
+ $this->storeManager->method('isSingleStoreMode')->willReturn(true);
+
+ $this->model->setWebsite('1');
+ $this->model->setSection('section');
+ $this->model->setGroups(['1' => ['fields' => ['key' => ['data']]]]);
$backendModel = $this->createPartialMock(
- \Magento\Framework\App\Config\Value::class,
+ Value::class,
['setPath', 'addData', '__sleep', '__wakeup']
);
- $backendModel->expects($this->once())
- ->method('addData')
+ $backendModel->method('addData')
->with([
'field' => 'key',
'groups' => [1 => ['fields' => ['key' => ['data']]]],
'group_id' => null,
'scope' => 'websites',
- 'scope_id' => 0,
+ 'scope_id' => 1,
'scope_code' => 'website_code',
'field_config' => null,
'fieldset_data' => ['key' => null],
@@ -278,29 +347,68 @@ public function testSaveToCheckScopeDataSet()
$backendModel->expects($this->once())
->method('setPath')
->with('section/1/key')
- ->will($this->returnValue($backendModel));
+ ->willReturn($backendModel);
- $this->_dataFactoryMock->expects($this->any())->method('create')->will($this->returnValue($backendModel));
+ $this->dataFactoryMock->method('create')
+ ->willReturn($backendModel);
- $this->_model->save();
+ $this->model->save();
}
- public function testSetDataByPath()
+ /**
+ * @param string $path
+ * @param string $value
+ * @param string $section
+ * @param array $groups
+ * @return void
+ * @dataProvider setDataByPathDataProvider
+ */
+ public function testSetDataByPath(string $path, string $value, string $section, array $groups)
{
- $value = 'value';
- $path = '//';
- $this->_model->setDataByPath($path, $value);
- $expected = [
- 'section' => '',
- 'groups' => [
- '' => [
- 'fields' => [
- '' => ['value' => $value],
+ $this->model->setDataByPath($path, $value);
+ $this->assertEquals($section, $this->model->getData('section'));
+ $this->assertEquals($groups, $this->model->getData('groups'));
+ }
+
+ /**
+ * @return array
+ */
+ public function setDataByPathDataProvider(): array
+ {
+ return [
+ 'depth 3' => [
+ 'a/b/c',
+ 'value1',
+ 'a',
+ [
+ 'b' => [
+ 'fields' => [
+ 'c' => ['value' => 'value1'],
+ ],
+ ],
+ ],
+ ],
+ 'depth 5' => [
+ 'a/b/c/d/e',
+ 'value1',
+ 'a',
+ [
+ 'b' => [
+ 'groups' => [
+ 'c' => [
+ 'groups' => [
+ 'd' => [
+ 'fields' => [
+ 'e' => ['value' => 'value1'],
+ ],
+ ],
+ ],
+ ],
+ ],
],
],
],
];
- $this->assertSame($expected, $this->_model->getData());
}
/**
@@ -309,34 +417,32 @@ public function testSetDataByPath()
*/
public function testSetDataByPathEmpty()
{
- $this->_model->setDataByPath('', 'value');
+ $this->model->setDataByPath('', 'value');
}
/**
* @param string $path
- * @param string $expectedException
- *
+ * @return void
* @dataProvider setDataByPathWrongDepthDataProvider
*/
- public function testSetDataByPathWrongDepth($path, $expectedException)
+ public function testSetDataByPathWrongDepth(string $path)
{
- $expectedException = 'Allowed depth of configuration is 3 (//). ' . $expectedException;
- $this->expectException('\UnexpectedValueException');
+ $currentDepth = count(explode('/', $path));
+ $expectedException = 'Minimal depth of configuration is 3. Your configuration depth is ' . $currentDepth;
+ $this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessage($expectedException);
$value = 'value';
- $this->_model->setDataByPath($path, $value);
+ $this->model->setDataByPath($path, $value);
}
/**
* @return array
*/
- public function setDataByPathWrongDepthDataProvider()
+ public function setDataByPathWrongDepthDataProvider(): array
{
return [
- 'depth 2' => ['section/group', "Your configuration depth is 2 for path 'section/group'"],
- 'depth 1' => ['section', "Your configuration depth is 1 for path 'section'"],
- 'depth 4' => ['section/group/field/sub-field', "Your configuration depth is 4 for path"
- . " 'section/group/field/sub-field'", ],
+ 'depth 2' => ['section/group'],
+ 'depth 1' => ['section'],
];
}
}
diff --git a/app/code/Magento/Config/composer.json b/app/code/Magento/Config/composer.json
index 793d423280414..f36c29d387c9b 100644
--- a/app/code/Magento/Config/composer.json
+++ b/app/code/Magento/Config/composer.json
@@ -13,7 +13,7 @@
"magento/module-deploy": "100.2.*"
},
"type": "magento2-module",
- "version": "101.0.7",
+ "version": "101.0.8",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml
index a5dd18097fb47..87a0e666d2d7b 100644
--- a/app/code/Magento/Config/etc/di.xml
+++ b/app/code/Magento/Config/etc/di.xml
@@ -77,6 +77,11 @@
+
+
+ Magento\Framework\App\Cache\Type\Config
+
+
systemConfigSourceAggregatedProxy
@@ -85,6 +90,7 @@
Magento\Framework\App\Config\PreProcessorComposite
Magento\Framework\Serialize\Serializer\Serialize
Magento\Config\App\Config\Type\System\Reader\Proxy
+ Magento\Framework\Lock\Backend\Cache
diff --git a/app/code/Magento/Config/etc/module.xml b/app/code/Magento/Config/etc/module.xml
index cdf31ab7a5d19..b7df33554af90 100644
--- a/app/code/Magento/Config/etc/module.xml
+++ b/app/code/Magento/Config/etc/module.xml
@@ -6,7 +6,7 @@
*/
-->
-
+
diff --git a/app/code/Magento/ConfigurableImportExport/composer.json b/app/code/Magento/ConfigurableImportExport/composer.json
index b3d8af9f419d2..cf0e0e819a89c 100644
--- a/app/code/Magento/ConfigurableImportExport/composer.json
+++ b/app/code/Magento/ConfigurableImportExport/composer.json
@@ -12,7 +12,7 @@
"magento/module-store": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.4",
+ "version": "100.2.5",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php
index a80a15b59c2ce..efade5cd2c605 100644
--- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php
+++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php
@@ -15,6 +15,8 @@
use Magento\Framework\Pricing\PriceCurrencyInterface;
/**
+ * Configurable product view type.
+ *
* @api
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @api
@@ -277,6 +279,8 @@ protected function getOptionImages()
}
/**
+ * Collect price options.
+ *
* @return array
*/
protected function getOptionPrices()
@@ -315,6 +319,11 @@ protected function getOptionPrices()
),
],
'tierPrices' => $tierPrices,
+ 'msrpPrice' => [
+ 'amount' => $this->localeFormat->getNumber(
+ $product->getMsrp()
+ ),
+ ],
];
}
return $prices;
diff --git a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php
index 6f5f106a8bb24..45057a3591044 100644
--- a/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php
+++ b/app/code/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/CreateOptions.php
@@ -50,7 +50,11 @@ public function __construct(
*/
public function execute()
{
- $this->getResponse()->representJson($this->jsonHelper->jsonEncode($this->saveAttributeOptions()));
+ $result = [];
+ if ($this->getRequest()->isPost()) {
+ $result = $this->saveAttributeOptions();
+ }
+ $this->getResponse()->representJson($this->jsonHelper->jsonEncode($result));
}
/**
diff --git a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php
index 01981b5dae9db..fcbd0075b4cd0 100644
--- a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php
+++ b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php
@@ -1,6 +1,5 @@
productRepository->get($sku);
+ $product = $this->productRepository->get($sku, true);
$child = $this->productRepository->get($childSku);
$childrenIds = array_values($this->configurableType->getChildrenIds($product->getId())[0]);
@@ -144,7 +152,11 @@ public function addChild($sku, $childSku)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
+ * @throws InputException
+ * @throws NoSuchEntityException
+ * @throws StateException
+ * @throws \Magento\Framework\Exception\CouldNotSaveException
*/
public function removeChild($sku, $childSku)
{
diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
index f98075f2294cc..46f10608bc95e 100644
--- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
@@ -24,6 +24,7 @@
* @SuppressWarnings(PHPMD.TooManyFields)
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
* @api
* @since 100.0.2
*/
@@ -1385,7 +1386,7 @@ function ($item) {
*/
private function getUsedProductsCacheKey($keyParts)
{
- return md5(implode('_', $keyParts));
+ return sha1(implode('_', $keyParts));
}
/**
diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php
index bee334596e990..f2bf3116af9e4 100644
--- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php
+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php
@@ -7,14 +7,15 @@
*/
namespace Magento\ConfigurableProduct\Model\Product\Type\Configurable;
+use Magento\Catalog\Model\Product;
+
+/**
+ * Class Price for configurable product
+ */
class Price extends \Magento\Catalog\Model\Product\Type\Price
{
/**
- * Get product final price
- *
- * @param float $qty
- * @param \Magento\Catalog\Model\Product $product
- * @return float
+ * @inheritdoc
*/
public function getFinalPrice($qty, $product)
{
@@ -22,7 +23,10 @@ public function getFinalPrice($qty, $product)
return $product->getCalculatedFinalPrice();
}
if ($product->getCustomOption('simple_product') && $product->getCustomOption('simple_product')->getProduct()) {
- $finalPrice = parent::getFinalPrice($qty, $product->getCustomOption('simple_product')->getProduct());
+ /** @var Product $simpleProduct */
+ $simpleProduct = $product->getCustomOption('simple_product')->getProduct();
+ $simpleProduct->setCustomerGroupId($product->getCustomerGroupId());
+ $finalPrice = parent::getFinalPrice($qty, $simpleProduct);
} else {
$priceInfo = $product->getPriceInfo();
$finalPrice = $priceInfo->getPrice('final_price')->getAmount()->getValue();
@@ -35,7 +39,7 @@ public function getFinalPrice($qty, $product)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getPrice($product)
{
@@ -48,6 +52,7 @@ public function getPrice($product)
}
}
}
+
return 0;
}
}
diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php
new file mode 100644
index 0000000000000..8bdde2aeb0cff
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php
@@ -0,0 +1,44 @@
+getProduct()->getTypeId() === Configurable::TYPE_CODE && $item->getHasChildren()) {
+ $childItem = $item->getChildren()[0];
+ $result->getTaxClassKey()->setValue($childItem->getProduct()->getTaxClassId());
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php b/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php
index 611523a60b06d..816de36b16f96 100644
--- a/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php
+++ b/app/code/Magento/ConfigurableProduct/Pricing/Render/TierPriceBox.php
@@ -5,6 +5,8 @@
*/
namespace Magento\ConfigurableProduct\Pricing\Render;
+use Magento\Catalog\Pricing\Price\TierPrice;
+
/**
* Responsible for displaying tier price box on configurable product page.
*
@@ -17,9 +19,28 @@ class TierPriceBox extends FinalPriceBox
*/
public function toHtml()
{
- // Hide tier price block in case of MSRP.
- if (!$this->isMsrpPriceApplicable()) {
+ // Hide tier price block in case of MSRP or in case when no options with tier price.
+ if (!$this->isMsrpPriceApplicable() && $this->isTierPriceApplicable()) {
return parent::toHtml();
}
}
+
+ /**
+ * Check if at least one of simple products has tier price.
+ *
+ * @return bool
+ */
+ private function isTierPriceApplicable(): bool
+ {
+ $product = $this->getSaleableItem();
+ foreach ($product->getTypeInstance()->getUsedProducts($product) as $simpleProduct) {
+ if ($simpleProduct->isSalable()
+ && !empty($simpleProduct->getPriceInfo()->getPrice(TierPrice::PRICE_CODE)->getTierPriceList())
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/app/code/Magento/ConfigurableProduct/Setup/InstallData.php b/app/code/Magento/ConfigurableProduct/Setup/InstallData.php
index 1c26f159405dd..57cc287aa24aa 100644
--- a/app/code/Magento/ConfigurableProduct/Setup/InstallData.php
+++ b/app/code/Magento/ConfigurableProduct/Setup/InstallData.php
@@ -57,18 +57,24 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface
'color'
];
foreach ($attributes as $attributeCode) {
- $relatedProductTypes = explode(
- ',',
- $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode, 'apply_to')
- );
- if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) {
- $relatedProductTypes[] = Configurable::TYPE_CODE;
- $eavSetup->updateAttribute(
- \Magento\Catalog\Model\Product::ENTITY,
- $attributeCode,
- 'apply_to',
- implode(',', $relatedProductTypes)
+ if ($attribute = $eavSetup->getAttribute(
+ \Magento\Catalog\Model\Product::ENTITY,
+ $attributeCode,
+ 'apply_to'
+ )) {
+ $relatedProductTypes = explode(
+ ',',
+ $attribute
);
+ if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) {
+ $relatedProductTypes[] = Configurable::TYPE_CODE;
+ $eavSetup->updateAttribute(
+ \Magento\Catalog\Model\Product::ENTITY,
+ $attributeCode,
+ 'apply_to',
+ implode(',', $relatedProductTypes)
+ );
+ }
}
}
}
diff --git a/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php b/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php
index 1ff78a632c3bb..ca100cbf85bfc 100644
--- a/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php
+++ b/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php
@@ -47,16 +47,18 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface
if (version_compare($context->getVersion(), '2.2.0') < 0) {
$relatedProductTypes = $this->getRelatedProductTypes('tier_price', $eavSetup);
- $key = array_search(Configurable::TYPE_CODE, $relatedProductTypes);
- if ($key !== false) {
- unset($relatedProductTypes[$key]);
- $this->updateRelatedProductTypes('tier_price', $relatedProductTypes, $eavSetup);
+ if (!empty($relatedProductTypes)) {
+ $key = array_search(Configurable::TYPE_CODE, $relatedProductTypes);
+ if ($key !== false) {
+ unset($relatedProductTypes[$key]);
+ $this->updateRelatedProductTypes('tier_price', $relatedProductTypes, $eavSetup);
+ }
}
}
if (version_compare($context->getVersion(), '2.2.1') < 0) {
$relatedProductTypes = $this->getRelatedProductTypes('manufacturer', $eavSetup);
- if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) {
+ if (!empty($relatedProductTypes) && !in_array(Configurable::TYPE_CODE, $relatedProductTypes)) {
$relatedProductTypes[] = Configurable::TYPE_CODE;
$this->updateRelatedProductTypes('manufacturer', $relatedProductTypes, $eavSetup);
}
@@ -78,10 +80,17 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface
*/
private function getRelatedProductTypes(string $attributeId, EavSetup $eavSetup)
{
- return explode(
- ',',
- $eavSetup->getAttribute(Product::ENTITY, $attributeId, 'apply_to')
- );
+ if ($attribute = $eavSetup->getAttribute(
+ Product::ENTITY,
+ $attributeId,
+ 'apply_to'
+ )) {
+ return explode(
+ ',',
+ $attribute
+ );
+ }
+ return [];
}
/**
@@ -109,8 +118,10 @@ private function updateRelatedProductTypes(string $attributeId, array $relatedPr
*/
private function upgradeQuoteItemPrice(ModuleDataSetupInterface $setup)
{
- $connection = $setup->getConnection();
- $quoteItemTable = $setup->getTable('quote_item');
+ $connectionName = 'checkout';
+ $connection = $setup->getConnection($connectionName);
+ $quoteItemTable = $setup->getTable('quote_item', $connectionName);
+
$select = $connection->select();
$select->joinLeft(
['qi2' => $quoteItemTable],
@@ -121,10 +132,10 @@ private function upgradeQuoteItemPrice(ModuleDataSetupInterface $setup)
. ' AND qi1.parent_item_id IS NOT NULL'
. ' AND qi2.product_type = "' . Configurable::TYPE_CODE . '"'
);
- $updateQuoteItem = $setup->getConnection()->updateFromSelect(
+ $updateQuoteItem = $connection->updateFromSelect(
$select,
['qi1' => $quoteItemTable]
);
- $setup->getConnection()->query($updateQuoteItem);
+ $connection->query($updateQuoteItem);
}
}
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml
index d918649ed4914..abbef02adc520 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateApiConfigurableProductActionGroup.xml
@@ -10,7 +10,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
-
+
@@ -69,4 +69,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 50
+
+
+
+
+ 60
+
+
+
+
+ 70
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml
index 3b4e7f55d186c..6c47a24315c9a 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml
@@ -65,4 +65,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml
index 429d535952f76..7a722959c9996 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml
@@ -7,7 +7,7 @@
-->
+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
@@ -21,5 +21,12 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml
index 5ae70488d164d..c774dad1ed607 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml
@@ -7,7 +7,7 @@
-->
+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
index be0c2b05e48ba..4f2320666efbc 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
@@ -14,5 +14,6 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml
new file mode 100644
index 0000000000000..03f4d8461cebb
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckingProductQtyAfterOrderCancelTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckingProductQtyAfterOrderCancelTest.xml
new file mode 100644
index 0000000000000..af463c9042357
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckingProductQtyAfterOrderCancelTest.xml
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml
new file mode 100644
index 0000000000000..5daf699294155
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml
index 36615d3af6b7b..bddd71f565c33 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml
@@ -7,7 +7,7 @@
-->
+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
@@ -42,7 +42,9 @@
-
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php
index b45306d670bff..20b0905b7707b 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php
@@ -347,15 +347,15 @@ public function testGetJsonConfig()
}
/**
- * Retrieve array with expected parameters for method getJsonConfig()
+ * Retrieve array with expected parameters for method getJsonConfig().
*
- * @param $productId
- * @param $amount
- * @param $priceQty
- * @param $percentage
+ * @param int $productId
+ * @param float $amount
+ * @param int $priceQty
+ * @param int $percentage
* @return array
*/
- private function getExpectedArray($productId, $amount, $priceQty, $percentage)
+ private function getExpectedArray(int $productId, float $amount, int $priceQty, int $percentage): array
{
$expectedArray = [
'attributes' => [],
@@ -379,6 +379,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage)
'percentage' => $percentage,
],
],
+ 'msrpPrice' => [
+ 'amount' => null,
+ ],
],
],
'priceFormat' => [],
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php
index 64b9b3776442a..0fc650a4113c6 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php
@@ -6,22 +6,47 @@
namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Type\Configurable;
+use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\Product\Configuration\Item\Option;
+use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price as ConfigurablePrice;
+use Magento\Framework\Event\ManagerInterface;
+use Magento\Framework\Pricing\Amount\AmountInterface;
+use Magento\Framework\Pricing\Price\PriceInterface;
+use Magento\Framework\Pricing\PriceInfo\Base as PriceInfoBase;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
class PriceTest extends \PHPUnit\Framework\TestCase
{
- /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price */
+ /**
+ * @var ObjectManagerHelper
+ */
+ protected $objectManagerHelper;
+
+ /**
+ * @var ConfigurablePrice
+ */
protected $model;
- /** @var ObjectManagerHelper */
- protected $objectManagerHelper;
+ /**
+ * @var ManagerInterface|MockObject
+ */
+ private $eventManagerMock;
+ /**
+ * @inheritdoc
+ */
protected function setUp()
{
$this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->eventManagerMock = $this->createPartialMock(
+ ManagerInterface::class,
+ ['dispatch']
+ );
$this->model = $this->objectManagerHelper->getObject(
- \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price::class
+ ConfigurablePrice::class,
+ ['eventManager' => $this->eventManagerMock]
);
}
@@ -29,29 +54,29 @@ public function testGetFinalPrice()
{
$finalPrice = 10;
$qty = 1;
- $configurableProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice', '__wakeUp'])
- ->getMock();
- $customOption = $this->getMockBuilder(\Magento\Catalog\Model\Product\Configuration\Item\Option::class)
+
+ /** @var Product|MockObject $configurableProduct */
+ $configurableProduct = $this->getMockBuilder(Product::class)
->disableOriginalConstructor()
- ->setMethods(['getProduct'])
+ ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice'])
->getMock();
- $priceInfo = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo\Base::class)
+ /** @var PriceInfoBase|MockObject $priceInfo */
+ $priceInfo = $this->getMockBuilder(PriceInfoBase::class)
->disableOriginalConstructor()
->setMethods(['getPrice'])
->getMock();
- $price = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class)
+ /** @var PriceInterface|MockObject $price */
+ $price = $this->getMockBuilder(PriceInterface::class)
->disableOriginalConstructor()
->getMock();
- $amount = $this->getMockBuilder(\Magento\Framework\Pricing\Amount\AmountInterface::class)
+ /** @var AmountInterface|MockObject $amount */
+ $amount = $this->getMockBuilder(AmountInterface::class)
->disableOriginalConstructor()
->getMock();
$configurableProduct->expects($this->any())
->method('getCustomOption')
->willReturnMap([['simple_product', false], ['option_ids', false]]);
- $customOption->expects($this->never())->method('getProduct');
$configurableProduct->expects($this->once())->method('getPriceInfo')->willReturn($priceInfo);
$priceInfo->expects($this->once())->method('getPrice')->with('final_price')->willReturn($price);
$price->expects($this->once())->method('getAmount')->willReturn($amount);
@@ -60,4 +85,60 @@ public function testGetFinalPrice()
$this->assertEquals($finalPrice, $this->model->getFinalPrice($qty, $configurableProduct));
}
+
+ public function testGetFinalPriceWithSimpleProduct()
+ {
+ $finalPrice = 10;
+ $qty = 1;
+ $customerGroupId = 1;
+
+ /** @var Product|MockObject $configurableProduct */
+ $configurableProduct = $this->createPartialMock(
+ Product::class,
+ ['getCustomOption', 'setFinalPrice', 'getCustomerGroupId']
+ );
+ /** @var Option|MockObject $customOption */
+ $customOption = $this->createPartialMock(
+ Option::class,
+ ['getProduct']
+ );
+ /** @var Product|MockObject $simpleProduct */
+ $simpleProduct = $this->createPartialMock(
+ Product::class,
+ ['setCustomerGroupId', 'setFinalPrice', 'getPrice', 'getTierPrice', 'getData', 'getCustomOption']
+ );
+
+ $configurableProduct->method('getCustomOption')
+ ->willReturnMap([
+ ['simple_product', $customOption],
+ ['option_ids', false]
+ ]);
+ $configurableProduct->method('getCustomerGroupId')->willReturn($customerGroupId);
+ $configurableProduct->expects($this->atLeastOnce())
+ ->method('setFinalPrice')
+ ->with($finalPrice)
+ ->willReturnSelf();
+ $customOption->method('getProduct')->willReturn($simpleProduct);
+ $simpleProduct->expects($this->atLeastOnce())
+ ->method('setCustomerGroupId')
+ ->with($customerGroupId)
+ ->willReturnSelf();
+ $simpleProduct->method('getPrice')->willReturn($finalPrice);
+ $simpleProduct->method('getTierPrice')->with($qty)->willReturn($finalPrice);
+ $simpleProduct->expects($this->atLeastOnce())
+ ->method('setFinalPrice')
+ ->with($finalPrice)
+ ->willReturnSelf();
+ $simpleProduct->method('getData')->with('final_price')->willReturn($finalPrice);
+ $simpleProduct->method('getCustomOption')->with('option_ids')->willReturn(false);
+ $this->eventManagerMock->expects($this->once())
+ ->method('dispatch')
+ ->with('catalog_product_get_final_price', ['product' => $simpleProduct, 'qty' => $qty]);
+
+ $this->assertEquals(
+ $finalPrice,
+ $this->model->getFinalPrice($qty, $configurableProduct),
+ 'The final price calculation is wrong'
+ );
+ }
}
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php
index d1cf77f03a7bd..c351d12fa813d 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php
@@ -11,10 +11,13 @@
use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface;
use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
+use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable\AttributeFactory;
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection;
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\CollectionFactory;
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ProductCollection;
+use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory
+ as ProductCollectionFactory;
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\ConfigurableFactory;
use Magento\Customer\Model\Session;
use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend;
@@ -27,7 +30,6 @@
* @SuppressWarnings(PHPMD.LongVariable)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.TooManyFields)
- * @codingStandardsIgnoreFile
*/
class ConfigurableTest extends \PHPUnit\Framework\TestCase
{
@@ -154,8 +156,7 @@ protected function setUp()
->disableOriginalConstructor()
->setMethods(['create'])
->getMock();
- $this->productCollectionFactory = $this->getMockBuilder(
- \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory::class)
+ $this->productCollectionFactory = $this->getMockBuilder(ProductCollectionFactory::class)
->disableOriginalConstructor()
->setMethods(['create'])
->getMock();
@@ -281,8 +282,7 @@ public function testSave()
$product->expects($this->atLeastOnce())
->method('getData')
->willReturnMap($dataMap);
- $attribute = $this->getMockBuilder(
- \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class)
+ $attribute = $this->getMockBuilder(Attribute::class)
->disableOriginalConstructor()
->setMethods(['addData', 'setStoreId', 'setProductId', 'save', '__wakeup', '__sleep'])
->getMock();
@@ -464,8 +464,7 @@ public function testGetConfigurableAttributesAsArray($productStore)
$eavAttribute->expects($this->once())->method('getSource')->willReturn($attributeSource);
$eavAttribute->expects($this->atLeastOnce())->method('getStoreLabel')->willReturn('Store Label');
- $attribute = $this->getMockBuilder(
- \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class)
+ $attribute = $this->getMockBuilder(Attribute::class)
->disableOriginalConstructor()
->setMethods(['getProductAttribute', '__wakeup', '__sleep'])
->getMock();
@@ -524,7 +523,7 @@ public function testGetConfigurableAttributesNewProduct()
$this->assertEquals([], $this->model->getConfigurableAttributes($product));
}
- public function testGetConfigurableAttributes()
+ public function testGetConfigurableAttributes()
{
$configurableAttributes = '_cache_instance_configurable_attributes';
@@ -591,8 +590,7 @@ public function testHasOptionsConfigurableAttribute()
->setMethods(['__wakeup', 'getAttributeCode', 'getOptions', 'hasData', 'getData'])
->disableOriginalConstructor()
->getMock();
- $attributeMock = $this->getMockBuilder(
- \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class)
+ $attributeMock = $this->getMockBuilder(Attribute::class)
->disableOriginalConstructor()
->getMock();
@@ -698,7 +696,7 @@ function ($value) {
->disableOriginalConstructor()
->getMock();
$usedAttributeMock = $this->getMockBuilder(
- \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute::class
+ Attribute::class
)
->setMethods(['getProductAttribute'])
->disableOriginalConstructor()
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php
new file mode 100644
index 0000000000000..1a5c6c0003bfa
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php
@@ -0,0 +1,94 @@
+objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+ $this->commonTaxCollectorPlugin = $this->objectManager->getObject(CommonTaxCollectorPlugin::class);
+ }
+
+ /**
+ * Test to apply Tax Class Id from child item for configurable product
+ */
+ public function testAfterMapItem()
+ {
+ $childTaxClassId = 10;
+
+ /** @var Product|MockObject $childProductMock */
+ $childProductMock = $this->createPartialMock(
+ Product::class,
+ ['getTaxClassId']
+ );
+ $childProductMock->method('getTaxClassId')->willReturn($childTaxClassId);
+ /* @var AbstractItem|MockObject $quoteItemMock */
+ $childQuoteItemMock = $this->createMock(
+ AbstractItem::class
+ );
+ $childQuoteItemMock->method('getProduct')->willReturn($childProductMock);
+
+ /** @var Product|MockObject $productMock */
+ $productMock = $this->createPartialMock(
+ Product::class,
+ ['getTypeId']
+ );
+ $productMock->method('getTypeId')->willReturn(Configurable::TYPE_CODE);
+ /* @var AbstractItem|MockObject $quoteItemMock */
+ $quoteItemMock = $this->createPartialMock(
+ AbstractItem::class,
+ ['getProduct', 'getHasChildren', 'getChildren', 'getQuote', 'getAddress', 'getOptionByCode']
+ );
+ $quoteItemMock->method('getProduct')->willReturn($productMock);
+ $quoteItemMock->method('getHasChildren')->willReturn(true);
+ $quoteItemMock->method('getChildren')->willReturn([$childQuoteItemMock]);
+
+ /* @var TaxClassKeyInterface|MockObject $taxClassObjectMock */
+ $taxClassObjectMock = $this->createMock(TaxClassKeyInterface::class);
+ $taxClassObjectMock->expects($this->once())->method('setValue')->with($childTaxClassId);
+
+ /* @var QuoteDetailsItemInterface|MockObject $quoteDetailsItemMock */
+ $quoteDetailsItemMock = $this->createMock(QuoteDetailsItemInterface::class);
+ $quoteDetailsItemMock->method('getTaxClassKey')->willReturn($taxClassObjectMock);
+
+ $this->commonTaxCollectorPlugin->afterMapItem(
+ $this->createMock(CommonTaxCollector::class),
+ $quoteDetailsItemMock,
+ $this->createMock(QuoteDetailsItemInterfaceFactory::class),
+ $quoteItemMock
+ );
+ }
+}
diff --git a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php
index 9fd225e8acaab..44d0ca86d98d0 100644
--- a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php
+++ b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php
@@ -5,14 +5,14 @@
*/
namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier;
+use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Model\Product\Attribute\Backend\Sku;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
+use Magento\Framework\UrlInterface;
use Magento\Ui\Component\Container;
-use Magento\Ui\Component\Form;
use Magento\Ui\Component\DynamicRows;
+use Magento\Ui\Component\Form;
use Magento\Ui\Component\Modal;
-use Magento\Framework\UrlInterface;
-use Magento\Catalog\Model\Locator\LocatorInterface;
/**
* Data provider for Configurable panel
@@ -90,7 +90,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function modifyData(array $data)
{
@@ -98,7 +98,7 @@ public function modifyData(array $data)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
public function modifyMeta(array $meta)
@@ -197,7 +197,7 @@ public function modifyMeta(array $meta)
'autoRender' => false,
'componentType' => 'insertListing',
'component' => 'Magento_ConfigurableProduct/js'
- .'/components/associated-product-insert-listing',
+ . '/components/associated-product-insert-listing',
'dataScope' => $this->associatedListingPrefix
. static::ASSOCIATED_PRODUCT_LISTING,
'externalProvider' => $this->associatedListingPrefix
@@ -328,14 +328,12 @@ protected function getButtonSet()
'component' => 'Magento_Ui/js/form/components/button',
'actions' => [
[
- 'targetName' =>
- $this->dataScopeName . '.configurableModal',
+ 'targetName' => $this->dataScopeName . '.configurableModal',
'actionName' => 'trigger',
'params' => ['active', true],
],
[
- 'targetName' =>
- $this->dataScopeName . '.configurableModal',
+ 'targetName' => $this->dataScopeName . '.configurableModal',
'actionName' => 'openModal',
],
],
@@ -574,6 +572,7 @@ protected function getColumn(
'dataType' => Form\Element\DataType\Text::NAME,
'dataScope' => $name,
'visibleIfCanEdit' => false,
+ 'labelVisible' => false,
'imports' => [
'visible' => '!${$.provider}:${$.parentScope}.canEdit'
],
@@ -591,7 +590,9 @@ protected function getColumn(
'formElement' => Container::NAME,
'component' => 'Magento_Ui/js/form/components/group',
'label' => $label,
+ 'showLabel' => false,
'dataScope' => '',
+ 'showLabel' => false
];
$container['children'] = [
$name . '_edit' => $fieldEdit,
diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json
index 74e611af2cbbc..90b1ce26a920b 100644
--- a/app/code/Magento/ConfigurableProduct/composer.json
+++ b/app/code/Magento/ConfigurableProduct/composer.json
@@ -22,10 +22,11 @@
"magento/module-sales-rule": "101.0.*",
"magento/module-product-video": "100.2.*",
"magento/module-configurable-sample-data": "Sample Data version:100.2.*",
- "magento/module-product-links-sample-data": "Sample Data version:100.2.*"
+ "magento/module-product-links-sample-data": "Sample Data version:100.2.*",
+ "magento/module-tax": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.7",
+ "version": "100.2.8",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml
index e6e0da721e150..dd39bcb477699 100644
--- a/app/code/Magento/ConfigurableProduct/etc/di.xml
+++ b/app/code/Magento/ConfigurableProduct/etc/di.xml
@@ -245,4 +245,14 @@
+
+
+
+
+
+
+ - configurable
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml
index a8712cdc183de..78fa8b1c68b7a 100644
--- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml
+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml
@@ -20,9 +20,8 @@
-
getProductAttribute()
- ->getStoreLabel($_product->getStoreId());
+ =
+ $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel($_product->getStoreId()))
?>
'
+ ''; %>
- <%= $t('Buy %1 for %2 each and').replace('%1', item.qty).replace('%2', priceStr) %>
-
- <%= $t('save') %> <%= item.percentage %> %
-
+ <%= '= $block->escapeHtml(__('Buy %1 for %2 each and', '%1', '%2')) ?>'
+ .replace('%1', item.qty)
+ .replace('%2', priceStr) %>
+
+ = $block->escapeHtml(__('save')) ?> <%= item.percentage %> %
+
<% }); %>
diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js
index 6357bbd6c7c0c..ef40dcb9a7323 100644
--- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js
+++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js
@@ -373,10 +373,11 @@ define([
allowedProducts,
i,
j,
- basePrice = parseFloat(this.options.spConfig.prices.basePrice.amount),
+ finalPrice = parseFloat(this.options.spConfig.prices.finalPrice.amount),
optionFinalPrice,
optionPriceDiff,
- optionPrices = this.options.spConfig.optionPrices;
+ optionPrices = this.options.spConfig.optionPrices,
+ allowedProductMinPrice;
this._clearSelect(element);
element.options[0] = new Option('', '');
@@ -407,9 +408,9 @@ define([
if (typeof allowedProducts[0] !== 'undefined' &&
typeof optionPrices[allowedProducts[0]] !== 'undefined') {
-
- optionFinalPrice = parseFloat(optionPrices[allowedProducts[0]].finalPrice.amount);
- optionPriceDiff = optionFinalPrice - basePrice;
+ allowedProductMinPrice = this._getAllowedProductWithMinPrice(allowedProducts);
+ optionFinalPrice = parseFloat(optionPrices[allowedProductMinPrice].finalPrice.amount);
+ optionPriceDiff = optionFinalPrice - finalPrice;
if (optionPriceDiff !== 0) {
options[i].label = options[i].label + ' ' + priceUtils.formatPrice(
@@ -489,36 +490,27 @@ define([
_getPrices: function () {
var prices = {},
elements = _.toArray(this.options.settings),
- hasProductPrice = false,
- optionPriceDiff = 0,
- allowedProduct, optionPrices, basePrice, optionFinalPrice;
+ allowedProduct;
_.each(elements, function (element) {
var selected = element.options[element.selectedIndex],
config = selected && selected.config,
priceValue = {};
- if (config && config.allowedProducts.length === 1 && !hasProductPrice) {
- prices = {};
+ if (config && config.allowedProducts.length === 1) {
priceValue = this._calculatePrice(config);
- hasProductPrice = true;
} else if (element.value) {
allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts);
- optionPrices = this.options.spConfig.optionPrices;
- basePrice = parseFloat(this.options.spConfig.prices.basePrice.amount);
-
- if (!_.isEmpty(allowedProduct)) {
- optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount);
- optionPriceDiff = optionFinalPrice - basePrice;
- }
-
- if (optionPriceDiff !== 0) {
- prices = {};
- priceValue = this._calculatePriceDifference(allowedProduct);
- }
+ priceValue = this._calculatePrice({
+ 'allowedProducts': [
+ allowedProduct
+ ]
+ });
}
- prices[element.attributeId] = priceValue;
+ if (!_.isEmpty(priceValue)) {
+ prices.prices = priceValue;
+ }
}, this);
return prices;
@@ -539,40 +531,15 @@ define([
_.each(allowedProducts, function (allowedProduct) {
optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount);
- if (_.isEmpty(product)) {
+ if (_.isEmpty(product) || optionFinalPrice < optionMinPrice) {
optionMinPrice = optionFinalPrice;
product = allowedProduct;
}
-
- if (optionFinalPrice < optionMinPrice) {
- product = allowedProduct;
- }
}, this);
return product;
},
- /**
- * Calculate price difference for allowed product
- *
- * @param {*} allowedProduct - Product
- * @returns {*}
- * @private
- */
- _calculatePriceDifference: function (allowedProduct) {
- var displayPrices = $(this.options.priceHolderSelector).priceBox('option').prices,
- newPrices = this.options.spConfig.optionPrices[allowedProduct];
-
- _.each(displayPrices, function (price, code) {
-
- if (newPrices[code]) {
- displayPrices[code].amount = newPrices[code].amount - displayPrices[code].amount;
- }
- });
-
- return displayPrices;
- },
-
/**
* Returns prices for configured products
*
@@ -642,6 +609,13 @@ define([
} else {
$(this.options.slyOldPriceSelector).hide();
}
+
+ $(document).trigger('updateMsrpPriceBlock',
+ [
+ optionId,
+ this.options.spConfig.optionPrices
+ ]
+ );
},
/**
diff --git a/app/code/Magento/Contact/composer.json b/app/code/Magento/Contact/composer.json
index 72c8005c53432..bb95e20c1172a 100644
--- a/app/code/Magento/Contact/composer.json
+++ b/app/code/Magento/Contact/composer.json
@@ -10,7 +10,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.4",
+ "version": "100.2.5",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Contact/view/frontend/web/css/source/_module.less b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less
new file mode 100644
index 0000000000000..d79806eecbe9b
--- /dev/null
+++ b/app/code/Magento/Contact/view/frontend/web/css/source/_module.less
@@ -0,0 +1,52 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+*/
+
+& when (@media-common = true) {
+ .contact-index-index {
+ .column:not(.sidebar-main) {
+ .form.contact {
+ float: none;
+ width: 50%;
+ }
+ }
+
+ .column:not(.sidebar-additional) {
+ .form.contact {
+ float: none;
+ width: 50%;
+ }
+ }
+ }
+}
+
+//
+// Desktop
+// _____________________________________________
+
+.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) {
+ .contact-index-index .column:not(.sidebar-additional) .form.contact {
+ min-width: 600px;
+ }
+}
+
+// Mobile
+.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) {
+ .contact-index-index {
+ .column:not(.sidebar-main) {
+ .form.contact {
+ float: none;
+ width: 100%;
+ }
+ }
+
+ .column:not(.sidebar-additional) {
+ .form.contact {
+ float: none;
+ width: 100%;
+ }
+ }
+ }
+}
+
diff --git a/app/code/Magento/Cron/Model/Schedule.php b/app/code/Magento/Cron/Model/Schedule.php
index 39a58ef360cb3..a9ae04cb0c5d1 100644
--- a/app/code/Magento/Cron/Model/Schedule.php
+++ b/app/code/Magento/Cron/Model/Schedule.php
@@ -9,6 +9,7 @@
use Magento\Framework\Exception\CronException;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
+use Magento\Framework\Intl\DateTimeFactory;
/**
* Crontab schedule model
@@ -50,13 +51,19 @@ class Schedule extends \Magento\Framework\Model\AbstractModel
*/
private $timezoneConverter;
+ /**
+ * @var DateTimeFactory
+ */
+ private $dateTimeFactory;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
* @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
* @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
* @param array $data
- * @param TimezoneInterface $timezoneConverter
+ * @param TimezoneInterface|null $timezoneConverter
+ * @param DateTimeFactory|null $dateTimeFactory
*/
public function __construct(
\Magento\Framework\Model\Context $context,
@@ -64,10 +71,12 @@ public function __construct(
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
array $data = [],
- TimezoneInterface $timezoneConverter = null
+ TimezoneInterface $timezoneConverter = null,
+ DateTimeFactory $dateTimeFactory = null
) {
parent::__construct($context, $registry, $resource, $resourceCollection, $data);
$this->timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class);
+ $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class);
}
/**
@@ -109,17 +118,20 @@ public function trySchedule()
if (!$e || !$time) {
return false;
}
+ $configTimeZone = $this->timezoneConverter->getConfigTimezone();
+ $storeDateTime = $this->dateTimeFactory->create(null, new \DateTimeZone($configTimeZone));
if (!is_numeric($time)) {
//convert time from UTC to admin store timezone
//we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone
- $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i');
- $time = strtotime($time);
+ $dateTimeUtc = $this->dateTimeFactory->create($time);
+ $time = $dateTimeUtc->getTimestamp();
}
- $match = $this->matchCronExpression($e[0], strftime('%M', $time))
- && $this->matchCronExpression($e[1], strftime('%H', $time))
- && $this->matchCronExpression($e[2], strftime('%d', $time))
- && $this->matchCronExpression($e[3], strftime('%m', $time))
- && $this->matchCronExpression($e[4], strftime('%w', $time));
+ $time = $storeDateTime->setTimestamp($time);
+ $match = $this->matchCronExpression($e[0], $time->format('i'))
+ && $this->matchCronExpression($e[1], $time->format('H'))
+ && $this->matchCronExpression($e[2], $time->format('d'))
+ && $this->matchCronExpression($e[3], $time->format('m'))
+ && $this->matchCronExpression($e[4], $time->format('w'));
return $match;
}
diff --git a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php
index e9f4c61c7f551..76e9627ad7098 100644
--- a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php
+++ b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php
@@ -6,6 +6,9 @@
namespace Magento\Cron\Test\Unit\Model;
use Magento\Cron\Model\Schedule;
+use Magento\Framework\Intl\DateTimeFactory;
+use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
/**
* Class \Magento\Cron\Test\Unit\Model\ObserverTest
@@ -18,11 +21,27 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase
*/
protected $helper;
+ /**
+ * @var \Magento\Cron\Model\ResourceModel\Schedule
+ */
protected $resourceJobMock;
+ /**
+ * @var TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $timezoneConverter;
+
+ /**
+ * @var DateTimeFactory|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $dateTimeFactory;
+
+ /**
+ * @inheritdoc
+ */
protected function setUp()
{
- $this->helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+ $this->helper = new ObjectManager($this);
$this->resourceJobMock = $this->getMockBuilder(\Magento\Cron\Model\ResourceModel\Schedule::class)
->disableOriginalConstructor()
@@ -32,18 +51,30 @@ protected function setUp()
$this->resourceJobMock->expects($this->any())
->method('getIdFieldName')
->will($this->returnValue('id'));
+
+ $this->timezoneConverter = $this->getMockBuilder(TimezoneInterface::class)
+ ->setMethods(['date'])
+ ->getMockForAbstractClass();
+
+ $this->dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class)
+ ->setMethods(['create'])
+ ->getMock();
}
/**
+ * Test for SetCronExpr
+ *
* @param string $cronExpression
* @param array $expected
+ *
+ * @return void
* @dataProvider setCronExprDataProvider
*/
public function testSetCronExpr($cronExpression, $expected)
{
// 1. Create mocks
- /** @var \Magento\Cron\Model\Schedule $model */
- $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class);
+ /** @var Schedule $model */
+ $model = $this->helper->getObject(Schedule::class);
// 2. Run tested method
$model->setCronExpr($cronExpression);
@@ -61,7 +92,7 @@ public function testSetCronExpr($cronExpression, $expected)
*
* @return array
*/
- public function setCronExprDataProvider()
+ public function setCronExprDataProvider(): array
{
return [
['1 2 3 4 5', [1, 2, 3, 4, 5]],
@@ -121,27 +152,33 @@ public function setCronExprDataProvider()
}
/**
+ * Test for SetCronExprException
+ *
* @param string $cronExpression
+ *
+ * @return void
* @expectedException \Magento\Framework\Exception\CronException
* @dataProvider setCronExprExceptionDataProvider
*/
public function testSetCronExprException($cronExpression)
{
// 1. Create mocks
- /** @var \Magento\Cron\Model\Schedule $model */
- $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class);
+ /** @var Schedule $model */
+ $model = $this->helper->getObject(Schedule::class);
// 2. Run tested method
$model->setCronExpr($cronExpression);
}
/**
+ * Data provider
+ *
* Here is a list of allowed characters and values for Cron expression
* http://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm
*
* @return array
*/
- public function setCronExprExceptionDataProvider()
+ public function setCronExprExceptionDataProvider(): array
{
return [
[''],
@@ -153,17 +190,31 @@ public function setCronExprExceptionDataProvider()
}
/**
+ * Test for trySchedule
+ *
* @param int $scheduledAt
* @param array $cronExprArr
* @param $expected
+ *
+ * @return void
* @dataProvider tryScheduleDataProvider
*/
public function testTrySchedule($scheduledAt, $cronExprArr, $expected)
{
// 1. Create mocks
+ $this->timezoneConverter->method('getConfigTimezone')
+ ->willReturn('UTC');
+
+ $this->dateTimeFactory->method('create')
+ ->willReturn(new \DateTime());
+
/** @var \Magento\Cron\Model\Schedule $model */
$model = $this->helper->getObject(
- \Magento\Cron\Model\Schedule::class
+ \Magento\Cron\Model\Schedule::class,
+ [
+ 'timezoneConverter' => $this->timezoneConverter,
+ 'dateTimeFactory' => $this->dateTimeFactory,
+ ]
);
// 2. Set fixtures
@@ -177,22 +228,29 @@ public function testTrySchedule($scheduledAt, $cronExprArr, $expected)
$this->assertEquals($expected, $result);
}
+ /**
+ * Test for tryScheduleWithConversionToAdminStoreTime
+ *
+ * @return void
+ */
public function testTryScheduleWithConversionToAdminStoreTime()
{
$scheduledAt = '2011-12-13 14:15:16';
$cronExprArr = ['*', '*', '*', '*', '*'];
- // 1. Create mocks
- $timezoneConverter = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class);
- $timezoneConverter->expects($this->once())
- ->method('date')
- ->with($scheduledAt)
- ->willReturn(new \DateTime($scheduledAt));
+ $this->timezoneConverter->method('getConfigTimezone')
+ ->willReturn('UTC');
+
+ $this->dateTimeFactory->method('create')
+ ->willReturn(new \DateTime());
/** @var \Magento\Cron\Model\Schedule $model */
$model = $this->helper->getObject(
\Magento\Cron\Model\Schedule::class,
- ['timezoneConverter' => $timezoneConverter]
+ [
+ 'timezoneConverter' => $this->timezoneConverter,
+ 'dateTimeFactory' => $this->dateTimeFactory,
+ ]
);
// 2. Set fixtures
@@ -207,11 +265,15 @@ public function testTryScheduleWithConversionToAdminStoreTime()
}
/**
+ * Data provider
+ *
* @return array
*/
- public function tryScheduleDataProvider()
+ public function tryScheduleDataProvider(): array
{
$date = '2011-12-13 14:15:16';
+ $timestamp = (new \DateTime($date))->getTimestamp();
+ $day = 'Monday';
return [
[$date, [], false],
[$date, null, false],
@@ -219,19 +281,23 @@ public function tryScheduleDataProvider()
[$date, [], false],
[$date, null, false],
[$date, false, false],
- [strtotime($date), ['*', '*', '*', '*', '*'], true],
- [strtotime($date), ['15', '*', '*', '*', '*'], true],
- [strtotime($date), ['*', '14', '*', '*', '*'], true],
- [strtotime($date), ['*', '*', '13', '*', '*'], true],
- [strtotime($date), ['*', '*', '*', '12', '*'], true],
- [strtotime('Monday'), ['*', '*', '*', '*', '1'], true],
+ [$timestamp, ['*', '*', '*', '*', '*'], true],
+ [$timestamp, ['15', '*', '*', '*', '*'], true],
+ [$timestamp, ['*', '14', '*', '*', '*'], true],
+ [$timestamp, ['*', '*', '13', '*', '*'], true],
+ [$timestamp, ['*', '*', '*', '12', '*'], true],
+ [(new \DateTime($day))->getTimestamp(), ['*', '*', '*', '*', '1'], true],
];
}
/**
+ * Test for matchCronExpression
+ *
* @param string $cronExpressionPart
* @param int $dateTimePart
* @param bool $expectedResult
+ *
+ * @return void
* @dataProvider matchCronExpressionDataProvider
*/
public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $expectedResult)
@@ -248,9 +314,11 @@ public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $exp
}
/**
+ * Data provider
+ *
* @return array
*/
- public function matchCronExpressionDataProvider()
+ public function matchCronExpressionDataProvider(): array
{
return [
['*', 0, true],
@@ -287,7 +355,11 @@ public function matchCronExpressionDataProvider()
}
/**
+ * Test for matchCronExpressionException
+ *
* @param string $cronExpressionPart
+ *
+ * @return void
* @expectedException \Magento\Framework\Exception\CronException
* @dataProvider matchCronExpressionExceptionDataProvider
*/
@@ -304,9 +376,11 @@ public function testMatchCronExpressionException($cronExpressionPart)
}
/**
+ * Data provider
+ *
* @return array
*/
- public function matchCronExpressionExceptionDataProvider()
+ public function matchCronExpressionExceptionDataProvider(): array
{
return [
['1/2/3'], //Invalid cron expression, expecting 'match/modulus': 1/2/3
@@ -317,8 +391,12 @@ public function matchCronExpressionExceptionDataProvider()
}
/**
+ * Test for GetNumeric
+ *
* @param mixed $param
* @param int $expectedResult
+ *
+ * @return void
* @dataProvider getNumericDataProvider
*/
public function testGetNumeric($param, $expectedResult)
@@ -335,9 +413,11 @@ public function testGetNumeric($param, $expectedResult)
}
/**
+ * Data provider
+ *
* @return array
*/
- public function getNumericDataProvider()
+ public function getNumericDataProvider(): array
{
return [
[null, false],
@@ -362,6 +442,11 @@ public function getNumericDataProvider()
];
}
+ /**
+ * Test for tryLockJobSuccess
+ *
+ * @return void
+ */
public function testTryLockJobSuccess()
{
$scheduleId = 1;
@@ -386,6 +471,11 @@ public function testTryLockJobSuccess()
$this->assertEquals(Schedule::STATUS_RUNNING, $model->getStatus());
}
+ /**
+ * Test for tryLockJobFailure
+ *
+ * @return void
+ */
public function testTryLockJobFailure()
{
$scheduleId = 1;
diff --git a/app/code/Magento/Cron/composer.json b/app/code/Magento/Cron/composer.json
index ddbe7df2c0a2e..a1f1e107a3c57 100644
--- a/app/code/Magento/Cron/composer.json
+++ b/app/code/Magento/Cron/composer.json
@@ -10,7 +10,7 @@
"magento/module-config": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.5",
+ "version": "100.2.6",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php
index ae13c4d399e47..3ab1bfc086721 100644
--- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php
+++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currency/SaveRates.php
@@ -7,15 +7,22 @@
namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currency;
+use Magento\Framework\Exception\NotFoundException;
+
class SaveRates extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currency
{
/**
* Save rates action
*
* @return void
+ * @throws NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found'));
+ }
+
$data = $this->getRequest()->getParam('rate');
if (is_array($data)) {
try {
diff --git a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php
index eee7961b02f4a..ad80833d8da5d 100644
--- a/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php
+++ b/app/code/Magento/CurrencySymbol/Controller/Adminhtml/System/Currencysymbol/Save.php
@@ -6,15 +6,22 @@
*/
namespace Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol;
+use Magento\Framework\Exception\NotFoundException;
+
class Save extends \Magento\CurrencySymbol\Controller\Adminhtml\System\Currencysymbol
{
/**
* Save custom Currency symbol
*
* @return void
+ * @throws NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found'));
+ }
+
$symbolsDataArray = $this->getRequest()->getParam('custom_currency_symbol', null);
if (is_array($symbolsDataArray)) {
foreach ($symbolsDataArray as &$symbolsData) {
@@ -27,9 +34,9 @@ public function execute()
try {
$this->_objectManager->create(\Magento\CurrencySymbol\Model\System\Currencysymbol::class)
->setCurrencySymbolsData($symbolsDataArray);
- $this->messageManager->addSuccess(__('You applied the custom currency symbols.'));
+ $this->messageManager->addSuccessMessage(__('You applied the custom currency symbols.'));
} catch (\Exception $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
}
$this->getResponse()->setRedirect($this->_redirect->getRedirectUrl($this->getUrl('*')));
diff --git a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php
index 0863104a2bf8d..455449a449103 100644
--- a/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php
+++ b/app/code/Magento/CurrencySymbol/Test/Unit/Controller/Adminhtml/System/Currencysymbol/SaveTest.php
@@ -57,11 +57,18 @@ class SaveTest extends \PHPUnit\Framework\TestCase
*/
protected $filterManagerMock;
+ /**
+ * @inheritdoc
+ */
protected function setUp()
{
$objectManager = new ObjectManager($this);
- $this->requestMock = $this->createMock(\Magento\Framework\App\RequestInterface::class);
+ $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['isPost'])
+ ->getMockForAbstractClass();
+ $this->requestMock->expects($this->any())->method('isPost')->willReturn(true);
$this->helperMock = $this->createMock(\Magento\Backend\Helper\Data::class);
@@ -128,7 +135,7 @@ public function testExecute()
->willReturn($this->filterManagerMock);
$this->messageManagerMock->expects($this->once())
- ->method('addSuccess')
+ ->method('addSuccessMessage')
->with(__('You applied the custom currency symbols.'));
$this->action->execute();
diff --git a/app/code/Magento/CurrencySymbol/composer.json b/app/code/Magento/CurrencySymbol/composer.json
index 021fe7ae9bc56..378b0df398ef5 100644
--- a/app/code/Magento/CurrencySymbol/composer.json
+++ b/app/code/Magento/CurrencySymbol/composer.json
@@ -11,7 +11,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.3",
+ "version": "100.2.4",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit.php b/app/code/Magento/Customer/Block/Adminhtml/Edit.php
index 973016baba29c..701e38bea6b58 100644
--- a/app/code/Magento/Customer/Block/Adminhtml/Edit.php
+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit.php
@@ -122,7 +122,7 @@ protected function _construct()
[
'label' => __('Force Sign-In'),
'onclick' => 'deleteConfirm(\'' . $this->escapeJs($this->escapeHtml($deleteConfirmMsg)) .
- '\', \'' . $url . '\')',
+ '\', \'' . $url . '\', {data: {}})',
'class' => 'invalidate-token'
],
10
diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php
index 180cb3d66ea35..506ba3fb9bfda 100644
--- a/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php
+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/InvalidateTokenButton.php
@@ -25,7 +25,8 @@ public function getButtonData()
$data = [
'label' => __('Force Sign-In'),
'class' => 'invalidate-token',
- 'on_click' => 'deleteConfirm("' . $deleteConfirmMsg . '", "' . $this->getInvalidateTokenUrl() . '")',
+ 'on_click' => 'deleteConfirm("' . $deleteConfirmMsg . '", "' . $this->getInvalidateTokenUrl() .
+ '", {data: {}})',
'sort_order' => 65,
];
}
diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php
index 9a025211c9b0a..0aeed1562c51e 100644
--- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php
+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php
@@ -48,7 +48,7 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele
$regionId = $element->getForm()->getElement('region_id')->getValue();
- $html = '
';
+ $html = '
';
$element->setClass('input-text admin__control-text');
$element->setRequired(true);
$html .= $element->getLabelHtml() . '
';
diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php
index bb190260e4776..c1266febff99d 100644
--- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php
+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php
@@ -63,7 +63,8 @@ protected function _construct()
{
parent::_construct();
$this->setId('customer_orders_grid');
- $this->setDefaultSort('created_at', 'desc');
+ $this->setDefaultSort('created_at');
+ $this->setDefaultDir('desc');
$this->setUseAjax(true);
}
diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php
index 3f2c7cda7608d..988a157805b36 100644
--- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php
+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php
@@ -77,7 +77,8 @@ protected function _construct()
{
parent::_construct();
$this->setId('customer_view_cart_grid');
- $this->setDefaultSort('added_at', 'desc');
+ $this->setDefaultSort('added_at');
+ $this->setDefaultDir('desc');
$this->setSortable(false);
$this->setPagerVisibility(false);
$this->setFilterVisibility(false);
@@ -94,7 +95,7 @@ protected function _prepareCollection()
$quote = $this->getQuote();
if ($quote) {
- $collection = $quote->getItemsCollection(false);
+ $collection = $quote->getItemsCollection(true);
} else {
$collection = $this->_dataCollectionFactory->create();
}
diff --git a/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php
new file mode 100644
index 0000000000000..280948439e1f8
--- /dev/null
+++ b/app/code/Magento/Customer/Block/DataProviders/PostCodesPatternsAttributeData.php
@@ -0,0 +1,50 @@
+postCodeConfig = $postCodeConfig;
+ $this->serializer = $serializer;
+ }
+
+ /**
+ * Get serialized post codes
+ *
+ * @return string
+ */
+ public function getSerializedPostCodes(): string
+ {
+ return $this->serializer->serialize($this->postCodeConfig->getPostCodes());
+ }
+}
diff --git a/app/code/Magento/Customer/Block/Form/Login.php b/app/code/Magento/Customer/Block/Form/Login.php
index 7b265ae1f0f32..d3d3306a49b44 100644
--- a/app/code/Magento/Customer/Block/Form/Login.php
+++ b/app/code/Magento/Customer/Block/Form/Login.php
@@ -47,15 +47,6 @@ public function __construct(
$this->_customerSession = $customerSession;
}
- /**
- * @return $this
- */
- protected function _prepareLayout()
- {
- $this->pageConfig->getTitle()->set(__('Customer Login'));
- return parent::_prepareLayout();
- }
-
/**
* Retrieve form posting url
*
diff --git a/app/code/Magento/Customer/Block/Widget/Name.php b/app/code/Magento/Customer/Block/Widget/Name.php
index 35f3bbefb8f00..2576545601c73 100644
--- a/app/code/Magento/Customer/Block/Widget/Name.php
+++ b/app/code/Magento/Customer/Block/Widget/Name.php
@@ -245,10 +245,14 @@ public function getStoreLabel($attributeCode)
*/
public function getAttributeValidationClass($attributeCode)
{
- return $this->_addressHelper->getAttributeValidationClass($attributeCode);
+ $attributeMetadata = $this->_getAttribute($attributeCode);
+
+ return $attributeMetadata ? $attributeMetadata->getFrontendClass() : '';
}
/**
+ * Check if attribute is required
+ *
* @param string $attributeCode
* @return bool
*/
@@ -259,6 +263,8 @@ private function _isAttributeRequired($attributeCode)
}
/**
+ * Check if attribute is visible
+ *
* @param string $attributeCode
* @return bool
*/
diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php
index da0ad29c5c72f..4d9ec962c292d 100644
--- a/app/code/Magento/Customer/Controller/Account/EditPost.php
+++ b/app/code/Magento/Customer/Controller/Account/EditPost.php
@@ -6,6 +6,8 @@
*/
namespace Magento\Customer\Controller\Account;
+use Magento\Customer\Api\Data\CustomerInterface;
+use Magento\Customer\Model\AddressRegistry;
use Magento\Customer\Model\AuthenticationInterface;
use Magento\Customer\Model\Customer\Mapper;
use Magento\Customer\Model\EmailNotificationInterface;
@@ -23,7 +25,8 @@
use Magento\Framework\Escaper;
/**
- * Class EditPost
+ * Class to editing post.
+ *
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class EditPost extends \Magento\Customer\Controller\AbstractAccount
@@ -73,9 +76,16 @@ class EditPost extends \Magento\Customer\Controller\AbstractAccount
*/
private $customerMapper;
- /** @var Escaper */
+ /**
+ * @var Escaper
+ */
private $escaper;
+ /**
+ * @var AddressRegistry
+ */
+ private $addressRegistry;
+
/**
* @param Context $context
* @param Session $customerSession
@@ -84,6 +94,7 @@ class EditPost extends \Magento\Customer\Controller\AbstractAccount
* @param Validator $formKeyValidator
* @param CustomerExtractor $customerExtractor
* @param Escaper|null $escaper
+ * @param AddressRegistry|null $addressRegistry
*/
public function __construct(
Context $context,
@@ -92,7 +103,8 @@ public function __construct(
CustomerRepositoryInterface $customerRepository,
Validator $formKeyValidator,
CustomerExtractor $customerExtractor,
- Escaper $escaper = null
+ Escaper $escaper = null,
+ AddressRegistry $addressRegistry = null
) {
parent::__construct($context);
$this->session = $customerSession;
@@ -101,6 +113,7 @@ public function __construct(
$this->formKeyValidator = $formKeyValidator;
$this->customerExtractor = $customerExtractor;
$this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class);
+ $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class);
}
/**
@@ -138,7 +151,7 @@ private function getEmailNotification()
}
/**
- * Change customer email or password action
+ * Change customer email or password action.
*
* @return \Magento\Framework\Controller\Result\Redirect
*/
@@ -162,6 +175,9 @@ public function execute()
// whether a customer enabled change password option
$isPasswordChanged = $this->changeCustomerPassword($currentCustomerDataObject->getEmail());
+ // No need to validate customer address while editing customer profile
+ $this->disableAddressValidation($customerCandidateDataObject);
+
$this->customerRepository->save($customerCandidateDataObject);
$this->getEmailNotification()->credentialsChanged(
$customerCandidateDataObject,
@@ -170,6 +186,7 @@ public function execute()
);
$this->dispatchSuccessEvent($customerCandidateDataObject);
$this->messageManager->addSuccess(__('You saved the account information.'));
+
return $resultRedirect->setPath('customer/account');
} catch (InvalidEmailOrPasswordException $e) {
$this->messageManager->addError($e->getMessage());
@@ -180,6 +197,7 @@ public function execute()
$this->session->logout();
$this->session->start();
$this->messageManager->addError($message);
+
return $resultRedirect->setPath('customer/account/login');
} catch (InputException $e) {
$this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage()));
@@ -313,4 +331,18 @@ private function getCustomerMapper()
}
return $this->customerMapper;
}
+
+ /**
+ * Disable Customer Address Validation.
+ *
+ * @param CustomerInterface $customer
+ * @return void
+ */
+ private function disableAddressValidation(CustomerInterface $customer)
+ {
+ foreach ($customer->getAddresses() as $address) {
+ $addressModel = $this->addressRegistry->retrieve($address->getId());
+ $addressModel->setShouldIgnoreValidation(true);
+ }
+ }
}
diff --git a/app/code/Magento/Customer/Controller/Address/Delete.php b/app/code/Magento/Customer/Controller/Address/Delete.php
index ef92bd2ef533b..d287808b4056d 100644
--- a/app/code/Magento/Customer/Controller/Address/Delete.php
+++ b/app/code/Magento/Customer/Controller/Address/Delete.php
@@ -6,13 +6,20 @@
*/
namespace Magento\Customer\Controller\Address;
+use Magento\Framework\Exception\NotFoundException;
+
class Delete extends \Magento\Customer\Controller\Address
{
/**
* @return \Magento\Framework\Controller\Result\Redirect
+ * @throws NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found'));
+ }
+
$addressId = $this->getRequest()->getParam('id', false);
if ($addressId && $this->_formKeyValidator->validate($this->getRequest())) {
@@ -20,12 +27,12 @@ public function execute()
$address = $this->_addressRepository->getById($addressId);
if ($address->getCustomerId() === $this->_getSession()->getCustomerId()) {
$this->_addressRepository->deleteById($addressId);
- $this->messageManager->addSuccess(__('You deleted the address.'));
+ $this->messageManager->addSuccessMessage(__('You deleted the address.'));
} else {
- $this->messageManager->addError(__('We can\'t delete the address right now.'));
+ $this->messageManager->addErrorMessage(__('We can\'t delete the address right now.'));
}
} catch (\Exception $other) {
- $this->messageManager->addException($other, __('We can\'t delete the address right now.'));
+ $this->messageManager->addExceptionMessage($other, __('We can\'t delete the address right now.'));
}
}
return $this->resultRedirectFactory->create()->setPath('*/*/index');
diff --git a/app/code/Magento/Customer/Controller/Address/Index.php b/app/code/Magento/Customer/Controller/Address/Index.php
index ad04c7bd5c71b..674d3bcf0e0d3 100644
--- a/app/code/Magento/Customer/Controller/Address/Index.php
+++ b/app/code/Magento/Customer/Controller/Address/Index.php
@@ -28,9 +28,9 @@ class Index extends \Magento\Customer\Controller\Address
* @param \Magento\Customer\Api\Data\RegionInterfaceFactory $regionDataFactory
* @param \Magento\Framework\Reflection\DataObjectProcessor $dataProcessor
* @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper
- * @param CustomerRepositoryInterface $customerRepository
* @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory
* @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
+ * @param CustomerRepositoryInterface $customerRepository
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php
index 936d9cdbc1704..6b35397d9be13 100644
--- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php
+++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php
@@ -9,6 +9,7 @@
use Magento\Customer\Api\Data\GroupInterfaceFactory;
use Magento\Customer\Api\Data\GroupInterface;
use Magento\Customer\Api\GroupRepositoryInterface;
+use Magento\Framework\Exception\NotFoundException;
class Save extends \Magento\Customer\Controller\Adminhtml\Group
{
@@ -66,9 +67,14 @@ protected function storeCustomerGroupDataToSession($customerGroupData)
* Create or save customer group.
*
* @return \Magento\Backend\Model\View\Result\Redirect|\Magento\Backend\Model\View\Result\Forward
+ * @throws NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found'));
+ }
+
$taxClass = (int)$this->getRequest()->getParam('tax_class');
/** @var \Magento\Customer\Api\Data\GroupInterface $customerGroup */
diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php
index 6753a48d02d6a..4d1bc18a98a06 100644
--- a/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php
+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/InlineEdit.php
@@ -8,12 +8,14 @@
use Magento\Backend\App\Action;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Api\Data\CustomerInterface;
+use Magento\Customer\Model\AddressRegistry;
use Magento\Customer\Model\EmailNotificationInterface;
use Magento\Customer\Ui\Component\Listing\AttributeRepository;
use Magento\Framework\Message\MessageInterface;
+use Magento\Framework\App\ObjectManager;
/**
- * Customer inline edit action
+ * Customer inline edit action.
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
@@ -61,6 +63,11 @@ class InlineEdit extends \Magento\Backend\App\Action
*/
private $emailNotification;
+ /**
+ * @var AddressRegistry
+ */
+ private $addressRegistry;
+
/**
* @param Action\Context $context
* @param CustomerRepositoryInterface $customerRepository
@@ -68,6 +75,7 @@ class InlineEdit extends \Magento\Backend\App\Action
* @param \Magento\Customer\Model\Customer\Mapper $customerMapper
* @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper
* @param \Psr\Log\LoggerInterface $logger
+ * @param AddressRegistry|null $addressRegistry
*/
public function __construct(
Action\Context $context,
@@ -75,13 +83,15 @@ public function __construct(
\Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory,
\Magento\Customer\Model\Customer\Mapper $customerMapper,
\Magento\Framework\Api\DataObjectHelper $dataObjectHelper,
- \Psr\Log\LoggerInterface $logger
+ \Psr\Log\LoggerInterface $logger,
+ AddressRegistry $addressRegistry = null
) {
$this->customerRepository = $customerRepository;
$this->resultJsonFactory = $resultJsonFactory;
$this->customerMapper = $customerMapper;
$this->dataObjectHelper = $dataObjectHelper;
$this->logger = $logger;
+ $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class);
parent::__construct($context);
}
@@ -106,8 +116,6 @@ private function getEmailNotification()
* Inline edit action execute
*
* @return \Magento\Framework\Controller\Result\Json
- * @throws \Magento\Framework\Exception\LocalizedException
- * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function execute()
{
@@ -115,7 +123,7 @@ public function execute()
$resultJson = $this->resultJsonFactory->create();
$postItems = $this->getRequest()->getParam('items', []);
- if (!($this->getRequest()->getParam('isAjax') && count($postItems))) {
+ if (!($this->getRequest()->getParam('isAjax') && $this->getRequest()->isPost() && count($postItems))) {
return $resultJson->setData([
'messages' => [__('Please correct the data sent.')],
'error' => true,
@@ -210,7 +218,7 @@ protected function updateDefaultBilling(array $data)
}
/**
- * Save customer with error catching
+ * Save customer with error catching.
*
* @param CustomerInterface $customer
* @return void
@@ -218,6 +226,8 @@ protected function updateDefaultBilling(array $data)
protected function saveCustomer(CustomerInterface $customer)
{
try {
+ // No need to validate customer address during inline edit action
+ $this->disableAddressValidation($customer);
$this->customerRepository->save($customer);
} catch (\Magento\Framework\Exception\InputException $e) {
$this->getMessageManager()->addError($this->getErrorWithCustomerId($e->getMessage()));
@@ -303,4 +313,18 @@ protected function getErrorWithCustomerId($errorText)
{
return '[Customer ID: ' . $this->getCustomer()->getId() . '] ' . __($errorText);
}
+
+ /**
+ * Disable Customer Address Validation.
+ *
+ * @param CustomerInterface $customer
+ * @return void
+ */
+ private function disableAddressValidation(CustomerInterface $customer)
+ {
+ foreach ($customer->getAddresses() as $address) {
+ $addressModel = $this->addressRegistry->retrieve($address->getId());
+ $addressModel->setShouldIgnoreValidation(true);
+ }
+ }
}
diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php
index 762b872b97b6d..49a51052beb90 100644
--- a/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php
+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/MassAssignGroup.php
@@ -5,6 +5,7 @@
*/
namespace Magento\Customer\Controller\Adminhtml\Index;
+use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Backend\App\Action\Context;
use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory;
use Magento\Eav\Model\Entity\Collection\AbstractCollection;
@@ -13,7 +14,7 @@
use Magento\Framework\Controller\ResultFactory;
/**
- * Class MassAssignGroup
+ * Class to execute MassAssignGroup action.
*/
class MassAssignGroup extends AbstractMassAction
{
@@ -39,7 +40,7 @@ public function __construct(
}
/**
- * Customer mass assign group action
+ * Customer mass assign group action.
*
* @param AbstractCollection $collection
* @return \Magento\Backend\Model\View\Result\Redirect
@@ -51,6 +52,8 @@ protected function massAction(AbstractCollection $collection)
// Verify customer exists
$customer = $this->customerRepository->getById($customerId);
$customer->setGroupId($this->getRequest()->getParam('group'));
+ // No need to validate customer and customer address during assigning customer to the group
+ $this->setIgnoreValidationFlag($customer);
$this->customerRepository->save($customer);
$customersUpdated++;
}
@@ -64,4 +67,15 @@ protected function massAction(AbstractCollection $collection)
return $resultRedirect;
}
+
+ /**
+ * Set ignore_validation_flag to skip unnecessary address and customer validation.
+ *
+ * @param CustomerInterface $customer
+ * @return void
+ */
+ private function setIgnoreValidationFlag(CustomerInterface $customer)
+ {
+ $customer->setData('ignore_validation_flag', true);
+ }
}
diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php
index 37c8ed5a252f8..eab18520e69a7 100644
--- a/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php
+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/ResetPassword.php
@@ -31,7 +31,9 @@ public function execute()
\Magento\Customer\Model\AccountManagement::EMAIL_REMINDER,
$customer->getWebsiteId()
);
- $this->messageManager->addSuccess(__('The customer will receive an email with a link to reset password.'));
+ $this->messageManager->addSuccessMessage(
+ __('The customer will receive an email with a link to reset password.')
+ );
} catch (NoSuchEntityException $exception) {
$resultRedirect->setPath('customer/index');
return $resultRedirect;
@@ -44,7 +46,7 @@ public function execute()
} catch (SecurityViolationException $exception) {
$this->messageManager->addErrorMessage($exception->getMessage());
} catch (\Exception $exception) {
- $this->messageManager->addException(
+ $this->messageManager->addExceptionMessage(
$exception,
__('Something went wrong while resetting customer password.')
);
diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php
index 12732f81f78a0..561039990f705 100644
--- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php
+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php
@@ -5,6 +5,15 @@
*/
namespace Magento\Customer\Controller\Adminhtml\Index;
+use Magento\Customer\Api\AccountManagementInterface;
+use Magento\Customer\Api\AddressRepositoryInterface;
+use Magento\Customer\Api\CustomerRepositoryInterface;
+use Magento\Customer\Model\Address\Mapper;
+use Magento\Customer\Model\AddressRegistry;
+use Magento\Framework\Api\DataObjectHelper;
+use Magento\Customer\Api\Data\AddressInterfaceFactory;
+use Magento\Customer\Api\Data\CustomerInterfaceFactory;
+use Magento\Framework\DataObjectFactory as ObjectFactory;
use Magento\Customer\Api\AddressMetadataInterface;
use Magento\Customer\Api\CustomerMetadataInterface;
use Magento\Customer\Api\Data\CustomerInterface;
@@ -12,8 +21,11 @@
use Magento\Customer\Model\EmailNotificationInterface;
use Magento\Customer\Model\Metadata\Form;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\App\ObjectManager;
/**
+ * Class to Save customer.
+ *
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Save extends \Magento\Customer\Controller\Adminhtml\Index
@@ -23,6 +35,98 @@ class Save extends \Magento\Customer\Controller\Adminhtml\Index
*/
private $emailNotification;
+ /**
+ * @var AddressRegistry
+ */
+ private $addressRegistry;
+
+ /**
+ * @param \Magento\Backend\App\Action\Context $context
+ * @param \Magento\Framework\Registry $coreRegistry
+ * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory
+ * @param \Magento\Customer\Model\CustomerFactory $customerFactory
+ * @param \Magento\Customer\Model\AddressFactory $addressFactory
+ * @param \Magento\Customer\Model\Metadata\FormFactory $formFactory
+ * @param \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory
+ * @param \Magento\Customer\Helper\View $viewHelper
+ * @param \Magento\Framework\Math\Random $random
+ * @param CustomerRepositoryInterface $customerRepository
+ * @param \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter
+ * @param Mapper $addressMapper
+ * @param AccountManagementInterface $customerAccountManagement
+ * @param AddressRepositoryInterface $addressRepository
+ * @param CustomerInterfaceFactory $customerDataFactory
+ * @param AddressInterfaceFactory $addressDataFactory
+ * @param \Magento\Customer\Model\Customer\Mapper $customerMapper
+ * @param \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor
+ * @param DataObjectHelper $dataObjectHelper
+ * @param ObjectFactory $objectFactory
+ * @param \Magento\Framework\View\LayoutFactory $layoutFactory
+ * @param \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory
+ * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
+ * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory
+ * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory
+ * @param AddressRegistry|null $addressRegistry
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
+ */
+ public function __construct(
+ \Magento\Backend\App\Action\Context $context,
+ \Magento\Framework\Registry $coreRegistry,
+ \Magento\Framework\App\Response\Http\FileFactory $fileFactory,
+ \Magento\Customer\Model\CustomerFactory $customerFactory,
+ \Magento\Customer\Model\AddressFactory $addressFactory,
+ \Magento\Customer\Model\Metadata\FormFactory $formFactory,
+ \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory,
+ \Magento\Customer\Helper\View $viewHelper,
+ \Magento\Framework\Math\Random $random,
+ CustomerRepositoryInterface $customerRepository,
+ \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter,
+ Mapper $addressMapper,
+ AccountManagementInterface $customerAccountManagement,
+ AddressRepositoryInterface $addressRepository,
+ CustomerInterfaceFactory $customerDataFactory,
+ AddressInterfaceFactory $addressDataFactory,
+ \Magento\Customer\Model\Customer\Mapper $customerMapper,
+ \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor,
+ DataObjectHelper $dataObjectHelper,
+ ObjectFactory $objectFactory,
+ \Magento\Framework\View\LayoutFactory $layoutFactory,
+ \Magento\Framework\View\Result\LayoutFactory $resultLayoutFactory,
+ \Magento\Framework\View\Result\PageFactory $resultPageFactory,
+ \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory,
+ \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory,
+ AddressRegistry $addressRegistry = null
+ ) {
+ parent::__construct(
+ $context,
+ $coreRegistry,
+ $fileFactory,
+ $customerFactory,
+ $addressFactory,
+ $formFactory,
+ $subscriberFactory,
+ $viewHelper,
+ $random,
+ $customerRepository,
+ $extensibleDataObjectConverter,
+ $addressMapper,
+ $customerAccountManagement,
+ $addressRepository,
+ $customerDataFactory,
+ $addressDataFactory,
+ $customerMapper,
+ $dataObjectProcessor,
+ $dataObjectHelper,
+ $objectFactory,
+ $layoutFactory,
+ $resultLayoutFactory,
+ $resultPageFactory,
+ $resultForwardFactory,
+ $resultJsonFactory
+ );
+ $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class);
+ }
+
/**
* Reformat customer account data to be compatible with customer service interface
*
@@ -169,7 +273,7 @@ protected function _extractCustomerAddressData(array & $extractedCustomerData)
}
/**
- * Save customer action
+ * Save customer action.
*
* @return \Magento\Backend\Model\View\Result\Redirect
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
@@ -179,11 +283,9 @@ protected function _extractCustomerAddressData(array & $extractedCustomerData)
public function execute()
{
$returnToEdit = false;
- $originalRequestData = $this->getRequest()->getPostValue();
-
$customerId = $this->getCurrentCustomerId();
- if ($originalRequestData) {
+ if ($this->getRequest()->getPostValue()) {
try {
// optional fields might be set in request for future processing by observers in other modules
$customerData = $this->_extractCustomerData();
@@ -191,6 +293,8 @@ public function execute()
if ($customerId) {
$currentCustomer = $this->_customerRepository->getById($customerId);
+ // No need to validate customer address while editing customer profile
+ $this->disableAddressValidation($currentCustomer);
$customerData = array_merge(
$this->customerMapper->toFlatArray($currentCustomer),
$customerData
@@ -269,7 +373,7 @@ public function execute()
$messages = $exception->getMessage();
}
$this->_addSessionErrorMessages($messages);
- $this->_getSession()->setCustomerFormData($originalRequestData);
+ $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData());
$returnToEdit = true;
} catch (\Magento\Framework\Exception\AbstractAggregateException $exception) {
$errors = $exception->getErrors();
@@ -278,18 +382,19 @@ public function execute()
$messages[] = $error->getMessage();
}
$this->_addSessionErrorMessages($messages);
- $this->_getSession()->setCustomerFormData($originalRequestData);
+ $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData());
$returnToEdit = true;
} catch (LocalizedException $exception) {
$this->_addSessionErrorMessages($exception->getMessage());
- $this->_getSession()->setCustomerFormData($originalRequestData);
+ $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData());
$returnToEdit = true;
} catch (\Exception $exception) {
$this->messageManager->addException($exception, __('Something went wrong while saving the customer.'));
- $this->_getSession()->setCustomerFormData($originalRequestData);
+ $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData());
$returnToEdit = true;
}
}
+
$resultRedirect = $this->resultRedirectFactory->create();
if ($returnToEdit) {
if ($customerId) {
@@ -306,6 +411,7 @@ public function execute()
} else {
$resultRedirect->setPath('customer/index');
}
+
return $resultRedirect;
}
@@ -380,4 +486,43 @@ private function getCurrentCustomerId()
return $customerId;
}
+
+ /**
+ * Disable Customer Address Validation
+ *
+ * @param CustomerInterface $customer
+ * @return void
+ */
+ private function disableAddressValidation(CustomerInterface $customer)
+ {
+ foreach ($customer->getAddresses() as $address) {
+ $addressModel = $this->addressRegistry->retrieve($address->getId());
+ $addressModel->setShouldIgnoreValidation(true);
+ }
+ }
+
+ /**
+ * Retrieve formatted form data
+ *
+ * @return array
+ */
+ private function retrieveFormattedFormData(): array
+ {
+ $originalRequestData = $this->getRequest()->getPostValue();
+
+ /* Customer data filtration */
+ if (isset($originalRequestData['customer'])) {
+ $customerData = $this->_extractData(
+ 'adminhtml_customer',
+ CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER,
+ [],
+ 'customer'
+ );
+
+ $customerData = array_intersect_key($customerData, $originalRequestData['customer']);
+ $originalRequestData['customer'] = array_merge($originalRequestData['customer'], $customerData);
+ }
+
+ return $originalRequestData;
+ }
}
diff --git a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php
index aa73e275ee0ca..f82a4d15ae8bf 100644
--- a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php
+++ b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php
@@ -5,10 +5,13 @@
*/
namespace Magento\Customer\CustomerData\Plugin;
-use Magento\Framework\Session\SessionManager;
+use Magento\Framework\Session\SessionManagerInterface;
use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory;
use Magento\Framework\Stdlib\Cookie\PhpCookieManager;
+/**
+ * Class SessionChecker
+ */
class SessionChecker
{
/**
@@ -36,10 +39,12 @@ public function __construct(
/**
* Delete frontend session cookie if customer session is expired
*
- * @param SessionManager $sessionManager
+ * @param SessionManagerInterface $sessionManager
* @return void
+ * @throws \Magento\Framework\Exception\InputException
+ * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException
*/
- public function beforeStart(SessionManager $sessionManager)
+ public function beforeStart(SessionManagerInterface $sessionManager)
{
if (!$this->cookieManager->getCookie($sessionManager->getName())
&& $this->cookieManager->getCookie('mage-cache-sessid')
diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php
index 5ed8ada049b8e..e90ab18172ecc 100644
--- a/app/code/Magento/Customer/Model/AccountManagement.php
+++ b/app/code/Magento/Customer/Model/AccountManagement.php
@@ -16,7 +16,10 @@
use Magento\Customer\Model\Config\Share as ConfigShare;
use Magento\Customer\Model\Customer as CustomerModel;
use Magento\Customer\Model\Customer\CredentialsValidator;
+use Magento\Customer\Model\Data\Customer;
use Magento\Customer\Model\Metadata\Validator;
+use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory;
+use Magento\Directory\Model\AllowedCountries;
use Magento\Eav\Model\Validator\Attribute\Backend;
use Magento\Framework\Api\ExtensibleDataObjectConverter;
use Magento\Framework\Api\SearchCriteriaBuilder;
@@ -44,21 +47,21 @@
use Magento\Framework\Phrase;
use Magento\Framework\Reflection\DataObjectProcessor;
use Magento\Framework\Registry;
+use Magento\Framework\Session\SaveHandlerInterface;
+use Magento\Framework\Session\SessionManagerInterface;
use Magento\Framework\Stdlib\DateTime;
use Magento\Framework\Stdlib\StringUtils as StringHelper;
use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface as PsrLogger;
-use Magento\Framework\Session\SessionManagerInterface;
-use Magento\Framework\Session\SaveHandlerInterface;
-use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory;
/**
- * Handle various customer account actions
+ * Handle various customer account actions.
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.TooManyFields)
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class AccountManagement implements AccountManagementInterface
{
@@ -332,6 +335,16 @@ class AccountManagement implements AccountManagementInterface
*/
private $searchCriteriaBuilder;
+ /**
+ * @var AddressRegistry
+ */
+ private $addressRegistry;
+
+ /**
+ * @var AllowedCountries
+ */
+ private $allowedCountriesReader;
+
/**
* @param CustomerFactory $customerFactory
* @param ManagerInterface $eventManager
@@ -359,12 +372,15 @@ class AccountManagement implements AccountManagementInterface
* @param CredentialsValidator|null $credentialsValidator
* @param DateTimeFactory|null $dateTimeFactory
* @param AccountConfirmation|null $accountConfirmation
- * @param DateTimeFactory $dateTimeFactory
* @param SessionManagerInterface|null $sessionManager
* @param SaveHandlerInterface|null $saveHandler
* @param CollectionFactory|null $visitorCollectionFactory
* @param SearchCriteriaBuilder|null $searchCriteriaBuilder
+ * @param AddressRegistry|null $addressRegistry
+ * @param AllowedCountries|null $allowedCountriesReader
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function __construct(
CustomerFactory $customerFactory,
@@ -396,7 +412,9 @@ public function __construct(
SessionManagerInterface $sessionManager = null,
SaveHandlerInterface $saveHandler = null,
CollectionFactory $visitorCollectionFactory = null,
- SearchCriteriaBuilder $searchCriteriaBuilder = null
+ SearchCriteriaBuilder $searchCriteriaBuilder = null,
+ AddressRegistry $addressRegistry = null,
+ AllowedCountries $allowedCountriesReader = null
) {
$this->customerFactory = $customerFactory;
$this->eventManager = $eventManager;
@@ -434,6 +452,10 @@ public function __construct(
?: ObjectManager::getInstance()->get(CollectionFactory::class);
$this->searchCriteriaBuilder = $searchCriteriaBuilder
?: ObjectManager::getInstance()->get(SearchCriteriaBuilder::class);
+ $this->addressRegistry = $addressRegistry
+ ?: ObjectManager::getInstance()->get(AddressRegistry::class);
+ $this->allowedCountriesReader = $allowedCountriesReader
+ ?: ObjectManager::getInstance()->get(AllowedCountries::class);
}
/**
@@ -453,7 +475,7 @@ private function getAuthentication()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function resendConfirmation($email, $websiteId = null, $redirectUrl = '')
{
@@ -476,7 +498,7 @@ public function resendConfirmation($email, $websiteId = null, $redirectUrl = '')
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function activate($email, $confirmationKey)
{
@@ -485,7 +507,7 @@ public function activate($email, $confirmationKey)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function activateById($customerId, $confirmationKey)
{
@@ -499,8 +521,11 @@ public function activateById($customerId, $confirmationKey)
* @param \Magento\Customer\Api\Data\CustomerInterface $customer
* @param string $confirmationKey
* @return \Magento\Customer\Api\Data\CustomerInterface
- * @throws \Magento\Framework\Exception\State\InvalidTransitionException
- * @throws \Magento\Framework\Exception\State\InputMismatchException
+ * @throws InputException
+ * @throws InputMismatchException
+ * @throws InvalidTransitionException
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
*/
private function activateCustomer($customer, $confirmationKey)
{
@@ -514,13 +539,15 @@ private function activateCustomer($customer, $confirmationKey)
}
$customer->setConfirmation(null);
+ // No need to validate customer and customer address while activating customer
+ $this->setIgnoreValidationFlag($customer);
$this->customerRepository->save($customer);
$this->getEmailNotification()->newAccount($customer, 'confirmed', '', $this->storeManager->getStore()->getId());
return $customer;
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function authenticate($username, $password)
{
@@ -555,7 +582,7 @@ public function authenticate($username, $password)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function validateResetPasswordLinkToken($customerId, $resetPasswordLinkToken)
{
@@ -564,7 +591,7 @@ public function validateResetPasswordLinkToken($customerId, $resetPasswordLinkTo
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function initiatePasswordReset($email, $template, $websiteId = null)
{
@@ -574,6 +601,9 @@ public function initiatePasswordReset($email, $template, $websiteId = null)
// load customer by email
$customer = $this->customerRepository->get($email, $websiteId);
+ // No need to validate customer address while saving customer reset password token
+ $this->disableAddressValidation($customer);
+
$newPasswordToken = $this->mathRandom->getUniqueHash();
$this->changeResetPasswordLinkToken($customer, $newPasswordToken);
@@ -611,10 +641,10 @@ public function initiatePasswordReset($email, $template, $websiteId = null)
* Match a customer by their RP token.
*
* @param string $rpToken
+ * @return CustomerInterface
* @throws ExpiredException
+ * @throws LocalizedException
* @throws NoSuchEntityException
- *
- * @return CustomerInterface
*/
private function matchCustomerByRpToken(string $rpToken): CustomerInterface
{
@@ -647,7 +677,7 @@ private function matchCustomerByRpToken(string $rpToken): CustomerInterface
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function resetPassword($email, $resetToken, $newPassword)
{
@@ -657,6 +687,11 @@ public function resetPassword($email, $resetToken, $newPassword)
} else {
$customer = $this->customerRepository->get($email);
}
+
+ // No need to validate customer and customer address while saving customer reset password token
+ $this->disableAddressValidation($customer);
+ $this->setIgnoreValidationFlag($customer);
+
//Validate Token and new password strength
$this->validateResetPasswordToken($customer->getId(), $resetToken);
$this->credentialsValidator->checkPasswordDifferentFromEmail(
@@ -671,7 +706,12 @@ public function resetPassword($email, $resetToken, $newPassword)
$customerSecure->setPasswordHash($this->createPasswordHash($newPassword));
$this->getAuthentication()->unlock($customer->getId());
$this->destroyCustomerSessions($customer->getId());
- $this->sessionManager->destroy();
+ if ($this->sessionManager->isSessionExists() && !headers_sent()) {
+ //delete old session and move data to the new session
+ //use this instead of $this->sessionManager->regenerateId because last one doesn't delete old session
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
+ session_regenerate_id(true);
+ }
$this->customerRepository->save($customer);
return true;
@@ -763,7 +803,7 @@ protected function getMinPasswordLength()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getConfirmationStatus($customerId)
{
@@ -779,7 +819,7 @@ public function getConfirmationStatus($customerId)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '', $extensions = [])
{
@@ -800,7 +840,7 @@ public function createAccount(CustomerInterface $customer, $password = null, $re
}
/**
- * {@inheritdoc}
+ * @inheritdoc
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
@@ -858,6 +898,9 @@ public function createAccountWithPasswordHash(
}
try {
foreach ($customerAddresses as $address) {
+ if (!$this->isAddressAllowedForWebsite($address, $customer->getStoreId())) {
+ continue;
+ }
if ($address->getId()) {
$newAddress = clone $address;
$newAddress->setId(null);
@@ -882,7 +925,7 @@ public function createAccountWithPasswordHash(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getDefaultBillingAddress($customerId)
{
@@ -891,7 +934,7 @@ public function getDefaultBillingAddress($customerId)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getDefaultShippingAddress($customerId)
{
@@ -906,6 +949,8 @@ public function getDefaultShippingAddress($customerId)
* @param string $redirectUrl
* @param array $extensions
* @return void
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
*/
protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl, $extensions = [])
{
@@ -934,7 +979,7 @@ protected function sendEmailConfirmation(CustomerInterface $customer, $redirectU
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function changePassword($email, $currentPassword, $newPassword)
{
@@ -947,7 +992,7 @@ public function changePassword($email, $currentPassword, $newPassword)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function changePasswordById($customerId, $currentPassword, $newPassword)
{
@@ -960,14 +1005,17 @@ public function changePasswordById($customerId, $currentPassword, $newPassword)
}
/**
- * Change customer password
+ * Change customer password.
*
* @param CustomerInterface $customer
* @param string $currentPassword
* @param string $newPassword
* @return bool true on success
* @throws InputException
+ * @throws InputMismatchException
* @throws InvalidEmailOrPasswordException
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
* @throws UserLockedException
*/
private function changePasswordForCustomer($customer, $currentPassword, $newPassword)
@@ -985,6 +1033,7 @@ private function changePasswordForCustomer($customer, $currentPassword, $newPass
$this->checkPasswordStrength($newPassword);
$customerSecure->setPasswordHash($this->createPasswordHash($newPassword));
$this->destroyCustomerSessions($customer->getId());
+ $this->disableAddressValidation($customer);
$this->customerRepository->save($customer);
return true;
@@ -1002,6 +1051,8 @@ protected function createPasswordHash($password)
}
/**
+ * Get EAV validator
+ *
* @return Backend
*/
private function getEavValidator()
@@ -1013,7 +1064,7 @@ private function getEavValidator()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function validate(CustomerInterface $customer)
{
@@ -1038,7 +1089,7 @@ public function validate(CustomerInterface $customer)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function isEmailAvailable($customerEmail, $websiteId = null)
{
@@ -1054,7 +1105,7 @@ public function isEmailAvailable($customerEmail, $websiteId = null)
}
/**
- * {@inheritDoc}
+ * @inheritdoc
*/
public function isCustomerInStore($customerWebsiteId, $storeId)
{
@@ -1076,10 +1127,11 @@ public function isCustomerInStore($customerWebsiteId, $storeId)
* @param int $customerId
* @param string $resetPasswordLinkToken
* @return bool
- * @throws \Magento\Framework\Exception\State\InputMismatchException If token is mismatched
- * @throws \Magento\Framework\Exception\State\ExpiredException If token is expired
- * @throws \Magento\Framework\Exception\InputException If token or customer id is invalid
- * @throws \Magento\Framework\Exception\NoSuchEntityException If customer doesn't exist
+ * @throws ExpiredException
+ * @throws InputException
+ * @throws InputMismatchException
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
*/
private function validateResetPasswordToken($customerId, $resetPasswordLinkToken)
{
@@ -1169,6 +1221,8 @@ protected function sendNewAccountEmail(
*
* @param CustomerInterface $customer
* @return $this
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
* @deprecated 100.1.0
*/
protected function sendPasswordResetNotificationEmail($customer)
@@ -1182,6 +1236,7 @@ protected function sendPasswordResetNotificationEmail($customer)
* @param CustomerInterface $customer
* @param int|string|null $defaultStoreId
* @return int
+ * @throws LocalizedException
* @deprecated 100.1.0
*/
protected function getWebsiteStoreId($customer, $defaultStoreId = null)
@@ -1195,6 +1250,8 @@ protected function getWebsiteStoreId($customer, $defaultStoreId = null)
}
/**
+ * Get email template types
+ *
* @return array
* @deprecated 100.1.0
*/
@@ -1228,6 +1285,7 @@ protected function getTemplateTypes()
* @param int|null $storeId
* @param string $email
* @return $this
+ * @throws MailException
* @deprecated 100.1.0
*/
protected function sendEmailTemplate(
@@ -1326,14 +1384,15 @@ public function isResetPasswordLinkTokenExpired($rpToken, $rpTokenCreatedAt)
}
/**
- * Change reset password link token
- *
- * Stores new reset password link token
+ * Set a new reset password link token.
*
* @param CustomerInterface $customer
* @param string $passwordLinkToken
* @return bool
* @throws InputException
+ * @throws InputMismatchException
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
*/
public function changeResetPasswordLinkToken($customer, $passwordLinkToken)
{
@@ -1351,8 +1410,10 @@ public function changeResetPasswordLinkToken($customer, $passwordLinkToken)
$customerSecure->setRpTokenCreatedAt(
$this->dateTimeFactory->create()->format(DateTime::DATETIME_PHP_FORMAT)
);
+ $this->setIgnoreValidationFlag($customer);
$this->customerRepository->save($customer);
}
+
return true;
}
@@ -1361,6 +1422,8 @@ public function changeResetPasswordLinkToken($customer, $passwordLinkToken)
*
* @param CustomerInterface $customer
* @return $this
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
* @deprecated 100.1.0
*/
public function sendPasswordReminderEmail($customer)
@@ -1388,6 +1451,8 @@ public function sendPasswordReminderEmail($customer)
*
* @param CustomerInterface $customer
* @return $this
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
* @deprecated 100.1.0
*/
public function sendPasswordResetConfirmationEmail($customer)
@@ -1432,6 +1497,7 @@ protected function getAddressById(CustomerInterface $customer, $addressId)
*
* @param CustomerInterface $customer
* @return Data\CustomerSecure
+ * @throws NoSuchEntityException
* @deprecated 100.1.0
*/
protected function getFullCustomerObject($customer)
@@ -1457,6 +1523,20 @@ public function getPasswordHash($password)
return $this->encryptor->getHash($password);
}
+ /**
+ * Disable Customer Address Validation
+ *
+ * @param CustomerInterface $customer
+ * @throws NoSuchEntityException
+ */
+ private function disableAddressValidation($customer)
+ {
+ foreach ($customer->getAddresses() as $address) {
+ $addressModel = $this->addressRegistry->retrieve($address->getId());
+ $addressModel->setShouldIgnoreValidation(true);
+ }
+ }
+
/**
* Get email notification
*
@@ -1476,6 +1556,7 @@ private function getEmailNotification()
/**
* Destroy all active customer sessions by customer id (current session will not be destroyed).
+ *
* Customer sessions which should be deleted are collecting from the "customer_visitor" table considering
* configured session lifetime.
*
@@ -1502,4 +1583,29 @@ private function destroyCustomerSessions($customerId)
$this->saveHandler->destroy($sessionId);
}
}
+
+ /**
+ * Set ignore_validation_flag for reset password flow to skip unnecessary address and customer validation.
+ *
+ * @param Customer $customer
+ * @return void
+ */
+ private function setIgnoreValidationFlag(Customer $customer)
+ {
+ $customer->setData('ignore_validation_flag', true);
+ }
+
+ /**
+ * Check is address allowed for store
+ *
+ * @param AddressInterface $address
+ * @param int|null $storeId
+ * @return bool
+ */
+ private function isAddressAllowedForWebsite(AddressInterface $address, $storeId): bool
+ {
+ $allowedCountries = $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_STORE, $storeId);
+
+ return in_array($address->getCountryId(), $allowedCountries);
+ }
}
diff --git a/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php b/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php
index fc0fa3ebc073d..40a10a1db0935 100644
--- a/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php
+++ b/app/code/Magento/Customer/Model/Config/Backend/Address/Street.php
@@ -87,15 +87,20 @@ public function afterDelete()
{
$result = parent::afterDelete();
- if ($this->getScope() == \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES) {
- $attribute = $this->_eavConfig->getAttribute('customer_address', 'street');
- $website = $this->_storeManager->getWebsite($this->getScopeCode());
- $attribute->setWebsite($website);
- $attribute->load($attribute->getId());
- $attribute->setData('scope_multiline_count', null);
- $attribute->save();
- }
+ $attribute = $this->_eavConfig->getAttribute('customer_address', 'street');
+ switch ($this->getScope()) {
+ case \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES:
+ $website = $this->_storeManager->getWebsite($this->getScopeCode());
+ $attribute->setWebsite($website);
+ $attribute->load($attribute->getId());
+ $attribute->setData('scope_multiline_count', null);
+ break;
+ case ScopeConfigInterface::SCOPE_TYPE_DEFAULT:
+ $attribute->setData('multiline_count', 2);
+ break;
+ }
+ $attribute->save();
return $result;
}
}
diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php
index ce976d3f62c74..9c9f04185477e 100644
--- a/app/code/Magento/Customer/Model/Customer/DataProvider.php
+++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php
@@ -375,45 +375,17 @@ protected function getAttributesMeta(Type $entityType)
return $meta;
}
- /**
- * Check whether the specific attribute can be shown in form: customer registration, customer edit, etc...
- *
- * @param Attribute $customerAttribute
- * @return bool
- */
- private function canShowAttributeInForm(AbstractAttribute $customerAttribute)
- {
- $isRegistration = $this->context->getRequestParam($this->getRequestFieldName()) === null;
-
- if ($customerAttribute->getEntityType()->getEntityTypeCode() === 'customer') {
- return is_array($customerAttribute->getUsedInForms()) &&
- (
- (in_array('customer_account_create', $customerAttribute->getUsedInForms()) && $isRegistration) ||
- (in_array('customer_account_edit', $customerAttribute->getUsedInForms()) && !$isRegistration)
- );
- } else {
- return is_array($customerAttribute->getUsedInForms()) &&
- in_array('customer_address_edit', $customerAttribute->getUsedInForms());
- }
- }
-
/**
* Detect can we show attribute on specific form or not
*
* @param Attribute $customerAttribute
* @return bool
*/
- private function canShowAttribute(AbstractAttribute $customerAttribute)
+ private function canShowAttribute(AbstractAttribute $customerAttribute): bool
{
- $userDefined = (bool) $customerAttribute->getIsUserDefined();
- if (!$userDefined) {
- return $customerAttribute->getIsVisible();
- }
-
- $canShowOnForm = $this->canShowAttributeInForm($customerAttribute);
-
- return ($this->allowToShowHiddenAttributes && $canShowOnForm) ||
- (!$this->allowToShowHiddenAttributes && $canShowOnForm && $customerAttribute->getIsVisible());
+ return $this->allowToShowHiddenAttributes && (bool) $customerAttribute->getIsUserDefined()
+ ? true
+ : (bool) $customerAttribute->getIsVisible();
}
/**
diff --git a/app/code/Magento/Customer/Model/CustomerAuthUpdate.php b/app/code/Magento/Customer/Model/CustomerAuthUpdate.php
index 06de649524e71..bc9bffb6ffdf0 100644
--- a/app/code/Magento/Customer/Model/CustomerAuthUpdate.php
+++ b/app/code/Magento/Customer/Model/CustomerAuthUpdate.php
@@ -6,31 +6,43 @@
namespace Magento\Customer\Model;
+use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Exception\NoSuchEntityException;
+
/**
* Customer Authentication update model.
*/
class CustomerAuthUpdate
{
/**
- * @var \Magento\Customer\Model\CustomerRegistry
+ * @var CustomerRegistry
*/
protected $customerRegistry;
/**
- * @var \Magento\Customer\Model\ResourceModel\Customer
+ * @var CustomerResourceModel
*/
protected $customerResourceModel;
/**
- * @param \Magento\Customer\Model\CustomerRegistry $customerRegistry
- * @param \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel
+ * @var Customer
+ */
+ private $customerModel;
+
+ /**
+ * @param CustomerRegistry $customerRegistry
+ * @param CustomerResourceModel $customerResourceModel
+ * @param Customer|null $customerModel
*/
public function __construct(
- \Magento\Customer\Model\CustomerRegistry $customerRegistry,
- \Magento\Customer\Model\ResourceModel\Customer $customerResourceModel
+ CustomerRegistry $customerRegistry,
+ CustomerResourceModel $customerResourceModel,
+ Customer $customerModel = null
) {
$this->customerRegistry = $customerRegistry;
$this->customerResourceModel = $customerResourceModel;
+ $this->customerModel = $customerModel ?: ObjectManager::getInstance()->get(Customer::class);
}
/**
@@ -38,21 +50,30 @@ public function __construct(
*
* @param int $customerId
* @return $this
+ * @throws NoSuchEntityException
*/
public function saveAuth($customerId)
{
$customerSecure = $this->customerRegistry->retrieveSecureData($customerId);
+ $this->customerResourceModel->load($this->customerModel, $customerId);
+ $currentLockExpiresVal = $this->customerModel->getData('lock_expires');
+ $newLockExpiresVal = $customerSecure->getData('lock_expires');
+
$this->customerResourceModel->getConnection()->update(
$this->customerResourceModel->getTable('customer_entity'),
[
'failures_num' => $customerSecure->getData('failures_num'),
'first_failure' => $customerSecure->getData('first_failure'),
- 'lock_expires' => $customerSecure->getData('lock_expires'),
+ 'lock_expires' => $newLockExpiresVal,
],
$this->customerResourceModel->getConnection()->quoteInto('entity_id = ?', $customerId)
);
+ if ($currentLockExpiresVal !== $newLockExpiresVal) {
+ $this->customerModel->reindex();
+ }
+
return $this;
}
}
diff --git a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php
index 5a46fdb9defc4..71f0b393e4a5d 100644
--- a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php
+++ b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php
@@ -12,6 +12,7 @@
use Magento\Framework\App\Cache\StateInterface;
use Magento\Framework\App\CacheInterface;
use Magento\Framework\Serialize\SerializerInterface;
+use Magento\Store\Model\StoreManagerInterface;
/**
* Cache for attribute metadata
@@ -53,6 +54,11 @@ class AttributeMetadataCache
*/
private $serializer;
+ /**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
+
/**
* Constructor
*
@@ -60,17 +66,21 @@ class AttributeMetadataCache
* @param StateInterface $state
* @param SerializerInterface $serializer
* @param AttributeMetadataHydrator $attributeMetadataHydrator
+ * @param StoreManagerInterface|null $storeManager
*/
public function __construct(
CacheInterface $cache,
StateInterface $state,
SerializerInterface $serializer,
- AttributeMetadataHydrator $attributeMetadataHydrator
+ AttributeMetadataHydrator $attributeMetadataHydrator,
+ StoreManagerInterface $storeManager = null
) {
$this->cache = $cache;
$this->state = $state;
$this->serializer = $serializer;
$this->attributeMetadataHydrator = $attributeMetadataHydrator;
+ $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(StoreManagerInterface::class);
}
/**
@@ -82,11 +92,12 @@ public function __construct(
*/
public function load($entityType, $suffix = '')
{
- if (isset($this->attributes[$entityType . $suffix])) {
- return $this->attributes[$entityType . $suffix];
+ $storeId = $this->storeManager->getStore()->getId();
+ if (isset($this->attributes[$entityType . $suffix . $storeId])) {
+ return $this->attributes[$entityType . $suffix . $storeId];
}
if ($this->isEnabled()) {
- $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix;
+ $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId;
$serializedData = $this->cache->load($cacheKey);
if ($serializedData) {
$attributesData = $this->serializer->unserialize($serializedData);
@@ -94,7 +105,7 @@ public function load($entityType, $suffix = '')
foreach ($attributesData as $key => $attributeData) {
$attributes[$key] = $this->attributeMetadataHydrator->hydrate($attributeData);
}
- $this->attributes[$entityType . $suffix] = $attributes;
+ $this->attributes[$entityType . $suffix . $storeId] = $attributes;
return $attributes;
}
}
@@ -111,9 +122,10 @@ public function load($entityType, $suffix = '')
*/
public function save($entityType, array $attributes, $suffix = '')
{
- $this->attributes[$entityType . $suffix] = $attributes;
+ $storeId = $this->storeManager->getStore()->getId();
+ $this->attributes[$entityType . $suffix . $storeId] = $attributes;
if ($this->isEnabled()) {
- $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix;
+ $cacheKey = self::ATTRIBUTE_METADATA_CACHE_PREFIX . $entityType . $suffix . $storeId;
$attributesData = [];
foreach ($attributes as $key => $attribute) {
$attributesData[$key] = $this->attributeMetadataHydrator->extract($attribute);
diff --git a/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php b/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php
index f28cce0ea2ae1..0e4fc68503122 100644
--- a/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php
+++ b/app/code/Magento/Customer/Model/Metadata/Form/AbstractData.php
@@ -1,7 +1,5 @@
setMessage(__('"%1" invalid type entered.', $label), \Zend_Validate_Alnum::INVALID);
$validator->setMessage(
__('"%1" contains non-alphabetic or non-numeric characters.', $label),
diff --git a/app/code/Magento/Customer/Model/Options.php b/app/code/Magento/Customer/Model/Options.php
index 7747e309d82a6..f8761b4888a32 100644
--- a/app/code/Magento/Customer/Model/Options.php
+++ b/app/code/Magento/Customer/Model/Options.php
@@ -8,7 +8,11 @@
use Magento\Config\Model\Config\Source\Nooptreq as NooptreqSource;
use Magento\Customer\Helper\Address as AddressHelper;
use Magento\Framework\Escaper;
+use Magento\Store\Api\Data\StoreInterface;
+/**
+ * Customer Options.
+ */
class Options
{
/**
@@ -38,7 +42,7 @@ public function __construct(
/**
* Retrieve name prefix dropdown options
*
- * @param null $store
+ * @param null|string|bool|int|StoreInterface $store
* @return array|bool
*/
public function getNamePrefixOptions($store = null)
@@ -52,7 +56,7 @@ public function getNamePrefixOptions($store = null)
/**
* Retrieve name suffix dropdown options
*
- * @param null $store
+ * @param null|string|bool|int|StoreInterface $store
* @return array|bool
*/
public function getNameSuffixOptions($store = null)
@@ -64,7 +68,9 @@ public function getNameSuffixOptions($store = null)
}
/**
- * @param $options
+ * Unserialize and clear name prefix or suffix options.
+ *
+ * @param string $options
* @param bool $isOptional
* @return array|bool
*
@@ -91,7 +97,7 @@ private function prepareNamePrefixSuffixOptions($options, $isOptional = false)
return false;
}
$result = [];
- $options = explode(';', $options);
+ $options = array_filter(explode(';', $options));
foreach ($options as $value) {
$value = $this->escaper->escapeHtml(trim($value));
$result[$value] = $value;
diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address.php b/app/code/Magento/Customer/Model/ResourceModel/Address.php
index a52c372310843..8b5c9a08931b5 100644
--- a/app/code/Magento/Customer/Model/ResourceModel/Address.php
+++ b/app/code/Magento/Customer/Model/ResourceModel/Address.php
@@ -13,7 +13,8 @@
use Magento\Framework\App\ObjectManager;
/**
- * Class Address
+ * Customer Address resource model.
+ *
* @package Magento\Customer\Model\ResourceModel
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
@@ -31,8 +32,8 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity
/**
* @param \Magento\Eav\Model\Entity\Context $context
- * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot,
- * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite,
+ * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot
+ * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite
* @param \Magento\Framework\Validator\Factory $validatorFactory
* @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository
* @param array $data
@@ -90,7 +91,7 @@ protected function _beforeSave(\Magento\Framework\DataObject $address)
}
/**
- * Validate customer address entity
+ * Validate customer address entity.
*
* @param \Magento\Framework\DataObject $address
* @return void
@@ -98,6 +99,9 @@ protected function _beforeSave(\Magento\Framework\DataObject $address)
*/
protected function _validate($address)
{
+ if ($address->getDataByKey('should_ignore_validation')) {
+ return;
+ };
$validator = $this->_validatorFactory->createValidator('customer_address', 'save');
if (!$validator->isValid($address)) {
diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php
index 7e5f9d51549ec..daacb2655f588 100644
--- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php
+++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php
@@ -11,7 +11,7 @@
use Magento\Framework\Exception\AlreadyExistsException;
/**
- * Customer entity resource model
+ * Customer entity resource model.
*
* @api
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -92,7 +92,7 @@ protected function _getDefaultAttributes()
}
/**
- * Check customer scope, email and confirmation key before saving
+ * Check customer scope, email and confirmation key before saving.
*
* @param \Magento\Framework\DataObject $customer
* @return $this
@@ -150,7 +150,9 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer)
$customer->setConfirmation(null);
}
- $this->_validate($customer);
+ if (!$customer->getData('ignore_validation_flag')) {
+ $this->_validate($customer);
+ }
return $this;
}
diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php b/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php
index e55c5d443c9d1..d55a5c0aea2be 100644
--- a/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php
+++ b/app/code/Magento/Customer/Model/ResourceModel/Customer/Relation.php
@@ -7,12 +7,12 @@
namespace Magento\Customer\Model\ResourceModel\Customer;
/**
- * Class Relation
+ * Class to process object relations.
*/
class Relation implements \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationInterface
{
/**
- * Save relations for Customer
+ * Save relations for Customer.
*
* @param \Magento\Framework\Model\AbstractModel $customer
* @return void
@@ -23,41 +23,43 @@ public function processRelation(\Magento\Framework\Model\AbstractModel $customer
$defaultBillingId = $customer->getData('default_billing');
$defaultShippingId = $customer->getData('default_shipping');
- /** @var \Magento\Customer\Model\Address $address */
- foreach ($customer->getAddresses() as $address) {
- if ($address->getData('_deleted')) {
- if ($address->getId() == $defaultBillingId) {
- $customer->setData('default_billing', null);
- }
+ if (!$customer->getData('ignore_validation_flag')) {
+ /** @var \Magento\Customer\Model\Address $address */
+ foreach ($customer->getAddresses() as $address) {
+ if ($address->getData('_deleted')) {
+ if ($address->getId() == $defaultBillingId) {
+ $customer->setData('default_billing', null);
+ }
- if ($address->getId() == $defaultShippingId) {
- $customer->setData('default_shipping', null);
- }
+ if ($address->getId() == $defaultShippingId) {
+ $customer->setData('default_shipping', null);
+ }
- $removedAddressId = $address->getId();
- $address->delete();
+ $removedAddressId = $address->getId();
+ $address->delete();
- // Remove deleted address from customer address collection
- $customer->getAddressesCollection()->removeItemByKey($removedAddressId);
- } else {
- $address->setParentId(
- $customer->getId()
- )->setStoreId(
- $customer->getStoreId()
- )->setIsCustomerSaveTransaction(
- true
- )->save();
+ // Remove deleted address from customer address collection
+ $customer->getAddressesCollection()->removeItemByKey($removedAddressId);
+ } else {
+ $address->setParentId(
+ $customer->getId()
+ )->setStoreId(
+ $customer->getStoreId()
+ )->setIsCustomerSaveTransaction(
+ true
+ )->save();
- if (($address->getIsPrimaryBilling() ||
- $address->getIsDefaultBilling()) && $address->getId() != $defaultBillingId
- ) {
- $customer->setData('default_billing', $address->getId());
- }
+ if (($address->getIsPrimaryBilling() ||
+ $address->getIsDefaultBilling()) && $address->getId() != $defaultBillingId
+ ) {
+ $customer->setData('default_billing', $address->getId());
+ }
- if (($address->getIsPrimaryShipping() ||
- $address->getIsDefaultShipping()) && $address->getId() != $defaultShippingId
- ) {
- $customer->setData('default_shipping', $address->getId());
+ if (($address->getIsPrimaryShipping() ||
+ $address->getIsDefaultShipping()) && $address->getId() != $defaultShippingId
+ ) {
+ $customer->setData('default_shipping', $address->getId());
+ }
}
}
}
diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php
index 9e745769e2c36..d0efdde3d8c59 100644
--- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php
+++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php
@@ -12,6 +12,7 @@
use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface;
use Magento\Customer\Model\CustomerFactory;
use Magento\Customer\Model\CustomerRegistry;
+use \Magento\Customer\Model\Customer as CustomerModel;
use Magento\Customer\Model\Data\CustomerSecureFactory;
use Magento\Customer\Model\Customer\NotificationStorage;
use Magento\Customer\Model\Delegation\Data\NewOperation;
@@ -170,7 +171,8 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
+ *
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
@@ -240,7 +242,7 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa
$prevCustomerDataArr['default_shipping']
);
}
-
+ $this->setIgnoreValidationFlag($customerArr, $customerModel);
$customerModel->save();
$this->customerRegistry->push($customerModel);
$customerId = $customerModel->getId();
@@ -253,7 +255,7 @@ public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $pa
$delegatedNewOperation->getCustomer()->getAddresses()
);
}
- if ($customer->getAddresses() !== null) {
+ if ($customer->getAddresses() !== null && !$customerModel->getData('ignore_validation_flag')) {
if ($customer->getId()) {
$existingAddresses = $this->getById($customer->getId())->getAddresses();
$getIdFunc = function ($address) {
@@ -365,7 +367,7 @@ public function getList(SearchCriteriaInterface $searchCriteria)
->joinAttribute('billing_telephone', 'customer_address/telephone', 'default_billing', null, 'left')
->joinAttribute('billing_region', 'customer_address/region', 'default_billing', null, 'left')
->joinAttribute('billing_country_id', 'customer_address/country_id', 'default_billing', null, 'left')
- ->joinAttribute('company', 'customer_address/company', 'default_billing', null, 'left');
+ ->joinAttribute('billing_company', 'customer_address/company', 'default_billing', null, 'left');
$this->collectionProcessor->process($searchCriteria, $collection);
@@ -420,4 +422,18 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collecti
$collection->addFieldToFilter($fields);
}
}
+
+ /**
+ * Set ignore_validation_flag to skip model validation.
+ *
+ * @param array $customerArray
+ * @param CustomerModel $customerModel
+ * @return void
+ */
+ private function setIgnoreValidationFlag(array $customerArray, CustomerModel $customerModel)
+ {
+ if (isset($customerArray['ignore_validation_flag'])) {
+ $customerModel->setData('ignore_validation_flag', true);
+ }
+ }
}
diff --git a/app/code/Magento/Customer/Model/Vat.php b/app/code/Magento/Customer/Model/Vat.php
index f608a6cf4c11c..123a9eef4b75a 100644
--- a/app/code/Magento/Customer/Model/Vat.php
+++ b/app/code/Magento/Customer/Model/Vat.php
@@ -179,18 +179,21 @@ public function checkVatNumber($countryCode, $vatNumber, $requesterCountryCode =
return $gatewayResponse;
}
+ $countryCodeForVatNumber = $this->getCountryCodeForVatNumber($countryCode);
+ $requesterCountryCodeForVatNumber = $this->getCountryCodeForVatNumber($requesterCountryCode);
+
try {
$soapClient = $this->createVatNumberValidationSoapClient();
$requestParams = [];
- $requestParams['countryCode'] = $countryCode;
+ $requestParams['countryCode'] = $countryCodeForVatNumber;
$vatNumberSanitized = $this->isCountryInEU($countryCode)
- ? str_replace([' ', '-', $countryCode], ['', '', ''], $vatNumber)
+ ? str_replace([' ', '-', $countryCodeForVatNumber], ['', '', ''], $vatNumber)
: str_replace([' ', '-'], ['', ''], $vatNumber);
$requestParams['vatNumber'] = $vatNumberSanitized;
- $requestParams['requesterCountryCode'] = $requesterCountryCode;
+ $requestParams['requesterCountryCode'] = $requesterCountryCodeForVatNumber;
$reqVatNumSanitized = $this->isCountryInEU($requesterCountryCode)
- ? str_replace([' ', '-', $requesterCountryCode], ['', '', ''], $requesterVatNumber)
+ ? str_replace([' ', '-', $requesterCountryCodeForVatNumber], ['', '', ''], $requesterVatNumber)
: str_replace([' ', '-'], ['', ''], $requesterVatNumber);
$requestParams['requesterVatNumber'] = $reqVatNumSanitized;
// Send request to service
@@ -301,4 +304,22 @@ public function isCountryInEU($countryCode, $storeId = null)
);
return in_array($countryCode, $euCountries);
}
+
+ /**
+ * Returns the country code to use in the VAT number which is not always the same as the normal country code
+ *
+ * @param string $countryCode
+ * @return string
+ */
+ private function getCountryCodeForVatNumber(string $countryCode): string
+ {
+ // Greece uses a different code for VAT numbers then its country code
+ // See: http://ec.europa.eu/taxation_customs/vies/faq.html#item_11
+ // And https://en.wikipedia.org/wiki/VAT_identification_number:
+ // "The full identifier starts with an ISO 3166-1 alpha-2 (2 letters) country code
+ // (except for Greece, which uses the ISO 639-1 language code EL for the Greek language,
+ // instead of its ISO 3166-1 alpha-2 country code GR)"
+
+ return $countryCode === 'GR' ? 'EL' : $countryCode;
+ }
}
diff --git a/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php b/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php
index eb7e81009c92c..831506af17cf6 100644
--- a/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php
+++ b/app/code/Magento/Customer/Observer/UpgradeCustomerPasswordObserver.php
@@ -6,11 +6,15 @@
namespace Magento\Customer\Observer;
+use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\Event\ObserverInterface;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Model\CustomerRegistry;
+/**
+ * Observer to execute upgrading customer password hash when customer has logged in.
+ */
class UpgradeCustomerPasswordObserver implements ObserverInterface
{
/**
@@ -46,7 +50,7 @@ public function __construct(
}
/**
- * Upgrade customer password hash when customer has logged in
+ * Upgrade customer password hash when customer has logged in.
*
* @param \Magento\Framework\Event\Observer $observer
* @return void
@@ -61,7 +65,20 @@ public function execute(\Magento\Framework\Event\Observer $observer)
if (!$this->encryptor->validateHashVersion($customerSecure->getPasswordHash(), true)) {
$customerSecure->setPasswordHash($this->encryptor->getHash($password, true));
+ // No need to validate customer and customer address while upgrading customer password
+ $this->setIgnoreValidationFlag($customer);
$this->customerRepository->save($customer);
}
}
+
+ /**
+ * Set ignore_validation_flag to skip unnecessary address and customer validation.
+ *
+ * @param CustomerInterface $customer
+ * @return void
+ */
+ private function setIgnoreValidationFlag(CustomerInterface $customer)
+ {
+ $customer->setData('ignore_validation_flag', true);
+ }
}
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAddCustomerAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAddCustomerAddressActionGroup.xml
new file mode 100644
index 0000000000000..1ffc258e78a43
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAddCustomerAddressActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerActionGroup.xml
new file mode 100644
index 0000000000000..b9bac35b503b3
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml
index 3d6e0fb54b054..3b7aab22f749e 100644
--- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/OpenEditCustomerFromAdminActionGroup.xml
@@ -6,18 +6,17 @@
*/
-->
-
+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
+
-
+
-
-
+
-
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/RemoveCustomerFromAdminActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/RemoveCustomerFromAdminActionGroup.xml
index a53968a920806..3ec06d8353a45 100644
--- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/RemoveCustomerFromAdminActionGroup.xml
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/RemoveCustomerFromAdminActionGroup.xml
@@ -10,10 +10,9 @@
-
+
-
@@ -28,6 +27,6 @@
-
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml
index a8ee604edee0a..d86591b799a33 100644
--- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml
@@ -9,10 +9,9 @@
xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd">
-
+
-
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml
index 50a238323e331..aa764e5f51de1 100644
--- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerAccountActionGroup.xml
@@ -17,4 +17,10 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRegisterCustomerFromOrderSuccessPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRegisterCustomerFromOrderSuccessPageActionGroup.xml
new file mode 100644
index 0000000000000..a97ad2a8b1907
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRegisterCustomerFromOrderSuccessPageActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml
index 4eea31665fe69..573767a12361a 100644
--- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml
@@ -67,6 +67,7 @@
California
90230
US
+
United States
Yes
Yes
RegionCA
@@ -101,6 +102,15 @@
SE1 7RW
GB
+
United Kingdom
444-44-444-44
+
+ false
+ true
+
+
+ true
+ false
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml
index 49453a7747d4b..c3efa0fbc880c 100644
--- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml
@@ -21,6 +21,7 @@
John
Doe
S
+
John Doe
pwdTest123!
Mr
Sr
@@ -44,6 +45,16 @@
0
US_Address_TX
+
+ 1
+ John.Doe@example.com
+ John
+ Doe
+ John Doe
+ pwdTest123!
+ 0
+ 0
+
Jane
@@ -74,4 +85,16 @@
0
US_Address_NY
+
+ 1
+ John.Doe@example.com
+ John
+ Doe
+ John Doe
+ pwdTest123!
+ 0
+ 0
+ US_Default_Billing_Address_TX
+ US_Default_Shipping_Address_CA
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerNameAddressOptionsConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerNameAddressOptionsConfigData.xml
new file mode 100644
index 0000000000000..1331f288286e5
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerNameAddressOptionsConfigData.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+ PrefixOptions
+
+
+ Mr;Mrs;Ms;Dr
+
+
+
+ DefaultPrefixOptions
+
+
+
+
+
+
+ SuffixOptions
+
+
+ Jr;Sr
+
+
+
+ DefaultSuffixOptions
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_name_address_options_config-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_name_address_options_config-meta.xml
new file mode 100644
index 0000000000000..07175a09fe3e1
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_name_address_options_config-meta.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+ string
+
+
+ string
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml
index 72d5d90bdc05f..7cd36c12c80bd 100644
--- a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml
@@ -12,5 +12,6 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountInformationPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountInformationPage.xml
new file mode 100644
index 0000000000000..80caea5a1f541
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAccountInformationPage.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressNewPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressNewPage.xml
new file mode 100644
index 0000000000000..8fbc7b10fdd95
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerAddressNewPage.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountAddressSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountAddressSection.xml
index db9619dde671f..70042e2a71467 100644
--- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountAddressSection.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountAddressSection.xml
@@ -7,7 +7,7 @@
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountNewAddressSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountNewAddressSection.xml
new file mode 100644
index 0000000000000..8445343c9b9c0
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountNewAddressSection.xml
@@ -0,0 +1,19 @@
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml
index 0a77890033295..9553752539757 100644
--- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml
@@ -7,8 +7,9 @@
-->
+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml
new file mode 100644
index 0000000000000..f6706d0e16ab3
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressEditFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressEditFormSection.xml
new file mode 100644
index 0000000000000..2af00532301ed
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerAddressEditFormSection.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml
index de3975fd82d01..419387fd92d1f 100644
--- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml
@@ -1,3 +1,4 @@
+
+
+ Customer Login
+
diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml b/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml
index f053805409fe5..f5ee2b347a5b2 100644
--- a/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml
+++ b/app/code/Magento/Customer/view/frontend/layout/customer_address_form.xml
@@ -20,6 +20,7 @@
Magento\Customer\Block\DataProviders\AddressAttributeData
+ Magento\Customer\Block\DataProviders\PostCodesPatternsAttributeData
diff --git a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml
index 644238e3949c5..992c866316d79 100644
--- a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml
+++ b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml
@@ -126,6 +126,9 @@
title="= /* @noEscape */ $block->getAttributeData()->getFrontendLabel('postcode') ?>"
id="zip"
class="input-text validate-zip-international = $block->escapeHtmlAttr($this->helper(\Magento\Customer\Helper\Address::class)->getAttributeValidationClass('postcode')) ?>">
+
+
+
@@ -184,7 +187,9 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/LayeredNavigation/composer.json b/app/code/Magento/LayeredNavigation/composer.json
index 8ce17fe30ec35..328549074f9b8 100644
--- a/app/code/Magento/LayeredNavigation/composer.json
+++ b/app/code/Magento/LayeredNavigation/composer.json
@@ -8,7 +8,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.4",
+ "version": "100.2.5",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Msrp/Helper/Data.php b/app/code/Magento/Msrp/Helper/Data.php
index b4ec34ebee19c..e8f8ca17a8b64 100644
--- a/app/code/Magento/Msrp/Helper/Data.php
+++ b/app/code/Magento/Msrp/Helper/Data.php
@@ -11,6 +11,8 @@
use Magento\Store\Model\StoreManagerInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\GroupedProduct\Model\Product\Type\Grouped;
+use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
/**
* Msrp data helper
@@ -70,8 +72,7 @@ public function __construct(
}
/**
- * Check if can apply Minimum Advertise price to product
- * in specific visibility
+ * Check if can apply MAP to product in specific visibility.
*
* @param int|Product $product
* @param int|null $visibility Check displaying price in concrete place (by default generally)
@@ -135,6 +136,8 @@ public function isShowPriceOnGesture($product)
}
/**
+ * Check if should show MAP price before order confirmation.
+ *
* @param int|Product $product
* @return bool
*/
@@ -144,6 +147,8 @@ public function isShowBeforeOrderConfirm($product)
}
/**
+ * Check if any MAP price is larger than "As low as" value.
+ *
* @param int|Product $product
* @return bool|float
*/
@@ -155,10 +160,18 @@ public function isMinimalPriceLessMsrp($product)
$msrp = $product->getMsrp();
$price = $product->getPriceInfo()->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE);
if ($msrp === null) {
- if ($product->getTypeId() !== \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE) {
- return false;
- } else {
+ if ($product->getTypeId() === Grouped::TYPE_CODE) {
$msrp = $product->getTypeInstance()->getChildrenMsrp($product);
+ } elseif ($product->getTypeId() === Configurable::TYPE_CODE) {
+ $prices = [];
+ foreach ($product->getTypeInstance()->getUsedProducts($product) as $item) {
+ if ($item->getMsrp() !== null) {
+ $prices[] = $item->getMsrp();
+ }
+ }
+ $msrp = $prices ? max($prices) : 0;
+ } else {
+ return false;
}
}
if ($msrp) {
diff --git a/app/code/Magento/Msrp/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Msrp/Test/Mftf/ActionGroup/AdminProductActionGroup.xml
new file mode 100644
index 0000000000000..c8cdf8db42a7b
--- /dev/null
+++ b/app/code/Magento/Msrp/Test/Mftf/ActionGroup/AdminProductActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Msrp/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml b/app/code/Magento/Msrp/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml
new file mode 100644
index 0000000000000..b3742574c8235
--- /dev/null
+++ b/app/code/Magento/Msrp/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+ ${{msrp}}
+ grabMsrp
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Msrp/Test/Mftf/Data/MsrpConfigData.xml b/app/code/Magento/Msrp/Test/Mftf/Data/MsrpConfigData.xml
new file mode 100644
index 0000000000000..731169aa40041
--- /dev/null
+++ b/app/code/Magento/Msrp/Test/Mftf/Data/MsrpConfigData.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ EnableMAP
+
+
+ 1
+
+
+
+ DisableMAP
+
+
+ 0
+
+
diff --git a/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_config-meta.xml b/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_config-meta.xml
new file mode 100644
index 0000000000000..f911d7072fb9a
--- /dev/null
+++ b/app/code/Magento/Msrp/Test/Mftf/Metadata/msrp_config-meta.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+ string
+
+
+
+
+
+
diff --git a/app/code/Magento/Msrp/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Msrp/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml
new file mode 100644
index 0000000000000..08c0cb8d48309
--- /dev/null
+++ b/app/code/Magento/Msrp/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/code/Magento/Msrp/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Msrp/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
new file mode 100644
index 0000000000000..7aba2d8d6e211
--- /dev/null
+++ b/app/code/Magento/Msrp/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontCheckingConfigurableProductPriceWithMapTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontCheckingConfigurableProductPriceWithMapTest.xml
new file mode 100644
index 0000000000000..1a2d4ad8e06e4
--- /dev/null
+++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontCheckingConfigurableProductPriceWithMapTest.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Msrp/composer.json b/app/code/Magento/Msrp/composer.json
index 625bec5d6c9d2..405c11dfe8195 100644
--- a/app/code/Magento/Msrp/composer.json
+++ b/app/code/Magento/Msrp/composer.json
@@ -8,6 +8,7 @@
"magento/module-downloadable": "100.2.*",
"magento/module-eav": "101.0.*",
"magento/module-grouped-product": "100.2.*",
+ "magento/module-configurable-product": "100.2.*",
"magento/module-tax": "100.2.*",
"magento/framework": "101.0.*"
},
@@ -16,7 +17,7 @@
"magento/module-msrp-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "100.2.3",
+ "version": "100.2.4",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Msrp/etc/adminhtml/system.xml b/app/code/Magento/Msrp/etc/adminhtml/system.xml
index 8ce0ea67343f8..c20d753a2e794 100644
--- a/app/code/Magento/Msrp/etc/adminhtml/system.xml
+++ b/app/code/Magento/Msrp/etc/adminhtml/system.xml
@@ -10,7 +10,7 @@
Minimum Advertised Price
-
+
Enable MAP
Magento\Config\Model\Config\Source\Yesno
diff --git a/app/code/Magento/Msrp/i18n/en_US.csv b/app/code/Magento/Msrp/i18n/en_US.csv
index d647f8527ec15..d47d72b2bdc9a 100644
--- a/app/code/Magento/Msrp/i18n/en_US.csv
+++ b/app/code/Magento/Msrp/i18n/en_US.csv
@@ -13,6 +13,7 @@ Price,Price
"Add to Cart","Add to Cart"
"Minimum Advertised Price","Minimum Advertised Price"
"Enable MAP","Enable MAP"
+"Warning! Enabling MAP by default will hide all product prices on Storefront.","Warning! Enabling MAP by default will hide all product prices on Storefront."
"Display Actual Price","Display Actual Price"
"Default Popup Text Message","Default Popup Text Message"
"Default ""What's This"" Text Message","Default ""What's This"" Text Message"
diff --git a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml
index 869d81563645a..ee282ebb82eb9 100644
--- a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml
+++ b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml
@@ -20,61 +20,79 @@ $priceType = $block->getPrice();
/** @var $product \Magento\Catalog\Model\Product */
$product = $block->getSaleableItem();
$productId = $product->getId();
+$amount = 0;
+
+if ($product->getMsrp()) {
+ $amount = $product->getMsrp();
+} elseif ($product->getTypeId() === \Magento\GroupedProduct\Model\Product\Type\Grouped::TYPE_CODE) {
+ $amount = $product->getTypeInstance()->getChildrenMsrp($product);
+} elseif ($product->getTypeId() === \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE) {
+ foreach ($product->getTypeInstance()->getUsedProducts($product) as $item) {
+ if ($item->getMsrp() !== null) {
+ $prices[] = $item->getMsrp();
+ }
+ }
+ $amount = $prices ? max($prices) : 0;
+}
+
$msrpPrice = $block->renderAmount(
- $priceType->getCustomAmount($product->getMsrp() ?: $product->getTypeInstance()->getChildrenMsrp($product)),
+ $priceType->getCustomAmount($amount),
[
'price_id' => $block->getPriceId() ? $block->getPriceId() : 'old-price-' . $productId,
'include_container' => false,
- 'skip_adjustments' => true
+ 'skip_adjustments' => true,
]
);
$priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElementIdPrefix() : 'product-price-';
-
-$addToCartUrl = '';
-if ($product->isSaleable()) {
- /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */
- $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton('Magento\Catalog\Block\Product\AbstractProduct');
- $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl(
- $product,
- ['_query' => [
- \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED =>
- $this->helper('Magento\Framework\Url\Helper\Data')->getEncodedUrl(
- $addToCartUrlGenerator->getAddToCartUrl($product)
- ),
- ]]
- );
-}
?>
-getMsrp()): ?>
+
+
= /* @escapeNotVerified */ $msrpPrice ?>
+ = /* @escapeNotVerified */ $msrpPrice ?>
isShowPriceOnGesture()): ?>
getIdSuffix();
- $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20);
- $data = ['addToCart' => [
- 'origin'=> 'msrp',
- 'popupId' => '#' . $popupId,
- 'productName' => $block->escapeJs($block->escapeHtml($product->getName())),
- 'productId' => $productId,
- 'productIdInput' => 'input[type="hidden"][name="product"]',
- 'realPrice' => $block->getRealPriceHtml(),
- 'isSaleable' => $product->isSaleable(),
- 'msrpPrice' => $msrpPrice,
- 'priceElementId' => $priceElementId,
- 'closeButtonId' => '#map-popup-close',
- 'addToCartUrl' => $addToCartUrl,
- 'paymentButtons' => '[data-label=or]'
- ]];
- if ($block->getRequest()->getFullActionName() === 'catalog_product_view') {
- $data['addToCart']['addToCartButton'] = '#product_addtocart_form [type=submit]';
- } else {
- $data['addToCart']['addToCartButton'] = sprintf(
- 'form:has(input[type="hidden"][name="product"][value="%s"]) button[type="submit"]',
- (int) $productId
- );
- }
+
+ $addToCartUrl = '';
+ if ($product->isSaleable()) {
+ /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */
+ $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton('Magento\Catalog\Block\Product\AbstractProduct');
+ $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl(
+ $product,
+ ['_query' => [
+ \Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED =>
+ $this->helper('Magento\Framework\Url\Helper\Data')->getEncodedUrl(
+ $addToCartUrlGenerator->getAddToCartUrl($product)
+ ),
+ ]]
+ );
+ }
+
+ $priceElementId = $priceElementIdPrefix . $productId . $block->getIdSuffix();
+ $popupId = 'msrp-popup-' . $productId . $block->getRandomString(20);
+ $data = ['addToCart' => [
+ 'origin' => 'msrp',
+ 'popupId' => '#' . $popupId,
+ 'productName' => $block->escapeJs($block->escapeHtml($product->getName())),
+ 'productId' => $productId,
+ 'productIdInput' => 'input[type="hidden"][name="product"]',
+ 'realPrice' => $block->getRealPriceHtml(),
+ 'isSaleable' => $product->isSaleable(),
+ 'msrpPrice' => $msrpPrice,
+ 'priceElementId' => $priceElementId,
+ 'closeButtonId' => '#map-popup-close',
+ 'addToCartUrl' => $addToCartUrl,
+ 'paymentButtons' => '[data-label=or]'
+ ]];
+ if ($block->getRequest()->getFullActionName() === 'catalog_product_view') {
+ $data['addToCart']['addToCartButton'] = '#product_addtocart_form [type=submit]';
+ } else {
+ $data['addToCart']['addToCartButton'] = sprintf(
+ 'form:has(input[type="hidden"][name="product"][value="%s"]) button[type="submit"]',
+ (int) $productId
+ );
+ }
?>
.ui-dialog-content').dropdownDialog('close');
}
+
+ e.preventDefault();
$(this.options.cartForm).submit();
+ },
+
+ /**
+ * Call on event updatePrice. Proxy to updateMsrpPrice method.
+ *
+ * @param {Event} event
+ * @param {mixed} priceIndex
+ * @param {Object} prices
+ */
+ onUpdateMsrpPrice: function onUpdateMsrpPrice(event, priceIndex, prices) {
+
+ var defaultMsrp,
+ defaultPrice,
+ msrpPrice,
+ finalPrice;
+
+ defaultMsrp = _.chain(prices).map(function (price) {
+ return price.msrpPrice.amount;
+ }).reject(function (p) {
+ return p === null;
+ }).max().value();
+
+ defaultPrice = _.chain(prices).map(function (p) {
+ return p.finalPrice.amount;
+ }).min().value();
+
+ if (typeof priceIndex !== 'undefined') {
+ msrpPrice = prices[priceIndex].msrpPrice.amount;
+ finalPrice = prices[priceIndex].finalPrice.amount;
+
+ if (msrpPrice === null || msrpPrice <= finalPrice) {
+ this.updateNonMsrpPrice(priceUtils.formatPrice(finalPrice));
+ } else {
+ this.updateMsrpPrice(
+ priceUtils.formatPrice(finalPrice),
+ priceUtils.formatPrice(msrpPrice),
+ false);
+ }
+ } else {
+ this.updateMsrpPrice(
+ priceUtils.formatPrice(defaultPrice),
+ priceUtils.formatPrice(defaultMsrp),
+ true);
+ }
+ },
+ /**
+ * Update prices for configurable product with MSRP enabled.
+ *
+ * @param {String} finalPrice
+ * @param {String} msrpPrice
+ * @param {Boolean} useDefaultPrice
+ */
+ updateMsrpPrice: function (finalPrice, msrpPrice, useDefaultPrice) {
+ var options = this.tierOptions || this.options;
+
+ $(this.options.fallbackPriceContainer).hide();
+ $(this.options.displayPriceContainer).show();
+ $(this.options.mapInfoLinks).show();
+
+ if (useDefaultPrice || !this.wasOpened) {
+ this.$popup.find(this.options.msrpLabelId).html(options.msrpPrice);
+ this.$popup.find(this.options.priceLabelId).html(options.realPrice);
+ $(this.options.displayPriceElement).html(msrpPrice);
+ this.wasOpened = true;
+ }
+
+ if (!useDefaultPrice) {
+ this.$popup.find(this.options.msrpPriceElement).html(msrpPrice);
+ this.$popup.find(this.options.priceElement).html(finalPrice);
+ $(this.options.displayPriceElement).html(msrpPrice);
+ }
+ },
+
+ /**
+ * Display non MAP price for irrelevant products.
+ *
+ * @param {String} price
+ */
+ updateNonMsrpPrice: function (price) {
+ $(this.options.fallbackPriceElement).html(price);
+ $(this.options.displayPriceContainer).hide();
+ $(this.options.mapInfoLinks).hide();
+ $(this.options.fallbackPriceContainer).show();
+ },
+
+ /**
+ * Handler for submit form.
+ *
+ * @private
+ */
+ _onSubmitForm: function () {
+ if ($(this.options.cartForm).valid()) {
+ $(this.options.cartButtonId).prop('disabled', true);
+ }
}
});
diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php
index ea7838085ddfb..0ea0abc135f9e 100644
--- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php
+++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php
@@ -1168,7 +1168,7 @@ private function removePlacedItemsFromQuote(array $shippingAddresses, array $pla
{
foreach ($shippingAddresses as $address) {
foreach ($address->getAllItems() as $addressItem) {
- if (in_array($addressItem->getId(), $placedAddressItems)) {
+ if (in_array($addressItem->getQuoteItemId(), $placedAddressItems)) {
if ($addressItem->getProduct()->getIsVirtual()) {
$addressItem->isDeleted(true);
} else {
@@ -1218,7 +1218,7 @@ private function searchQuoteAddressId(OrderInterface $order, array $addresses):
$item = array_pop($items);
foreach ($addresses as $address) {
foreach ($address->getAllItems() as $addressItem) {
- if ($addressItem->getId() == $item->getQuoteItemId()) {
+ if ($addressItem->getQuoteItemId() == $item->getQuoteItemId()) {
return (int)$address->getId();
}
}
diff --git a/app/code/Magento/Multishipping/composer.json b/app/code/Magento/Multishipping/composer.json
index d32bb1f5bd82a..48095da856e02 100644
--- a/app/code/Magento/Multishipping/composer.json
+++ b/app/code/Magento/Multishipping/composer.json
@@ -15,7 +15,7 @@
"magento/module-theme": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.4",
+ "version": "100.2.5",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml
index c6bcdeb7b0413..fee3cb790a522 100644
--- a/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml
+++ b/app/code/Magento/Multishipping/view/frontend/layout/multishipping_checkout_customer_address.xml
@@ -12,6 +12,7 @@
Magento\Customer\Block\DataProviders\AddressAttributeData
+ Magento\Customer\Block\DataProviders\PostCodesPatternsAttributeData
diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml
index d8514ca77f9c2..4354cfb7c1c3e 100644
--- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml
+++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml
@@ -55,6 +55,10 @@
= /* @noEscape */ $block->getCheckoutData()->getAddressHtml($block->getAddress()); ?>
+
@@ -79,36 +83,45 @@
if (isset($methodsForms[$code])) {
$block->setMethodFormTemplate($code, $methodsForms[$code]);
}
- ?>
-
- 1) : ?>
-
- checked="checked"
+ ?>
+
+
+ 1) : ?>
+
+ checked="checked"
+
+ class="radio"/>
+
+
- class="radio"/>
-
-
-
-
- = $block->escapeHtml($_method->getTitle()) ?>
-
-
- getChildHtml('payment.method.' . $code)) : ?>
-
- = /* @noEscape */ $html; ?>
-
-
+
+ = $block->escapeHtml($_method->getTitle()) ?>
+
+
+ getChildHtml('payment.method.' . $code)) : ?>
+
+ = /* @noEscape */ $html; ?>
+
+
+
= $block->getChildHtml('payment_methods_after') ?>
diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js
index 9b867cd7217b1..3a6d73e304974 100644
--- a/app/code/Magento/Multishipping/view/frontend/web/js/overview.js
+++ b/app/code/Magento/Multishipping/view/frontend/web/js/overview.js
@@ -15,7 +15,7 @@ define([
opacity: 0.5, // CSS opacity for the 'Place Order' button when it's clicked and then disabled.
pleaseWaitLoader: 'span.please-wait', // 'Submitting order information...' Ajax loader.
placeOrderSubmit: 'button[type="submit"]', // The 'Place Order' button.
- agreements: '#checkout-agreements' // Container for all of the checkout agreements and terms/conditions
+ agreements: '.checkout-agreements' // Container for all of the checkout agreements and terms/conditions
},
/**
diff --git a/app/code/Magento/NewRelicReporting/composer.json b/app/code/Magento/NewRelicReporting/composer.json
index abb2a200ed723..b256b25716c87 100644
--- a/app/code/Magento/NewRelicReporting/composer.json
+++ b/app/code/Magento/NewRelicReporting/composer.json
@@ -13,7 +13,7 @@
"magento/magento-composer-installer": "*"
},
"type": "magento2-module",
- "version": "100.2.5",
+ "version": "100.2.6",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php
index 7293b350fcd01..0f2192dc442db 100644
--- a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php
+++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php
@@ -9,18 +9,25 @@
namespace Magento\Newsletter\Controller\Adminhtml\Queue;
+use Magento\Framework\Exception\NotFoundException;
+
class Save extends \Magento\Newsletter\Controller\Adminhtml\Queue
{
/**
- * Save Newsletter queue
+ * Save newsletter queue.
*
- * @throws \Magento\Framework\Exception\LocalizedException
* @return void
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws NotFoundException
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function execute()
{
try {
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found'));
+ }
+
/* @var $queue \Magento\Newsletter\Model\Queue */
$queue = $this->_objectManager->create(\Magento\Newsletter\Model\Queue::class);
@@ -30,7 +37,9 @@ public function execute()
$template = $this->_objectManager->create(\Magento\Newsletter\Model\Template::class)->load($templateId);
if (!$template->getId() || $template->getIsSystem()) {
- throw new \Magento\Framework\Exception\LocalizedException(__('Please correct the newsletter template and try again.'));
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __('Please correct the newsletter template and try again.')
+ );
}
$queue->setTemplateId(
diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php
index 7f02e4ea13445..4794d86faa17a 100644
--- a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php
+++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php
@@ -6,11 +6,12 @@
*/
namespace Magento\Newsletter\Controller\Adminhtml\Subscriber;
-use Magento\Newsletter\Controller\Adminhtml\Subscriber;
use Magento\Backend\App\Action\Context;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\Response\Http\FileFactory;
+use Magento\Framework\Exception\NotFoundException;
+use Magento\Newsletter\Controller\Adminhtml\Subscriber;
use Magento\Newsletter\Model\SubscriberFactory;
-use Magento\Framework\App\ObjectManager;
class MassDelete extends Subscriber
{
@@ -36,12 +37,17 @@ public function __construct(
* Delete one or more subscribers action
*
* @return void
+ * @throws NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found.'));
+ }
+
$subscribersIds = $this->getRequest()->getParam('subscriber');
if (!is_array($subscribersIds)) {
- $this->messageManager->addError(__('Please select one or more subscribers.'));
+ $this->messageManager->addErrorMessage(__('Please select one or more subscribers.'));
} else {
try {
foreach ($subscribersIds as $subscriberId) {
@@ -50,9 +56,11 @@ public function execute()
);
$subscriber->delete();
}
- $this->messageManager->addSuccess(__('Total of %1 record(s) were deleted.', count($subscribersIds)));
+ $this->messageManager->addSuccessMessage(
+ __('Total of %1 record(s) were deleted.', count($subscribersIds))
+ );
} catch (\Exception $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
}
}
diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php
index b61494f795905..3b3ea0d4c67a0 100644
--- a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php
+++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php
@@ -6,6 +6,7 @@
*/
namespace Magento\Newsletter\Controller\Adminhtml\Subscriber;
+use Magento\Framework\Exception\NotFoundException;
use Magento\Newsletter\Controller\Adminhtml\Subscriber;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Response\Http\FileFactory;
@@ -37,9 +38,14 @@ public function __construct(
* Unsubscribe one or more subscribers action
*
* @return void
+ * @throws NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found.'));
+ }
+
$subscribersIds = $this->getRequest()->getParam('subscriber');
if (!is_array($subscribersIds)) {
$this->messageManager->addError(__('Please select one or more subscribers.'));
diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php
index d327d44feceb8..ac47f7e217b36 100644
--- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php
+++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php
@@ -1,6 +1,5 @@
getRequest()->isPost()) {
+ throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.'));
+ }
+
$template = $this->_objectManager->create(
\Magento\Newsletter\Model\Template::class
)->load(
diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php
index 52d46065ad05b..54a5eb651d99b 100644
--- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php
+++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Drop.php
@@ -6,15 +6,22 @@
*/
namespace Magento\Newsletter\Controller\Adminhtml\Template;
+use Magento\Framework\Exception\NotFoundException;
+
class Drop extends \Magento\Newsletter\Controller\Adminhtml\Template
{
/**
* Drop Newsletter Template
*
* @return void
+ * @throws NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found'));
+ }
+
$this->_view->loadLayout('newsletter_template_preview_popup');
$this->_view->renderLayout();
}
diff --git a/app/code/Magento/Newsletter/Controller/Manage/Save.php b/app/code/Magento/Newsletter/Controller/Manage/Save.php
index 419cbac10ffd1..1aa2a4505d518 100644
--- a/app/code/Magento/Newsletter/Controller/Manage/Save.php
+++ b/app/code/Magento/Newsletter/Controller/Manage/Save.php
@@ -8,8 +8,12 @@
namespace Magento\Newsletter\Controller\Manage;
use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository;
+use Magento\Customer\Model\Data\Customer;
use Magento\Newsletter\Model\Subscriber;
+/**
+ * Controller for customer newsletter subscription save.
+ */
class Save extends \Magento\Newsletter\Controller\Manage
{
/**
@@ -58,7 +62,7 @@ public function __construct(
}
/**
- * Save newsletter subscription preference action
+ * Save newsletter subscription preference action.
*
* @return void|null
*/
@@ -81,6 +85,8 @@ public function execute()
$isSubscribedParam = (boolean)$this->getRequest()
->getParam('is_subscribed', false);
if ($isSubscribedParam !== $isSubscribedState) {
+ // No need to validate customer and customer address while saving subscription preferences
+ $this->setIgnoreValidationFlag($customer);
$this->customerRepository->save($customer);
if ($isSubscribedParam) {
$subscribeModel = $this->subscriberFactory->create()
@@ -105,4 +111,15 @@ public function execute()
}
$this->_redirect('customer/account/');
}
+
+ /**
+ * Set ignore_validation_flag to skip unnecessary address and customer validation.
+ *
+ * @param Customer $customer
+ * @return void
+ */
+ private function setIgnoreValidationFlag(Customer $customer)
+ {
+ $customer->setData('ignore_validation_flag', true);
+ }
}
diff --git a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php
index 792dcf2fbe689..58b51009c205a 100644
--- a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php
+++ b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php
@@ -6,13 +6,12 @@
namespace Magento\Newsletter\Model\Plugin;
use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository;
-use Magento\Customer\Api\Data\CustomerExtensionInterface;
use Magento\Customer\Api\Data\CustomerInterface;
+use Magento\Newsletter\Model\SubscriberFactory;
use Magento\Framework\Api\ExtensionAttributesFactory;
-use Magento\Framework\App\ObjectManager;
use Magento\Newsletter\Model\ResourceModel\Subscriber;
-use Magento\Newsletter\Model\SubscriberFactory;
-use Magento\Store\Model\StoreManagerInterface;
+use Magento\Customer\Api\Data\CustomerExtensionInterface;
+use Magento\Framework\App\ObjectManager;
class CustomerPlugin
{
@@ -38,30 +37,22 @@ class CustomerPlugin
*/
private $customerSubscriptionStatus = [];
- /**
- * @var StoreManagerInterface
- */
- private $storeManager;
-
/**
* Initialize dependencies.
*
* @param SubscriberFactory $subscriberFactory
* @param ExtensionAttributesFactory|null $extensionFactory
* @param Subscriber|null $subscriberResource
- * @param StoreManagerInterface|null $storeManager
*/
public function __construct(
SubscriberFactory $subscriberFactory,
ExtensionAttributesFactory $extensionFactory = null,
- Subscriber $subscriberResource = null,
- StoreManagerInterface $storeManager = null
+ Subscriber $subscriberResource = null
) {
$this->subscriberFactory = $subscriberFactory;
$this->extensionFactory = $extensionFactory
?: ObjectManager::getInstance()->get(ExtensionAttributesFactory::class);
$this->subscriberResource = $subscriberResource ?: ObjectManager::getInstance()->get(Subscriber::class);
- $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class);
}
/**
@@ -158,8 +149,6 @@ public function afterDelete(CustomerRepository $subject, $result, CustomerInterf
public function afterGetById(CustomerRepository $subject, CustomerInterface $customer)
{
$extensionAttributes = $customer->getExtensionAttributes();
- $storeId = $this->storeManager->getStore()->getId();
- $customer->setStoreId($storeId);
if ($extensionAttributes === null) {
/** @var CustomerExtensionInterface $extensionAttributes */
$extensionAttributes = $this->extensionFactory->create(CustomerInterface::class);
diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php
index 48a89129f3098..2c6f494053efc 100644
--- a/app/code/Magento/Newsletter/Model/Subscriber.php
+++ b/app/code/Magento/Newsletter/Model/Subscriber.php
@@ -552,7 +552,7 @@ public function updateSubscription($customerId)
}
/**
- * Saving customer subscription status
+ * Saving customer subscription status.
*
* @param int $customerId
* @param bool $subscribe indicates whether the customer should be subscribed or unsubscribed
@@ -588,7 +588,12 @@ protected function _updateCustomerSubscription($customerId, $subscribe)
if (AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED
== $this->customerAccountManagement->getConfirmationStatus($customerId)
) {
- $status = self::STATUS_UNCONFIRMED;
+ if ($this->getId() && $this->getStatus() == self::STATUS_SUBSCRIBED) {
+ // if a customer was already subscribed then keep the subscribed
+ $status = self::STATUS_SUBSCRIBED;
+ } else {
+ $status = self::STATUS_UNCONFIRMED;
+ }
} elseif ($isConfirmNeed) {
if ($this->getStatus() != self::STATUS_SUBSCRIBED) {
$status = self::STATUS_NOT_ACTIVE;
diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/VerifySubscribedNewsletterDisplayedActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/VerifySubscribedNewsletterDisplayedActionGroup.xml
new file mode 100644
index 0000000000000..81b444fb5c1dc
--- /dev/null
+++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/VerifySubscribedNewsletterDisplayedActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Page/StorefrontNewsletterManagePage.xml b/app/code/Magento/Newsletter/Test/Mftf/Page/StorefrontNewsletterManagePage.xml
new file mode 100644
index 0000000000000..81fd3eb7c391c
--- /dev/null
+++ b/app/code/Magento/Newsletter/Test/Mftf/Page/StorefrontNewsletterManagePage.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml
new file mode 100644
index 0000000000000..36870fbfb0182
--- /dev/null
+++ b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml
new file mode 100644
index 0000000000000..15d6debd7ef25
--- /dev/null
+++ b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml
new file mode 100644
index 0000000000000..96a944a4952ac
--- /dev/null
+++ b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php
index 0bc79244bdf1c..39a9c2a0d95d2 100644
--- a/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php
+++ b/app/code/Magento/Newsletter/Test/Unit/Model/Plugin/CustomerPluginTest.php
@@ -10,8 +10,6 @@
use Magento\Customer\Api\Data\CustomerExtensionInterface;
use Magento\Framework\Api\ExtensionAttributesFactory;
use Magento\Newsletter\Model\ResourceModel\Subscriber;
-use Magento\Store\Model\Store;
-use Magento\Store\Model\StoreManagerInterface;
class CustomerPluginTest extends \PHPUnit\Framework\TestCase
{
@@ -55,11 +53,6 @@ class CustomerPluginTest extends \PHPUnit\Framework\TestCase
*/
private $customerMock;
- /**
- * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject
- */
- private $storeManagerMock;
-
protected function setUp()
{
$this->subscriberFactory = $this->getMockBuilder(\Magento\Newsletter\Model\SubscriberFactory::class)
@@ -94,8 +87,6 @@ protected function setUp()
->setMethods(["getExtensionAttributes"])
->disableOriginalConstructor()
->getMockForAbstractClass();
- $this->storeManagerMock = $this->createMock(StoreManagerInterface::class);
-
$this->subscriberFactory->expects($this->any())->method('create')->willReturn($this->subscriber);
$this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
$this->plugin = $this->objectManager->getObject(
@@ -103,8 +94,7 @@ protected function setUp()
[
'subscriberFactory' => $this->subscriberFactory,
'extensionFactory' => $this->extensionFactoryMock,
- 'subscriberResource' => $this->subscriberResourceMock,
- 'storeManager' => $this->storeManagerMock,
+ 'subscriberResource' => $this->subscriberResourceMock
]
);
}
@@ -208,7 +198,6 @@ public function testAfterGetByIdCreatesExtensionAttributesIfItIsNotSet(
) {
$subject = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class);
$subscriber = [$subscriberStatusKey => $subscriberStatusValue];
- $this->prepareStoreData();
$this->extensionFactoryMock->expects($this->any())
->method('create')
->willReturn($this->customerExtensionMock);
@@ -234,7 +223,6 @@ public function testAfterGetByIdSetsIsSubscribedFlagIfItIsNotSet()
{
$subject = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class);
$subscriber = ['subscriber_id' => 1, 'subscriber_status' => 1];
- $this->prepareStoreData();
$this->customerMock->expects($this->any())
->method('getExtensionAttributes')
->willReturn($this->customerExtensionMock);
@@ -267,17 +255,4 @@ public function afterGetByIdDataProvider()
[null, null, false]
];
}
-
- /**
- * Prepare store information
- *
- * @return void
- */
- private function prepareStoreData()
- {
- $storeId = 1;
- $storeMock = $this->createMock(Store::class);
- $storeMock->expects($this->any())->method('getId')->willReturn($storeId);
- $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock);
- }
}
diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php
index d8c770f02e8a7..0bf6f24a6b989 100644
--- a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php
+++ b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriberTest.php
@@ -209,6 +209,11 @@ public function testSubscribeNotLoggedIn()
$this->assertEquals(Subscriber::STATUS_NOT_ACTIVE, $this->subscriber->subscribe($email));
}
+ /**
+ * Update status with Confirmation Status - required.
+ *
+ * @return void
+ */
public function testUpdateSubscription()
{
$websiteId = 1;
@@ -225,7 +230,7 @@ public function testUpdateSubscription()
->willReturn(
[
'subscriber_id' => 1,
- 'subscriber_status' => Subscriber::STATUS_SUBSCRIBED
+ 'subscriber_status' => Subscriber::STATUS_SUBSCRIBED,
]
);
$customerDataMock->expects($this->atLeastOnce())->method('getId')->willReturn('id');
@@ -245,8 +250,9 @@ public function testUpdateSubscription()
->getMock();
$this->storeManager->expects($this->any())->method('getStore')->willReturn($storeModel);
$storeModel->expects($this->exactly(2))->method('getWebsiteId')->willReturn($websiteId);
+ $data = $this->subscriber->updateSubscription($customerId);
- $this->assertEquals($this->subscriber, $this->subscriber->updateSubscription($customerId));
+ $this->assertEquals(Subscriber::STATUS_SUBSCRIBED, $data->getSubscriberStatus());
}
public function testUnsubscribeCustomerById()
diff --git a/app/code/Magento/Newsletter/composer.json b/app/code/Magento/Newsletter/composer.json
index a97d0bca5634d..9e02676e2488c 100644
--- a/app/code/Magento/Newsletter/composer.json
+++ b/app/code/Magento/Newsletter/composer.json
@@ -14,7 +14,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.6",
+ "version": "100.2.7",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml
index eeca4fabd348d..b2a1c8d8c7208 100644
--- a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml
+++ b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml
@@ -34,9 +34,10 @@ require([
'tinymce',
'Magento_Ui/js/modal/prompt',
'Magento_Ui/js/modal/confirm',
+ 'mage/dataPost',
'mage/mage',
'prototype'
-], function(jQuery, tinyMCE, prompt, confirm){
+], function(jQuery, tinyMCE, prompt, confirm, dataPost){
//escapeJs($block->escapeHtml(__('Are you sure you want to delete this template?'))) ?>",
actions: {
confirm: function() {
- window.location.href = '= $block->escapeUrl($block->getDeleteUrl()) ?>';
+ dataPost().postData({
+ action: '= $block->escapeUrl($block->getDeleteUrl()) ?>',
+ data: {}
+ });
}
}
});
diff --git a/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml
new file mode 100644
index 0000000000000..4d63577319d5b
--- /dev/null
+++ b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml
@@ -0,0 +1,26 @@
+
+= $block->escapeHtml($block->getMethod()->getTitle()) ?>
+ {{pdf_row_separator}}
+getInfo()->getAdditionalInformation()): ?>
+ {{pdf_row_separator}}
+ getPayableTo()): ?>
+ = $block->escapeHtml(__('Make Check payable to: %1', $block->getPayableTo())) ?>
+ {{pdf_row_separator}}
+
+ getMailingAddress()): ?>
+ = $block->escapeHtml(__('Send Check to:')) ?>
+ {{pdf_row_separator}}
+ = /* @noEscape */ nl2br($block->escapeHtml($block->getMailingAddress())) ?>
+ {{pdf_row_separator}}
+
+
diff --git a/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml
new file mode 100644
index 0000000000000..4a6ea1c00b21c
--- /dev/null
+++ b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml
@@ -0,0 +1,11 @@
+
+= $block->escapeHtml(__('Purchase Order Number: %1', $block->getInfo()->getPoNumber())) ?>
+ {{pdf_row_separator}}
diff --git a/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml b/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml
new file mode 100644
index 0000000000000..32810ecef20da
--- /dev/null
+++ b/app/code/Magento/OfflinePayments/view/frontend/layout/multishipping_checkout_billing.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ - Magento_OfflinePayments::multishipping/checkmo_form.phtml
+
+
+
+
+
diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml
new file mode 100644
index 0000000000000..b96918243a7a7
--- /dev/null
+++ b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml
@@ -0,0 +1,28 @@
+
+
diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php b/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php
index 2373b5285ed00..0fc56c7136327 100644
--- a/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php
+++ b/app/code/Magento/OfflineShipping/Model/Carrier/Freeshipping.php
@@ -80,9 +80,8 @@ public function collectRates(RateRequest $request)
$this->_updateFreeMethodQuote($request);
- if ($request->getFreeShipping() || $request->getBaseSubtotalInclTax() >= $this->getConfigData(
- 'free_shipping_subtotal'
- )
+ if ($request->getFreeShipping()
+ || ($request->getPackageValueWithDiscount() >= $this->getConfigData('free_shipping_subtotal'))
) {
/** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */
$method = $this->_rateMethodFactory->create();
diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php
index 8d75cc32914b4..635d0c636ca36 100644
--- a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php
+++ b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php
@@ -13,6 +13,11 @@
class ImportTest extends \PHPUnit\Framework\TestCase
{
+ /**
+ * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $escaperMock;
+
/**
* @var \Magento\OfflineShipping\Block\Adminhtml\Form\Field\Import
*/
@@ -29,11 +34,16 @@ protected function setUp()
\Magento\Framework\Data\Form::class,
['getFieldNameSuffix', 'addSuffixToName', 'getHtmlIdPrefix', 'getHtmlIdSuffix']
);
+ $this->escaperMock = $this->createMock(\Magento\Framework\Escaper::class);
+ $this->escaperMock->method('escapeHtml')->willReturnArgument(0);
$testData = ['name' => 'test_name', 'html_id' => 'test_html_id'];
$testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
$this->_object = $testHelper->getObject(
\Magento\OfflineShipping\Block\Adminhtml\Form\Field\Import::class,
- ['data' => $testData]
+ [
+ 'escaper' => $this->escaperMock,
+ 'data' => $testData,
+ ]
);
$this->_object->setForm($this->_formMock);
}
diff --git a/app/code/Magento/OfflineShipping/composer.json b/app/code/Magento/OfflineShipping/composer.json
index e04816a3b1128..416181733dc07 100644
--- a/app/code/Magento/OfflineShipping/composer.json
+++ b/app/code/Magento/OfflineShipping/composer.json
@@ -19,7 +19,7 @@
"magento/module-offline-shipping-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "100.2.5",
+ "version": "100.2.6",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php
new file mode 100644
index 0000000000000..7a1cc8934c017
--- /dev/null
+++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php
@@ -0,0 +1,108 @@
+cacheManager = $cacheManager;
+ $this->pageCacheStateStorage = $pageCacheStateStorage;
+ }
+
+ /**
+ * Switches Full Page Cache.
+ *
+ * Depending on enabling or disabling Maintenance Mode it turns off or restores Full Page Cache state.
+ *
+ * @param Observer $observer
+ * @return void
+ */
+ public function execute(Observer $observer)
+ {
+ if ($observer->getData('isOn')) {
+ $this->pageCacheStateStorage->save($this->isFullPageCacheEnabled());
+ $this->turnOffFullPageCache();
+ } else {
+ $this->restoreFullPageCacheState();
+ }
+ }
+
+ /**
+ * Turns off Full Page Cache.
+ *
+ * @return void
+ */
+ private function turnOffFullPageCache()
+ {
+ if (!$this->isFullPageCacheEnabled()) {
+ return;
+ }
+
+ $this->cacheManager->clean([PageCacheType::TYPE_IDENTIFIER]);
+ $this->cacheManager->setEnabled([PageCacheType::TYPE_IDENTIFIER], false);
+ }
+
+ /**
+ * Full Page Cache state.
+ *
+ * @return bool
+ */
+ private function isFullPageCacheEnabled(): bool
+ {
+ $cacheStatus = $this->cacheManager->getStatus();
+
+ if (!array_key_exists(PageCacheType::TYPE_IDENTIFIER, $cacheStatus)) {
+ return false;
+ }
+
+ return (bool)$cacheStatus[PageCacheType::TYPE_IDENTIFIER];
+ }
+
+ /**
+ * Restores Full Page Cache state.
+ *
+ * Returns FPC to previous state that was before maintenance mode turning on.
+ *
+ * @return void
+ */
+ private function restoreFullPageCacheState()
+ {
+ $storedPageCacheState = $this->pageCacheStateStorage->isEnabled();
+ $this->pageCacheStateStorage->flush();
+
+ if ($storedPageCacheState) {
+ $this->cacheManager->setEnabled([PageCacheType::TYPE_IDENTIFIER], true);
+ }
+ }
+}
diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php
new file mode 100644
index 0000000000000..4180885fcbc54
--- /dev/null
+++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php
@@ -0,0 +1,74 @@
+flagDir = $fileSystem->getDirectoryWrite(DirectoryList::VAR_DIR);
+ }
+
+ /**
+ * Saves Full Page Cache state.
+ *
+ * Saves FPC state across requests.
+ *
+ * @param bool $state
+ * @return void
+ */
+ public function save(bool $state)
+ {
+ $this->flagDir->writeFile(self::PAGE_CACHE_STATE_FILENAME, (string)$state);
+ }
+
+ /**
+ * Returns stored Full Page Cache state.
+ *
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ if (!$this->flagDir->isExist(self::PAGE_CACHE_STATE_FILENAME)) {
+ return false;
+ }
+
+ return (bool)$this->flagDir->readFile(self::PAGE_CACHE_STATE_FILENAME);
+ }
+
+ /**
+ * Flushes Page Cache state storage.
+ *
+ * @return void
+ */
+ public function flush()
+ {
+ $this->flagDir->delete(self::PAGE_CACHE_STATE_FILENAME);
+ }
+}
diff --git a/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php b/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php
new file mode 100644
index 0000000000000..8c4661cddd44c
--- /dev/null
+++ b/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php
@@ -0,0 +1,161 @@
+cacheManager = $this->createMock(Manager::class);
+ $this->pageCacheStateStorage = $this->createMock(PageCacheState::class);
+ $this->observer = $this->createMock(Observer::class);
+
+ $this->model = $objectManager->getObject(SwitchPageCacheOnMaintenance::class, [
+ 'cacheManager' => $this->cacheManager,
+ 'pageCacheStateStorage' => $this->pageCacheStateStorage,
+ ]);
+ }
+
+ /**
+ * Tests execute when setting maintenance mode to on.
+ *
+ * @param array $cacheStatus
+ * @param bool $cacheState
+ * @param int $flushCacheCalls
+ * @return void
+ * @dataProvider enablingPageCacheStateProvider
+ */
+ public function testExecuteWhileMaintenanceEnabling(array $cacheStatus, bool $cacheState, int $flushCacheCalls)
+ {
+ $this->observer->method('getData')
+ ->with('isOn')
+ ->willReturn(true);
+ $this->cacheManager->method('getStatus')
+ ->willReturn($cacheStatus);
+
+ // Page Cache state will be stored.
+ $this->pageCacheStateStorage->expects($this->once())
+ ->method('save')
+ ->with($cacheState);
+
+ // Page Cache will be cleaned and disabled
+ $this->cacheManager->expects($this->exactly($flushCacheCalls))
+ ->method('clean')
+ ->with([PageCacheType::TYPE_IDENTIFIER]);
+ $this->cacheManager->expects($this->exactly($flushCacheCalls))
+ ->method('setEnabled')
+ ->with([PageCacheType::TYPE_IDENTIFIER], false);
+
+ $this->model->execute($this->observer);
+ }
+
+ /**
+ * Tests execute when setting Maintenance Mode to off.
+ *
+ * @param bool $storedCacheState
+ * @param int $enableCacheCalls
+ * @return void
+ * @dataProvider disablingPageCacheStateProvider
+ */
+ public function testExecuteWhileMaintenanceDisabling(bool $storedCacheState, int $enableCacheCalls)
+ {
+ $this->observer->method('getData')
+ ->with('isOn')
+ ->willReturn(false);
+
+ $this->pageCacheStateStorage->method('isEnabled')
+ ->willReturn($storedCacheState);
+
+ // Nullify Page Cache state.
+ $this->pageCacheStateStorage->expects($this->once())
+ ->method('flush');
+
+ // Page Cache will be enabled.
+ $this->cacheManager->expects($this->exactly($enableCacheCalls))
+ ->method('setEnabled')
+ ->with([PageCacheType::TYPE_IDENTIFIER]);
+
+ $this->model->execute($this->observer);
+ }
+
+ /**
+ * Page Cache state data provider.
+ *
+ * @return array
+ */
+ public function enablingPageCacheStateProvider(): array
+ {
+ return [
+ 'page_cache_is_enable' => [
+ 'cache_status' => [PageCacheType::TYPE_IDENTIFIER => 1],
+ 'cache_state' => true,
+ 'flush_cache_calls' => 1,
+ ],
+ 'page_cache_is_missing_in_system' => [
+ 'cache_status' => [],
+ 'cache_state' => false,
+ 'flush_cache_calls' => 0,
+ ],
+ 'page_cache_is_disable' => [
+ 'cache_status' => [PageCacheType::TYPE_IDENTIFIER => 0],
+ 'cache_state' => false,
+ 'flush_cache_calls' => 0,
+ ],
+ ];
+ }
+
+ /**
+ * Page Cache state data provider.
+ *
+ * @return array
+ */
+ public function disablingPageCacheStateProvider(): array
+ {
+ return [
+ ['stored_cache_state' => true, 'enable_cache_calls' => 1],
+ ['stored_cache_state' => false, 'enable_cache_calls' => 0],
+ ];
+ }
+}
diff --git a/app/code/Magento/PageCache/composer.json b/app/code/Magento/PageCache/composer.json
index 2b6b62aef2c47..e83801344dae7 100644
--- a/app/code/Magento/PageCache/composer.json
+++ b/app/code/Magento/PageCache/composer.json
@@ -9,7 +9,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.4",
+ "version": "100.2.5",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/PageCache/etc/events.xml b/app/code/Magento/PageCache/etc/events.xml
index 7584f5f36d69c..3f0a2532ae60a 100644
--- a/app/code/Magento/PageCache/etc/events.xml
+++ b/app/code/Magento/PageCache/etc/events.xml
@@ -57,4 +57,7 @@
+
+
+
diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl
index 793f8f81a03f9..21f48ef76502f 100644
--- a/app/code/Magento/PageCache/etc/varnish4.vcl
+++ b/app/code/Magento/PageCache/etc/varnish4.vcl
@@ -91,10 +91,11 @@ sub vcl_recv {
}
}
- # Remove Google gclid parameters to minimize the cache objects
- set req.url = regsuball(req.url,"\?gclid=[^&]+$",""); # strips when QS = "?gclid=AAA"
- set req.url = regsuball(req.url,"\?gclid=[^&]+&","?"); # strips when QS = "?gclid=AAA&foo=bar"
- set req.url = regsuball(req.url,"&gclid=[^&]+",""); # strips when QS = "?foo=bar&gclid=AAA" or QS = "?foo=bar&gclid=AAA&bar=baz"
+ # Remove all marketing get parameters to minimize the cache objects
+ if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") {
+ set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", "");
+ set req.url = regsub(req.url, "[?|&]+$", "");
+ }
# Static files caching
if (req.url ~ "^/(pub/)?(media|static)/") {
diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl
index 4dce6356d1e73..23df172dd3aa4 100644
--- a/app/code/Magento/PageCache/etc/varnish5.vcl
+++ b/app/code/Magento/PageCache/etc/varnish5.vcl
@@ -92,10 +92,11 @@ sub vcl_recv {
}
}
- # Remove Google gclid parameters to minimize the cache objects
- set req.url = regsuball(req.url,"\?gclid=[^&]+$",""); # strips when QS = "?gclid=AAA"
- set req.url = regsuball(req.url,"\?gclid=[^&]+&","?"); # strips when QS = "?gclid=AAA&foo=bar"
- set req.url = regsuball(req.url,"&gclid=[^&]+",""); # strips when QS = "?foo=bar&gclid=AAA" or QS = "?foo=bar&gclid=AAA&bar=baz"
+ # Remove all marketing get parameters to minimize the cache objects
+ if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") {
+ set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", "");
+ set req.url = regsub(req.url, "[?|&]+$", "");
+ }
# Static files caching
if (req.url ~ "^/(pub/)?(media|static)/") {
diff --git a/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php b/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php
new file mode 100644
index 0000000000000..c658baece7779
--- /dev/null
+++ b/app/code/Magento/Payment/Api/Data/PaymentAdditionalInfoInterface.php
@@ -0,0 +1,16 @@
+getPaymentMethods() as $code => $data) {
- if (isset($data['title'])) {
- $methods[$code] = $data['title'];
- } else {
- $methods[$code] = $this->getMethodInstance($code)->getConfigData('title', $store);
+ if (!empty($data['active'])) {
+ $storedTitle = $this->getMethodInstance($code)->getConfigData('title', $store);
+ if (!empty($storedTitle)) {
+ $methods[$code] = $storedTitle;
+ } elseif (!empty($data['title'])) {
+ $methods[$code] = $data['title'];
+ }
}
if ($asLabelValue && $withGroups && isset($data['group'])) {
$groupRelations[$code] = $data['group'];
diff --git a/app/code/Magento/Payment/Model/CcConfigProvider.php b/app/code/Magento/Payment/Model/CcConfigProvider.php
index 15bdd0072a51a..497ce93c30c71 100644
--- a/app/code/Magento/Payment/Model/CcConfigProvider.php
+++ b/app/code/Magento/Payment/Model/CcConfigProvider.php
@@ -44,7 +44,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getConfig()
{
@@ -69,7 +69,7 @@ public function getIcons()
}
$types = $this->ccConfig->getCcAvailableTypes();
- foreach (array_keys($types) as $code) {
+ foreach ($types as $code => $label) {
if (!array_key_exists($code, $this->icons)) {
$asset = $this->ccConfig->createAsset('Magento_Payment::images/cc/' . strtolower($code) . '.png');
$placeholder = $this->assetSource->findSource($asset);
@@ -78,7 +78,8 @@ public function getIcons()
$this->icons[$code] = [
'url' => $asset->getUrl(),
'width' => $width,
- 'height' => $height
+ 'height' => $height,
+ 'title' => __($label),
];
}
}
diff --git a/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php b/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php
new file mode 100644
index 0000000000000..4ce41181a008a
--- /dev/null
+++ b/app/code/Magento/Payment/Model/PaymentAdditionalInfo.php
@@ -0,0 +1,60 @@
+key;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setKey($key)
+ {
+ $this->key = $key;
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setValue($value)
+ {
+ $this->value = $value;
+ return $this;
+ }
+}
diff --git a/app/code/Magento/Payment/Test/Unit/Helper/DataTest.php b/app/code/Magento/Payment/Test/Unit/Helper/DataTest.php
index 3752e82fd1e5b..1df07f87a3054 100644
--- a/app/code/Magento/Payment/Test/Unit/Helper/DataTest.php
+++ b/app/code/Magento/Payment/Test/Unit/Helper/DataTest.php
@@ -18,6 +18,9 @@ class DataTest extends \PHPUnit\Framework\TestCase
/** @var \PHPUnit_Framework_MockObject_MockObject */
private $scopeConfig;
+ /** @var \PHPUnit_Framework_MockObject_MockObject */
+ private $paymentConfig;
+
/** @var \PHPUnit_Framework_MockObject_MockObject */
private $initialConfig;
@@ -48,6 +51,7 @@ protected function setUp()
$this->methodFactory = $arguments['paymentMethodFactory'];
$this->appEmulation = $arguments['appEmulation'];
+ $this->paymentConfig = $arguments['paymentConfig'];
$this->initialConfig = $arguments['initialConfig'];
$this->helper = $objectManagerHelper->getObject($className, $arguments);
@@ -253,6 +257,56 @@ public function testGetInfoBlockHtml()
$this->assertEquals($blockHtml, $this->helper->getInfoBlockHtml($infoMock, $storeId));
}
+ /**
+ * @param bool $sorted
+ * @param bool $asLabelValue
+ * @param bool $withGroups
+ * @param string|null $configTitle
+ * @param array $paymentMethod
+ * @param array $expectedPaymentMethodList
+ * @return void
+ *
+ * @dataProvider paymentMethodListDataProvider
+ */
+ public function testGetPaymentMethodList(
+ bool $sorted,
+ bool $asLabelValue,
+ bool $withGroups,
+ $configTitle,
+ array $paymentMethod,
+ array $expectedPaymentMethodList
+ ) {
+ $groups = ['group' => 'Group Title'];
+
+ $this->initialConfig->method('getData')
+ ->with('default')
+ ->willReturn(
+ [
+ Data::XML_PATH_PAYMENT_METHODS => [
+ $paymentMethod['code'] => $paymentMethod['data'],
+ ],
+ ]
+ );
+
+ $this->scopeConfig->method('getValue')
+ ->with(sprintf('%s/%s/model', Data::XML_PATH_PAYMENT_METHODS, $paymentMethod['code']))
+ ->willReturn(\Magento\Payment\Model\Method\AbstractMethod::class);
+
+ $methodInstanceMock = $this->getMockBuilder(\Magento\Payment\Model\MethodInterface::class)
+ ->getMockForAbstractClass();
+ $methodInstanceMock->method('getConfigData')
+ ->with('title', null)
+ ->willReturn($configTitle);
+ $this->methodFactory->method('create')
+ ->willReturn($methodInstanceMock);
+
+ $this->paymentConfig->method('getGroups')
+ ->willReturn($groups);
+
+ $paymentMethodList = $this->helper->getPaymentMethodList($sorted, $asLabelValue, $withGroups);
+ $this->assertEquals($expectedPaymentMethodList, $paymentMethodList);
+ }
+
/**
* @return array
*/
@@ -269,4 +323,89 @@ public function getSortMethodsDataProvider()
]
];
}
+
+ /**
+ * @return array
+ */
+ public function paymentMethodListDataProvider(): array
+ {
+ return [
+ 'Payment method with changed title' =>
+ [
+ true,
+ false,
+ false,
+ 'Config Payment Title',
+ [
+ 'code' => 'payment_method',
+ 'data' => [
+ 'active' => 1,
+ 'title' => 'Payment Title',
+ ],
+ ],
+ ['payment_method' => 'Config Payment Title'],
+ ],
+ 'Payment method with default title' =>
+ [
+ true,
+ false,
+ false,
+ null,
+ [
+ 'code' => 'payment_method',
+ 'data' => [
+ 'active' => 1,
+ 'title' => 'Payment Title',
+ ],
+ ],
+ ['payment_method' => 'Payment Title'],
+ ],
+ 'Payment method as value => label' =>
+ [
+ true,
+ true,
+ false,
+ null,
+ [
+ 'code' => 'payment_method',
+ 'data' => [
+ 'active' => 1,
+ 'title' => 'Payment Title',
+ ],
+ ],
+ [
+ 'payment_method' => [
+ 'value' => 'payment_method',
+ 'label' => 'Payment Title',
+ ],
+ ],
+ ],
+ 'Payment method with group' =>
+ [
+ true,
+ true,
+ true,
+ null,
+ [
+ 'code' => 'payment_method',
+ 'data' => [
+ 'active' => 1,
+ 'title' => 'Payment Title',
+ 'group' => 'group',
+ ],
+ ],
+ [
+ 'group' => [
+ 'label' => 'Group Title',
+ 'value' => [
+ 'payment_method' => [
+ 'value' => 'payment_method',
+ 'label' => 'Payment Title',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
}
diff --git a/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php b/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php
index a8856166995fc..ff6aea44645cf 100644
--- a/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php
+++ b/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php
@@ -42,12 +42,14 @@ public function testGetConfig()
'vi' => [
'url' => 'http://cc.card/vi.png',
'width' => getimagesize($imagesDirectoryPath . 'vi.png')[0],
- 'height' => getimagesize($imagesDirectoryPath . 'vi.png')[1]
+ 'height' => getimagesize($imagesDirectoryPath . 'vi.png')[1],
+ 'title' => __('Visa'),
],
'ae' => [
'url' => 'http://cc.card/ae.png',
'width' => getimagesize($imagesDirectoryPath . 'ae.png')[0],
- 'height' => getimagesize($imagesDirectoryPath . 'ae.png')[1]
+ 'height' => getimagesize($imagesDirectoryPath . 'ae.png')[1],
+ 'title' => __('American Express'),
]
]
]
@@ -56,11 +58,13 @@ public function testGetConfig()
$ccAvailableTypesMock = [
'vi' => [
+ 'title' => 'Visa',
'fileId' => 'Magento_Payment::images/cc/vi.png',
'path' => $imagesDirectoryPath . 'vi.png',
'url' => 'http://cc.card/vi.png'
],
'ae' => [
+ 'title' => 'American Express',
'fileId' => 'Magento_Payment::images/cc/ae.png',
'path' => $imagesDirectoryPath . 'ae.png',
'url' => 'http://cc.card/ae.png'
@@ -68,7 +72,11 @@ public function testGetConfig()
];
$assetMock = $this->createMock(\Magento\Framework\View\Asset\File::class);
- $this->ccConfigMock->expects($this->once())->method('getCcAvailableTypes')->willReturn($ccAvailableTypesMock);
+ $this->ccConfigMock->expects($this->once())->method('getCcAvailableTypes')
+ ->willReturn(array_combine(
+ array_keys($ccAvailableTypesMock),
+ array_column($ccAvailableTypesMock, 'title')
+ ));
$this->ccConfigMock->expects($this->atLeastOnce())
->method('createAsset')
diff --git a/app/code/Magento/Payment/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php b/app/code/Magento/Payment/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php
index 63ca1b47dc08c..75916dd2ea99b 100644
--- a/app/code/Magento/Payment/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php
+++ b/app/code/Magento/Payment/Test/Unit/Observer/SalesOrderBeforeSaveObserverTest.php
@@ -160,7 +160,7 @@ public function testSalesOrderBeforeSaveSetForced()
* The method should check that the payment is available, as this is not always the case.
*
* @expectedException \Magento\Framework\Exception\LocalizedException
- * @exceptedExceptionMessage Please provide payment for the order.
+ * @expectedExceptionMessage Please provide payment for the order.
*/
public function testDoesNothingWhenNoPaymentIsAvailable()
{
diff --git a/app/code/Magento/Payment/composer.json b/app/code/Magento/Payment/composer.json
index b5e7ecdfdaff9..2f00b878417bd 100644
--- a/app/code/Magento/Payment/composer.json
+++ b/app/code/Magento/Payment/composer.json
@@ -12,7 +12,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.5",
+ "version": "100.2.6",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Payment/etc/di.xml b/app/code/Magento/Payment/etc/di.xml
index 74f553cc64094..b7422bb00d543 100644
--- a/app/code/Magento/Payment/etc/di.xml
+++ b/app/code/Magento/Payment/etc/di.xml
@@ -7,6 +7,7 @@
-->
+
diff --git a/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml b/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml
new file mode 100644
index 0000000000000..7acac62f65d38
--- /dev/null
+++ b/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml
@@ -0,0 +1,23 @@
+
+= $block->escapeHtml($block->getMethod()->getTitle()) ?>{{pdf_row_separator}}
+
+getSpecificInformation()):?>
+ $value):?>
+ = $block->escapeHtml($label) ?>:
+ = $block->escapeHtml(implode(' ', $block->getValueAsArray($value))) ?>
+ {{pdf_row_separator}}
+
+
+
+= $block->escapeHtml(implode('{{pdf_row_separator}}', $block->getChildPdfAsArray())) ?>
diff --git a/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js b/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js
index 5eba4fd89d338..1e352e4297131 100644
--- a/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js
+++ b/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js
@@ -114,8 +114,12 @@ define([
* @override
*/
placeOrder: function () {
- if (this.validateHandler() && additionalValidators.validate()) {
+ var self = this;
+ if (this.validateHandler() &&
+ additionalValidators.validate() &&
+ this.isPlaceOrderActionAllowed() === true
+ ) {
fullScreenLoader.startLoader();
this.isPlaceOrderActionAllowed(false);
@@ -127,8 +131,15 @@ define([
method: this.getCode()
}
)
- ).done(this.done.bind(this))
- .fail(this.fail.bind(this));
+ ).done(
+ this.done.bind(this)
+ ).fail(
+ this.fail.bind(this)
+ ).always(
+ function () {
+ self.isPlaceOrderActionAllowed(true);
+ }
+ );
this.initTimeoutHandler();
}
@@ -192,7 +203,6 @@ define([
*/
fail: function () {
fullScreenLoader.stopLoader();
- this.isPlaceOrderActionAllowed(true);
return this;
},
diff --git a/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php b/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php
index 2efae34a96459..fc257e264d680 100644
--- a/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php
+++ b/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php
@@ -6,11 +6,14 @@
namespace Magento\Paypal\Controller\Transparent;
use Magento\Framework\App\Action\Context;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Controller\Result\Json;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Controller\ResultInterface;
+use Magento\Framework\Data\Form\FormKey\Validator;
use Magento\Framework\Session\Generic;
use Magento\Framework\Session\SessionManager;
+use Magento\Framework\Session\SessionManagerInterface;
use Magento\Paypal\Model\Payflow\Service\Request\SecureToken;
use Magento\Paypal\Model\Payflow\Transparent;
use Magento\Quote\Model\Quote;
@@ -39,7 +42,7 @@ class RequestSecureToken extends \Magento\Framework\App\Action\Action
private $secureTokenService;
/**
- * @var SessionManager
+ * @var SessionManager|SessionManagerInterface
*/
private $sessionManager;
@@ -48,6 +51,11 @@ class RequestSecureToken extends \Magento\Framework\App\Action\Action
*/
private $transparent;
+ /**
+ * @var Validator
+ */
+ private $formKeyValidator;
+
/**
* @param Context $context
* @param JsonFactory $resultJsonFactory
@@ -55,6 +63,8 @@ class RequestSecureToken extends \Magento\Framework\App\Action\Action
* @param SecureToken $secureTokenService
* @param SessionManager $sessionManager
* @param Transparent $transparent
+ * @param SessionManagerInterface|null $sessionInterface
+ * @param Validator $formKeyValidator
*/
public function __construct(
Context $context,
@@ -62,13 +72,17 @@ public function __construct(
Generic $sessionTransparent,
SecureToken $secureTokenService,
SessionManager $sessionManager,
- Transparent $transparent
+ Transparent $transparent,
+ SessionManagerInterface $sessionInterface = null,
+ Validator $formKeyValidator = null
) {
$this->resultJsonFactory = $resultJsonFactory;
$this->sessionTransparent = $sessionTransparent;
$this->secureTokenService = $secureTokenService;
- $this->sessionManager = $sessionManager;
+ $this->sessionManager = $sessionInterface ?: $sessionManager;
$this->transparent = $transparent;
+ $this->formKeyValidator = $formKeyValidator ?: ObjectManager::getInstance()->get(Validator::class);
+
parent::__construct($context);
}
@@ -82,8 +96,9 @@ public function execute()
/** @var Quote $quote */
$quote = $this->sessionManager->getQuote();
- if (!$quote or !$quote instanceof Quote) {
- return $this->getErrorResponse();
+ if (!$quote || !$quote instanceof Quote || !$this->formKeyValidator->validate($this->getRequest())
+ || !$this->getRequest()->isPost()) {
+ return $this->getErrorResponse();
}
$this->sessionTransparent->setQuoteId($quote->getId());
@@ -106,6 +121,8 @@ public function execute()
}
/**
+ * Get error response.
+ *
* @return Json
*/
private function getErrorResponse()
diff --git a/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php b/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php
index c2056aea08c00..5d5db0128b1eb 100644
--- a/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php
+++ b/app/code/Magento/Paypal/Model/Config/Structure/Element/FieldPlugin.php
@@ -5,7 +5,6 @@
*/
namespace Magento\Paypal\Model\Config\Structure\Element;
-use Magento\Framework\App\RequestInterface;
use Magento\Config\Model\Config\Structure\Element\Field as FieldConfigStructure;
use Magento\Paypal\Model\Config\StructurePlugin as ConfigStructurePlugin;
@@ -14,19 +13,6 @@
*/
class FieldPlugin
{
- /**
- * @var RequestInterface
- */
- private $request;
-
- /**
- * @param RequestInterface $request
- */
- public function __construct(RequestInterface $request)
- {
- $this->request = $request;
- }
-
/**
* Get original configPath (not changed by PayPal configuration inheritance)
*
@@ -36,7 +22,7 @@ public function __construct(RequestInterface $request)
*/
public function afterGetConfigPath(FieldConfigStructure $subject, $result)
{
- if (!$result && $this->request->getParam('section') == 'payment') {
+ if (!$result && strpos($subject->getPath(), 'payment_') === 0) {
$result = preg_replace(
'@^(' . implode('|', ConfigStructurePlugin::getPaypalConfigCountries(true)) . ')/@',
'payment/',
diff --git a/app/code/Magento/Paypal/Model/Express.php b/app/code/Magento/Paypal/Model/Express.php
index 196e59c6593b9..bae391058810c 100644
--- a/app/code/Magento/Paypal/Model/Express.php
+++ b/app/code/Magento/Paypal/Model/Express.php
@@ -17,10 +17,12 @@
use Magento\Store\Model\ScopeInterface;
/**
- * PayPal Express Module
+ * PayPal Express Module.
+ *
* @method \Magento\Quote\Api\Data\PaymentMethodExtensionInterface getExtensionAttributes()
* @SuppressWarnings(PHPMD.TooManyFields)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class Express extends \Magento\Payment\Model\Method\AbstractMethod
{
@@ -44,7 +46,7 @@ class Express extends \Magento\Payment\Model\Method\AbstractMethod
*
* @var bool
*/
- protected $_isGateway = false;
+ protected $_isGateway = true;
/**
* Availability option
@@ -179,6 +181,11 @@ class Express extends \Magento\Payment\Model\Method\AbstractMethod
*/
protected $transactionBuilder;
+ /**
+ * @var string
+ */
+ private static $authorizationExpiredCode = 10601;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -269,6 +276,7 @@ protected function _setApiProcessableErrors()
ApiProcessableException::API_MAXIMUM_AMOUNT_FILTER_DECLINE,
ApiProcessableException::API_OTHER_FILTER_DECLINE,
ApiProcessableException::API_ADDRESS_MATCH_FAIL,
+ self::$authorizationExpiredCode
]
);
}
@@ -538,7 +546,17 @@ public function refund(\Magento\Payment\Model\InfoInterface $payment, $amount)
*/
public function cancel(\Magento\Payment\Model\InfoInterface $payment)
{
- $this->void($payment);
+ try {
+ $this->void($payment);
+ } catch (ApiProcessableException $e) {
+ if ((int)$e->getCode() === self::$authorizationExpiredCode) {
+ $payment->setTransactionId(null);
+ $payment->setIsTransactionClosed(true);
+ $payment->setShouldCloseParentTransaction(true);
+ } else {
+ throw $e;
+ }
+ }
return $this;
}
diff --git a/app/code/Magento/Paypal/Model/Express/Checkout.php b/app/code/Magento/Paypal/Model/Express/Checkout.php
index f0b86588f1cfa..69e821910d84c 100644
--- a/app/code/Magento/Paypal/Model/Express/Checkout.php
+++ b/app/code/Magento/Paypal/Model/Express/Checkout.php
@@ -1070,6 +1070,7 @@ protected static function cmpShippingOptions(DataObject $option1, DataObject $op
*/
protected function _matchShippingMethodCode(Address $address, $selectedCode): string
{
+ $address->collectShippingRates();
$options = $this->_prepareShippingOptions($address, false);
foreach ($options as $option) {
if ($selectedCode === $option['code'] // the proper case as outlined in documentation
diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php b/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php
index 4afd398da2b54..82ccc59a53c8f 100644
--- a/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php
+++ b/app/code/Magento/Paypal/Model/Payflow/Service/Request/SecureToken.php
@@ -64,6 +64,7 @@ public function requestToken(Quote $quote)
$request->setTrxtype(Payflowpro::TRXTYPE_AUTH_ONLY);
$request->setVerbosity('HIGH');
$request->setAmt(0);
+ $request->setCurrency($quote->getBaseCurrencyCode());
$request->setCreatesecuretoken('Y');
$request->setSecuretokenid($this->mathRandom->getUniqueHash());
$request->setReturnurl($this->url->getUrl('paypal/transparent/response'));
diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php
index 06a8a5b680bf4..259f00ec5a9c5 100644
--- a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php
+++ b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php
@@ -3,9 +3,12 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Paypal\Model\Payflow\Service\Response;
use Magento\Framework\DataObject;
+use Magento\Framework\Intl\DateTimeFactory;
use Magento\Payment\Model\Method\Logger;
use Magento\Paypal\Model\Payflow\Service\Response\Handler\HandlerInterface;
use Magento\Framework\Session\Generic;
@@ -18,6 +21,8 @@
/**
* Class Transaction
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Transaction
{
@@ -51,6 +56,11 @@ class Transaction
*/
private $logger;
+ /**
+ * @var DateTimeFactory
+ */
+ private $dateTimeFactory;
+
/**
* @param Generic $sessionTransparent
* @param CartRepositoryInterface $quoteRepository
@@ -58,6 +68,7 @@ class Transaction
* @param PaymentMethodManagementInterface $paymentManagement
* @param HandlerInterface $errorHandler
* @param Logger $logger
+ * @param DateTimeFactory $dateTimeFactory
*/
public function __construct(
Generic $sessionTransparent,
@@ -65,7 +76,8 @@ public function __construct(
Transparent $transparent,
PaymentMethodManagementInterface $paymentManagement,
HandlerInterface $errorHandler,
- Logger $logger
+ Logger $logger,
+ DateTimeFactory $dateTimeFactory
) {
$this->sessionTransparent = $sessionTransparent;
$this->quoteRepository = $quoteRepository;
@@ -73,6 +85,7 @@ public function __construct(
$this->paymentManagement = $paymentManagement;
$this->errorHandler = $errorHandler;
$this->logger = $logger;
+ $this->dateTimeFactory = $dateTimeFactory;
}
/**
@@ -114,8 +127,45 @@ public function savePaymentInQuote($response)
$payment->setData(OrderPaymentInterface::CC_TYPE, $response->getData(OrderPaymentInterface::CC_TYPE));
$payment->setAdditionalInformation(Payflowpro::PNREF, $response->getData(Payflowpro::PNREF));
+ $expDate = $response->getData('expdate');
+ $expMonth = $this->getCcExpMonth($expDate);
+ $payment->setCcExpMonth($expMonth);
+ $expYear = $this->getCcExpYear($expDate);
+ $payment->setCcExpYear($expYear);
+
$this->errorHandler->handle($payment, $response);
$this->paymentManagement->set($quote->getId(), $payment);
}
+
+ /**
+ * Extracts expiration month from PayPal response expiration date.
+ *
+ * @param string $expDate format {MMYY}
+ * @return int
+ */
+ private function getCcExpMonth(string $expDate): int
+ {
+ return (int)substr($expDate, 0, 2);
+ }
+
+ /**
+ * Extracts expiration year from PayPal response expiration date.
+ *
+ * @param string $expDate format {MMYY}
+ * @return int
+ */
+ private function getCcExpYear(string $expDate): int
+ {
+ $last2YearDigits = (int)substr($expDate, 2, 2);
+ $currentDate = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC'));
+ $first2YearDigits = (int)substr($currentDate->format('Y'), 0, 2);
+
+ // case when credit card expires at next century
+ if ((int)$currentDate->format('y') > $last2YearDigits) {
+ $first2YearDigits++;
+ }
+
+ return 100 * $first2YearDigits + $last2YearDigits;
+ }
}
diff --git a/app/code/Magento/Paypal/Model/Payflow/Transparent.php b/app/code/Magento/Paypal/Model/Payflow/Transparent.php
index c161580c1b7f1..68b3389ac858e 100644
--- a/app/code/Magento/Paypal/Model/Payflow/Transparent.php
+++ b/app/code/Magento/Paypal/Model/Payflow/Transparent.php
@@ -3,11 +3,14 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Paypal\Model\Payflow;
use Magento\Payment\Helper\Formatter;
use Magento\Payment\Model\InfoInterface;
use Magento\Paypal\Model\Payflowpro;
+use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\Data\OrderPaymentExtensionInterfaceFactory;
use Magento\Sales\Model\Order\Payment;
use Magento\Paypal\Model\Payflow\Service\Gateway;
@@ -125,6 +128,8 @@ public function __construct(
}
/**
+ * Returns Payflow ResponseValidator instance.
+ *
* @return ResponseValidator
*/
public function getResponceValidator()
@@ -166,14 +171,17 @@ public function authorize(InfoInterface $payment, $amount)
$request->setData('origid', $token);
$request->setData('amt', $this->formatPrice($amount));
$request->setData('currency', $order->getBaseCurrencyCode());
- $request->setData('taxamt', $this->formatPrice($order->getBaseTaxAmount()));
+ $request->setData('itemamt', $this->formatPrice($order->getBaseSubtotal()));
+ $request->setData('taxamt', $this->calculateTaxAmount($order));
$request->setData('freightamt', $this->formatPrice($order->getBaseShippingAmount()));
+ $request->setData('discount', $this->formatPrice(abs($order->getBaseDiscountAmount())));
$response = $this->postRequest($request, $this->getConfig());
$this->processErrors($response);
try {
$this->responseValidator->validate($response, $this);
+ // phpcs:ignore Magento2.Exceptions.ThrowCatch
} catch (LocalizedException $exception) {
$payment->setParentTransactionId($response->getData(self::PNREF));
$this->void($payment);
@@ -199,10 +207,12 @@ public function getConfigInterface()
}
/**
+ * Creates vault payment token.
+ *
* @param Payment $payment
* @param string $token
- * @throws LocalizedException
* @return void
+ * @throws \Exception
*/
protected function createPaymentToken(Payment $payment, $token)
{
@@ -221,8 +231,11 @@ protected function createPaymentToken(Payment $payment, $token)
}
/**
+ * Generates CC expiration date by year and month provided in payment.
+ *
* @param Payment $payment
* @return string
+ * @throws \Exception
*/
private function getExpirationDate(Payment $payment)
{
@@ -241,6 +254,8 @@ private function getExpirationDate(Payment $payment)
}
/**
+ * Returns payment extension attributes instance.
+ *
* @param Payment $payment
* @return \Magento\Sales\Api\Data\OrderPaymentExtensionInterface
*/
@@ -276,4 +291,20 @@ public function capture(InfoInterface $payment, $amount)
return $this;
}
+
+ /**
+ * Calculates tax amount including discount compensation for product/shipping price included tax.
+ *
+ * @param OrderInterface $order
+ * @return string
+ */
+ private function calculateTaxAmount(
+ OrderInterface $order
+ ): string {
+ return $this->formatPrice(
+ $order->getBaseTaxAmount()
+ + $order->getBaseDiscountTaxCompensationAmount()
+ + $order->getBaseShippingDiscountTaxCompensationAmnt()
+ );
+ }
}
diff --git a/app/code/Magento/Paypal/Model/Pro.php b/app/code/Magento/Paypal/Model/Pro.php
index 698ed87f26c09..c404610590cf8 100644
--- a/app/code/Magento/Paypal/Model/Pro.php
+++ b/app/code/Magento/Paypal/Model/Pro.php
@@ -10,10 +10,10 @@
use Magento\Paypal\Model\Api\AbstractApi;
use Magento\Sales\Api\TransactionRepositoryInterface;
-use Magento\Paypal\Model\Info;
/**
- * PayPal Website Payments Pro implementation for payment method instances
+ * PayPal Website Payments Pro implementation for payment method instances.
+ *
* This model was created because right now PayPal Direct and PayPal Express payment methods cannot have same abstract
*/
class Pro
@@ -149,7 +149,8 @@ public function getConfig()
}
/**
- * API instance getter
+ * API instance getter.
+ *
* Sets current store id to current config instance and passes it to API
*
* @return \Magento\Paypal\Model\Api\Nvp
@@ -231,19 +232,22 @@ public function importPaymentInfo(\Magento\Framework\DataObject $from, \Magento\
public function void(\Magento\Framework\DataObject $payment)
{
$authTransactionId = $this->_getParentTransactionId($payment);
- if ($authTransactionId) {
- $api = $this->getApi();
- $api->setPayment($payment)->setAuthorizationId($authTransactionId)->callDoVoid();
- $this->importPaymentInfo($api, $payment);
- } else {
+ if (empty($authTransactionId)) {
throw new \Magento\Framework\Exception\LocalizedException(
__('You need an authorization transaction to void.')
);
}
+
+ $api = $this->getApi();
+ $api->setPayment($payment);
+ $api->setAuthorizationId($authTransactionId);
+ $api->callDoVoid();
+ $this->importPaymentInfo($api, $payment);
}
/**
- * Attempt to capture payment
+ * Attempt to capture payment.
+ *
* Will return false if the payment is not supposed to be captured
*
* @param \Magento\Framework\DataObject $payment
diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php
index b33d2f5723961..dd113562783aa 100644
--- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php
+++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php
@@ -37,6 +37,7 @@ protected function setUp()
->setMethods(
[
'getHtmlId',
+ 'getName',
'getTooltip',
'getForm',
]
diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php
index 60451a9827097..c404ef54aad1d 100644
--- a/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php
+++ b/app/code/Magento/Paypal/Test/Unit/Controller/Transparent/RequestSecureTokenTest.php
@@ -6,13 +6,15 @@
namespace Magento\Paypal\Test\Unit\Controller\Transparent;
use Magento\Framework\App\Action\Context;
+use Magento\Framework\Controller\Result\Json;
use Magento\Framework\Controller\Result\JsonFactory;
+use Magento\Framework\Data\Form\FormKey\Validator;
use Magento\Framework\Session\Generic;
use Magento\Framework\Session\SessionManager;
-use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Paypal\Controller\Transparent\RequestSecureToken;
use Magento\Paypal\Model\Payflow\Service\Request\SecureToken;
use Magento\Paypal\Model\Payflow\Transparent;
+use Magento\Quote\Model\Quote;
/**
* Class RequestSecureTokenTest
@@ -56,6 +58,11 @@ class RequestSecureTokenTest extends \PHPUnit\Framework\TestCase
*/
protected $sessionManagerMock;
+ /**
+ * @var Validator|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $formKeyValidator;
+
/**
* Set up
*
@@ -64,9 +71,16 @@ class RequestSecureTokenTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
+ $request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class)
+ ->setMethods(['isPost'])
+ ->getMockForAbstractClass();
+ $request->expects($this->any())->method('isPost')->willReturn(true);
$this->contextMock = $this->getMockBuilder(\Magento\Framework\App\Action\Context::class)
->disableOriginalConstructor()
->getMock();
+ $this->contextMock->method('getRequest')
+ ->willReturn($request);
+
$this->resultJsonFactoryMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\JsonFactory::class)
->setMethods(['create'])
->disableOriginalConstructor()
@@ -90,13 +104,19 @@ protected function setUp()
->disableOriginalConstructor()
->getMock();
+ $this->formKeyValidator = $this->getMockBuilder(Validator::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
$this->controller = new \Magento\Paypal\Controller\Transparent\RequestSecureToken(
$this->contextMock,
$this->resultJsonFactoryMock,
$this->sessionTransparentMock,
$this->secureTokenServiceMock,
$this->sessionManagerMock,
- $this->transparentMock
+ $this->transparentMock,
+ null,
+ $this->formKeyValidator
);
}
@@ -113,16 +133,15 @@ public function testExecuteSuccess()
'error' => false
];
- $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class)
+ $quoteMock = $this->getMockBuilder(Quote::class)
->disableOriginalConstructor()
->getMock();
$tokenMock = $this->getMockBuilder(\Magento\Framework\DataObject::class)
->disableOriginalConstructor()
->getMock();
- $jsonMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->formKeyValidator->method('validate')
+ ->willReturn(true);
$this->sessionManagerMock->expects($this->atLeastOnce())
->method('getQuote')
->willReturn($quoteMock);
@@ -147,15 +166,9 @@ public function testExecuteSuccess()
['securetoken', null, $secureToken]
]
);
- $this->resultJsonFactoryMock->expects($this->once())
- ->method('create')
- ->willReturn($jsonMock);
- $jsonMock->expects($this->once())
- ->method('setData')
- ->with($resultExpectation)
- ->willReturnSelf();
+ $jsonResult = $this->getJsonResult($resultExpectation);
- $this->assertEquals($jsonMock, $this->controller->execute());
+ $this->assertEquals($jsonResult, $this->controller->execute());
}
public function testExecuteTokenRequestException()
@@ -167,13 +180,11 @@ public function testExecuteTokenRequestException()
'error_messages' => __('Your payment has been declined. Please try again.')
];
- $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class)
- ->disableOriginalConstructor()
- ->getMock();
- $jsonMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class)
+ $quoteMock = $this->getMockBuilder(Quote::class)
->disableOriginalConstructor()
->getMock();
-
+ $this->formKeyValidator->method('validate')
+ ->willReturn(true);
$this->sessionManagerMock->expects($this->atLeastOnce())
->method('getQuote')
->willReturn($quoteMock);
@@ -187,18 +198,21 @@ public function testExecuteTokenRequestException()
->method('requestToken')
->with($quoteMock)
->willThrowException(new \Exception());
- $this->resultJsonFactoryMock->expects($this->once())
- ->method('create')
- ->willReturn($jsonMock);
- $jsonMock->expects($this->once())
- ->method('setData')
- ->with($resultExpectation)
- ->willReturnSelf();
- $this->assertEquals($jsonMock, $this->controller->execute());
+ $jsonResult = $this->getJsonResult($resultExpectation);
+
+ $this->assertEquals($jsonResult, $this->controller->execute());
}
- public function testExecuteEmptyQuoteError()
+ /**
+ * Tests error generation.
+ *
+ * @param Quote|null $quote
+ * @param bool $isValidToken
+ * @return void
+ * @dataProvider executeErrorDataProvider
+ */
+ public function testExecuteError($quote, bool $isValidToken)
{
$resultExpectation = [
'success' => false,
@@ -206,22 +220,51 @@ public function testExecuteEmptyQuoteError()
'error_messages' => __('Your payment has been declined. Please try again.')
];
- $quoteMock = null;
- $jsonMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Json::class)
+ $this->sessionManagerMock->expects($this->atLeastOnce())
+ ->method('getQuote')
+ ->willReturn($quote);
+ $this->formKeyValidator->method('validate')
+ ->willReturn($isValidToken);
+
+ $jsonResult = $this->getJsonResult($resultExpectation);
+
+ $this->assertEquals($jsonResult, $this->controller->execute());
+ }
+
+ /**
+ * @return array
+ */
+ public function executeErrorDataProvider()
+ {
+ $quote = $this->getMockBuilder(Quote::class)
->disableOriginalConstructor()
->getMock();
- $this->sessionManagerMock->expects($this->atLeastOnce())
- ->method('getQuote')
- ->willReturn($quoteMock);
- $this->resultJsonFactoryMock->expects($this->once())
- ->method('create')
- ->willReturn($jsonMock);
+ return [
+ 'empty quote' => [null, true],
+ 'invalid CSRF token' => [$quote, false]
+ ];
+ }
+
+ /**
+ * Returns json result.
+ *
+ * @param array $result
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getJsonResult(array $result): \PHPUnit_Framework_MockObject_MockObject
+ {
+ $jsonMock = $this->getMockBuilder(Json::class)
+ ->disableOriginalConstructor()
+ ->getMock();
$jsonMock->expects($this->once())
->method('setData')
- ->with($resultExpectation)
+ ->with($result)
->willReturnSelf();
+ $this->resultJsonFactoryMock->expects($this->once())
+ ->method('create')
+ ->willReturn($jsonMock);
- $this->assertEquals($jsonMock, $this->controller->execute());
+ return $jsonMock;
}
}
diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php
index 8615b91383aaa..72c13c80b31e4 100644
--- a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php
+++ b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/Element/FieldPluginTest.php
@@ -7,7 +7,6 @@
use Magento\Paypal\Model\Config\Structure\Element\FieldPlugin as FieldConfigStructurePlugin;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
-use Magento\Framework\App\RequestInterface;
use Magento\Config\Model\Config\Structure\Element\Field as FieldConfigStructureMock;
class FieldPluginTest extends \PHPUnit\Framework\TestCase
@@ -22,11 +21,6 @@ class FieldPluginTest extends \PHPUnit\Framework\TestCase
*/
private $objectManagerHelper;
- /**
- * @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject
- */
- private $requestMock;
-
/**
* @var FieldConfigStructureMock|\PHPUnit_Framework_MockObject_MockObject
*/
@@ -34,16 +28,13 @@ class FieldPluginTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
- $this->requestMock = $this->getMockBuilder(RequestInterface::class)
- ->getMockForAbstractClass();
$this->subjectMock = $this->getMockBuilder(FieldConfigStructureMock::class)
->disableOriginalConstructor()
->getMock();
$this->objectManagerHelper = new ObjectManagerHelper($this);
$this->plugin = $this->objectManagerHelper->getObject(
- FieldConfigStructurePlugin::class,
- ['request' => $this->requestMock]
+ FieldConfigStructurePlugin::class
);
}
@@ -56,10 +47,8 @@ public function testAroundGetConfigPathHasResult()
public function testAroundGetConfigPathNonPaymentSection()
{
- $this->requestMock->expects(static::once())
- ->method('getParam')
- ->with('section')
- ->willReturn('non-payment');
+ $this->subjectMock->method('getPath')
+ ->willReturn('non-payment/group/field');
$this->assertNull($this->plugin->afterGetConfigPath($this->subjectMock, null));
}
@@ -72,12 +61,7 @@ public function testAroundGetConfigPathNonPaymentSection()
*/
public function testAroundGetConfigPath($subjectPath, $expectedConfigPath)
{
- $this->requestMock->expects(static::once())
- ->method('getParam')
- ->with('section')
- ->willReturn('payment');
- $this->subjectMock->expects(static::once())
- ->method('getPath')
+ $this->subjectMock->method('getPath')
->willReturn($subjectPath);
$this->assertEquals($expectedConfigPath, $this->plugin->afterGetConfigPath($this->subjectMock, null));
diff --git a/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php b/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php
index 2575408078926..1fd816ecc6799 100644
--- a/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php
+++ b/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php
@@ -12,6 +12,7 @@
use Magento\Payment\Model\InfoInterface;
use Magento\Payment\Observer\AbstractDataAssignObserver;
use Magento\Paypal\Model\Api\Nvp;
+use Magento\Paypal\Model\Api\ProcessableException;
use Magento\Paypal\Model\Api\ProcessableException as ApiProcessableException;
use Magento\Paypal\Model\Express;
use Magento\Paypal\Model\Pro;
@@ -19,7 +20,7 @@
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Payment;
use Magento\Sales\Model\Order\Payment\Transaction\BuilderInterface;
-use \PHPUnit_Framework_MockObject_MockObject as MockObject;
+use PHPUnit_Framework_MockObject_MockObject as MockObject;
/**
* Class ExpressTest
@@ -27,10 +28,15 @@
*/
class ExpressTest extends \PHPUnit\Framework\TestCase
{
+ /**
+ * @var string
+ */
+ private static $authorizationExpiredCode = 10601;
+
/**
* @var array
*/
- protected $errorCodes = [
+ private $errorCodes = [
ApiProcessableException::API_INTERNAL_ERROR,
ApiProcessableException::API_UNABLE_PROCESS_PAYMENT_ERROR_CODE,
ApiProcessableException::API_DO_EXPRESS_CHECKOUT_FAIL,
@@ -40,7 +46,7 @@ class ExpressTest extends \PHPUnit\Framework\TestCase
ApiProcessableException::API_COUNTRY_FILTER_DECLINE,
ApiProcessableException::API_MAXIMUM_AMOUNT_FILTER_DECLINE,
ApiProcessableException::API_OTHER_FILTER_DECLINE,
- ApiProcessableException::API_ADDRESS_MATCH_FAIL
+ ApiProcessableException::API_ADDRESS_MATCH_FAIL,
];
/**
@@ -80,6 +86,7 @@ class ExpressTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
+ $this->errorCodes[] = self::$authorizationExpiredCode;
$this->checkoutSession = $this->createPartialMock(
Session::class,
['getPaypalTransactionData', 'setPaypalTransactionData']
@@ -104,16 +111,20 @@ protected function setUp()
);
$this->pro = $this->createPartialMock(
Pro::class,
- ['setMethod', 'getApi', 'importPaymentInfo', 'resetApi']
+ ['setMethod', 'getApi', 'importPaymentInfo', 'resetApi', 'void']
);
$this->eventManager = $this->getMockBuilder(ManagerInterface::class)
->setMethods(['dispatch'])
->getMockForAbstractClass();
- $this->pro->expects($this->any())->method('getApi')->will($this->returnValue($this->nvp));
+ $this->pro->method('getApi')
+ ->willReturn($this->nvp);
$this->helper = new ObjectManager($this);
}
+ /**
+ * Tests setting the list of processable errors.
+ */
public function testSetApiProcessableErrors()
{
$this->nvp->expects($this->once())->method('setProcessableErrors')->with($this->errorCodes);
@@ -128,6 +139,32 @@ public function testSetApiProcessableErrors()
);
}
+ /**
+ * Tests canceling order payment when expired authorization generates exception on a client.
+ */
+ public function testCancelWithExpiredAuthorizationTransaction()
+ {
+ $this->pro->method('void')
+ ->willThrowException(
+ new ProcessableException(__('PayPal gateway has rejected request.'), null, 10601)
+ );
+
+ $this->model = $this->helper->getObject(Express::class, ['data' => [$this->pro]]);
+ /** @var Payment|MockObject $paymentModel */
+ $paymentModel = $this->createMock(Payment::class);
+ $paymentModel->expects($this->once())
+ ->method('setTransactionId')
+ ->with(null);
+ $paymentModel->expects($this->once())
+ ->method('setIsTransactionClosed')
+ ->with(true);
+ $paymentModel->expects($this->once())
+ ->method('setShouldCloseParentTransaction')
+ ->with(true);
+
+ $this->model->cancel($paymentModel);
+ }
+
/**
* Tests order payment action.
*/
@@ -162,6 +199,11 @@ public function testOrder()
static::assertEquals($this->model, $this->model->order($paymentModel, 12.3));
}
+ /**
+ * Tests data assigning.
+ *
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
public function testAssignData()
{
$transportValue = 'something';
diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/TransparentTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/TransparentTest.php
index 9b67cedb47885..aa321a27e7617 100644
--- a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/TransparentTest.php
+++ b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/TransparentTest.php
@@ -120,8 +120,19 @@ protected function initializationAuthorizeMock()
{
$this->orderMock = $this->getMockBuilder(\Magento\Sales\Model\Order::class)
->setMethods([
- 'getCustomerId', 'getBillingAddress', 'getShippingAddress', 'getCustomerEmail',
- 'getId', 'getIncrementId', 'getBaseCurrencyCode'
+ 'getCustomerId',
+ 'getBillingAddress',
+ 'getShippingAddress',
+ 'getCustomerEmail',
+ 'getId',
+ 'getIncrementId',
+ 'getBaseCurrencyCode',
+ 'getBaseSubtotal',
+ 'getBaseTaxAmount',
+ 'getBaseDiscountTaxCompensationAmount',
+ 'getBaseShippingDiscountTaxCompensationAmnt',
+ 'getBaseShippingAmount',
+ 'getBaseDiscountAmount',
])
->disableOriginalConstructor()
->getMock();
@@ -365,6 +376,13 @@ public function testAuthorize()
$this->initializationAuthorizeMock();
$this->buildRequestData();
+ $this->orderMock->expects($this->once())->method('getBaseSubtotal');
+ $this->orderMock->expects($this->once())->method('getBaseTaxAmount');
+ $this->orderMock->expects($this->once())->method('getBaseDiscountTaxCompensationAmount');
+ $this->orderMock->expects($this->once())->method('getBaseShippingDiscountTaxCompensationAmnt');
+ $this->orderMock->expects($this->once())->method('getBaseShippingAmount');
+ $this->orderMock->expects($this->once())->method('getBaseDiscountAmount');
+
$paymentTokenMock = $this->createMock(PaymentTokenInterface::class);
$extensionAttributes = $this->getMockBuilder(\Magento\Sales\Api\Data\OrderPaymentExtensionInterface::class)
->disableOriginalConstructor()
diff --git a/app/code/Magento/Paypal/composer.json b/app/code/Magento/Paypal/composer.json
index 331198b474783..5ee1739253ced 100644
--- a/app/code/Magento/Paypal/composer.json
+++ b/app/code/Magento/Paypal/composer.json
@@ -26,7 +26,7 @@
"magento/module-checkout-agreements": "100.2.*"
},
"type": "magento2-module",
- "version": "100.2.5",
+ "version": "100.2.6",
"license": [
"proprietary"
],
diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml b/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml
index 90ffeddbb812e..82874c3c74ff2 100644
--- a/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml
+++ b/app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro_with_express_checkout.xml
@@ -6,7 +6,7 @@
*/
-->
-
+
Payflow Pro
0
@@ -30,11 +30,11 @@
-
+
Enable PayPal Credit
Learn More]]>
+ Learn More ]]>
payment/payflow_express_bml/active
Magento\Config\Model\Config\Source\Yesno
diff --git a/app/code/Magento/Paypal/i18n/en_US.csv b/app/code/Magento/Paypal/i18n/en_US.csv
index 2a1a6bbb48ef2..6c22ba09ca7cd 100644
--- a/app/code/Magento/Paypal/i18n/en_US.csv
+++ b/app/code/Magento/Paypal/i18n/en_US.csv
@@ -697,3 +697,8 @@ User,User
The PayPal Advertising Program has been shown to generate additional purchases as well as increase consumer's average purchase sizes by 15%
or more. See Details .
"
+"PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you.
+ You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®.
+ Learn More ","PayPal Express Checkout Payflow Edition lets you give customers access to financing through PayPal Credit® - at no additional cost to you.
+ You get paid up front, even though customers have more time to pay. A pre-integrated payment button lets customers pay quickly with PayPal Credit®.
+ Learn More "
diff --git a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro-method.js b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro-method.js
index 786f1a5aa85fd..24d06b7d0f8f2 100644
--- a/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro-method.js
+++ b/app/code/Magento/Paypal/view/frontend/web/js/view/payment/method-renderer/payflowpro-method.js
@@ -79,7 +79,10 @@ define([
placeOrder: function () {
var self = this;
- if (this.validateHandler() && additionalValidators.validate()) {
+ if (this.validateHandler() &&
+ additionalValidators.validate() &&
+ this.isPlaceOrderActionAllowed() === true
+ ) {
this.isPlaceOrderActionAllowed(false);
fullScreenLoader.startLoader();
$.when(
diff --git a/app/code/Magento/PaypalCaptcha/LICENSE.txt b/app/code/Magento/PaypalCaptcha/LICENSE.txt
new file mode 100644
index 0000000000000..49525fd99da9c
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/LICENSE.txt
@@ -0,0 +1,48 @@
+
+Open Software License ("OSL") v. 3.0
+
+This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:
+
+Licensed under the Open Software License version 3.0
+
+ 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following:
+
+ 1. to reproduce the Original Work in copies, either alone or as part of a collective work;
+
+ 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work;
+
+ 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License;
+
+ 4. to perform the Original Work publicly; and
+
+ 5. to display the Original Work publicly.
+
+ 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works.
+
+ 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work.
+
+ 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license.
+
+ 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c).
+
+ 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
+
+ 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer.
+
+ 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation.
+
+ 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c).
+
+ 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
+
+ 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License.
+
+ 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
+
+ 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
+
+ 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+ 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
+
+ 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.
\ No newline at end of file
diff --git a/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt b/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+Academic Free License ("AFL") v. 3.0
+
+This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:
+
+Licensed under the Academic Free License version 3.0
+
+ 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following:
+
+ 1. to reproduce the Original Work in copies, either alone or as part of a collective work;
+
+ 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work;
+
+ 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License;
+
+ 4. to perform the Original Work publicly; and
+
+ 5. to display the Original Work publicly.
+
+ 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works.
+
+ 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work.
+
+ 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license.
+
+ 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c).
+
+ 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
+
+ 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer.
+
+ 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation.
+
+ 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c).
+
+ 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
+
+ 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License.
+
+ 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
+
+ 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
+
+ 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+ 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
+
+ 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.
diff --git a/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php b/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php
new file mode 100644
index 0000000000000..289a1631ed1f6
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/Model/Checkout/ConfigProviderPayPal.php
@@ -0,0 +1,135 @@
+storeManager = $storeManager;
+ $this->captchaData = $captchaData;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getConfig(): array
+ {
+ $config['captchaPayments'][self::$formId] = [
+ 'isCaseSensitive' => $this->isCaseSensitive(self::$formId),
+ 'imageHeight' => $this->getImageHeight(self::$formId),
+ 'imageSrc' => $this->getImageSrc(self::$formId),
+ 'refreshUrl' => $this->getRefreshUrl(),
+ 'isRequired' => $this->isRequired(self::$formId),
+ 'timestamp' => time()
+ ];
+
+ return $config;
+ }
+
+ /**
+ * Returns is captcha case sensitive
+ *
+ * @param string $formId
+ * @return bool
+ */
+ private function isCaseSensitive(string $formId): bool
+ {
+ return (bool)$this->getCaptchaModel($formId)->isCaseSensitive();
+ }
+
+ /**
+ * Returns captcha image height
+ *
+ * @param string $formId
+ * @return int
+ */
+ private function getImageHeight(string $formId): int
+ {
+ return (int)$this->getCaptchaModel($formId)->getHeight();
+ }
+
+ /**
+ * Returns captcha image source path
+ *
+ * @param string $formId
+ * @return string
+ */
+ private function getImageSrc(string $formId): string
+ {
+ if ($this->isRequired($formId)) {
+ $captcha = $this->getCaptchaModel($formId);
+ $captcha->generate();
+ return $captcha->getImgSrc();
+ }
+
+ return '';
+ }
+
+ /**
+ * Returns URL to controller action which returns new captcha image
+ *
+ * @return string
+ */
+ private function getRefreshUrl(): string
+ {
+ $store = $this->storeManager->getStore();
+ return $store->getUrl('captcha/refresh', ['_secure' => $store->isCurrentlySecure()]);
+ }
+
+ /**
+ * Whether captcha is required to be inserted to this form
+ *
+ * @param string $formId
+ * @return bool
+ */
+ private function isRequired(string $formId): bool
+ {
+ return (bool)$this->getCaptchaModel($formId)->isRequired();
+ }
+
+ /**
+ * Return captcha model for specified form
+ *
+ * @param string $formId
+ * @return CaptchaInterface
+ */
+ private function getCaptchaModel(string $formId): CaptchaInterface
+ {
+ return $this->captchaData->getCaptcha($formId);
+ }
+}
diff --git a/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php b/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php
new file mode 100644
index 0000000000000..e7cb282b1799b
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/Observer/CaptchaRequestToken.php
@@ -0,0 +1,76 @@
+helper = $helper;
+ $this->jsonSerializer = $jsonSerializer;
+ $this->actionFlag = $actionFlag;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute(Observer $observer)
+ {
+ $formId = 'co-payment-form';
+ $captcha = $this->helper->getCaptcha($formId);
+
+ if (!$captcha->isRequired()) {
+ return;
+ }
+
+ /** @var Action $controller */
+ $controller = $observer->getControllerAction();
+ $word = $controller->getRequest()->getPost('captcha_string');
+ if ($captcha->isCorrect($word)) {
+ return;
+ }
+
+ $data = $this->jsonSerializer->serialize([
+ 'success' => false,
+ 'error' => true,
+ 'error_messages' => __('Incorrect CAPTCHA.')
+ ]);
+ $this->actionFlag->set('', Action::FLAG_NO_DISPATCH, true);
+ $controller->getResponse()->representJson($data);
+ }
+}
diff --git a/app/code/Magento/PaypalCaptcha/README.md b/app/code/Magento/PaypalCaptcha/README.md
new file mode 100644
index 0000000000000..71588599a5ecd
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/README.md
@@ -0,0 +1 @@
+The PayPal Captcha module provides a possibility to enable Captcha validation on Payflow Pro payment form.
\ No newline at end of file
diff --git a/app/code/Magento/PaypalCaptcha/composer.json b/app/code/Magento/PaypalCaptcha/composer.json
new file mode 100644
index 0000000000000..c2c7032ae8060
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/composer.json
@@ -0,0 +1,27 @@
+{
+ "name": "magento/module-paypal-captcha",
+ "description": "N/A",
+ "require": {
+ "php": "~7.0.13|~7.1.0",
+ "magento/framework": "101.0.*",
+ "magento/module-captcha": "100.2.*",
+ "magento/module-checkout": "100.2.*",
+ "magento/module-store": "100.2.*"
+ },
+ "suggest": {
+ "magento/module-paypal": "100.2.*"
+ },
+ "type": "magento2-module",
+ "version": "100.2.0",
+ "license": [
+ "proprietary"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\PaypalCaptcha\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.xml b/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.xml
new file mode 100644
index 0000000000000..12afd8ceda60e
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/etc/adminhtml/system.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ CAPTCHA for "Create user", "Forgot password", "Payflow Pro" forms is always enabled if chosen.
+
+
+
+
+
diff --git a/app/code/Magento/PaypalCaptcha/etc/config.xml b/app/code/Magento/PaypalCaptcha/etc/config.xml
new file mode 100644
index 0000000000000..133a78a42f7b4
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/etc/config.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ 1
+
+
+ 1
+
+
+
+
+
+
+
+ Payflow Pro
+
+
+
+
+
+
diff --git a/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml b/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml
new file mode 100644
index 0000000000000..c236d5ea04ca0
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/etc/frontend/di.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ - Magento\PaypalCaptcha\Model\Checkout\ConfigProviderPayPal
+
+
+
+
diff --git a/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml b/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml
new file mode 100644
index 0000000000000..ae706c4485d61
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/etc/frontend/events.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/PaypalCaptcha/etc/module.xml b/app/code/Magento/PaypalCaptcha/etc/module.xml
new file mode 100644
index 0000000000000..a456cb0584fe6
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/etc/module.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/PaypalCaptcha/registration.php b/app/code/Magento/PaypalCaptcha/registration.php
new file mode 100644
index 0000000000000..4dac0582a6d1b
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/registration.php
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+ -
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- uiComponent
+ - paypal-captcha
+ - paypal-captcha
+ - checkoutProvider
+ -
+
- Magento_Checkout/payment/before-place-order
+
+ -
+
-
+
- Magento_PaypalCaptcha/js/view/checkout/paymentCaptcha
+ - paypal-captcha
+ - co-payment-form
+ - checkoutConfig
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js b/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js
new file mode 100644
index 0000000000000..78e7add4ec690
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/view/frontend/requirejs-config.js
@@ -0,0 +1,14 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+var config = {
+ config: {
+ mixins: {
+ 'Magento_Checkout/js/view/payment/list': {
+ 'Magento_PaypalCaptcha/js/view/payment/list-mixin': true
+ }
+ }
+ }
+};
diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js
new file mode 100644
index 0000000000000..f8f119e3b3396
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/checkout/paymentCaptcha.js
@@ -0,0 +1,44 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'jquery',
+ 'Magento_Captcha/js/view/checkout/defaultCaptcha',
+ 'Magento_Captcha/js/model/captchaList',
+ 'Magento_Captcha/js/model/captcha'
+],
+function ($, defaultCaptcha, captchaList, Captcha) {
+ 'use strict';
+
+ return defaultCaptcha.extend({
+
+ /** @inheritdoc */
+ initialize: function () {
+ var captchaConfigPayment,
+ currentCaptcha;
+
+ this._super();
+
+ if (window[this.configSource] && window[this.configSource].captchaPayments) {
+ captchaConfigPayment = window[this.configSource].captchaPayments;
+
+ $.each(captchaConfigPayment, function (formId, captchaData) {
+ var captcha;
+
+ captchaData.formId = formId;
+ captcha = Captcha(captchaData);
+ captchaList.add(captcha);
+ });
+ }
+
+ currentCaptcha = captchaList.getCaptchaByFormId(this.formId);
+
+ if (currentCaptcha != null) {
+ currentCaptcha.setIsVisible(true);
+ this.setCurrentCaptcha(currentCaptcha);
+ }
+ }
+ });
+});
diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js
new file mode 100644
index 0000000000000..60172f696e9ed
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/js/view/payment/list-mixin.js
@@ -0,0 +1,54 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'jquery',
+ 'Magento_Captcha/js/model/captchaList'
+], function ($, captchaList) {
+ 'use strict';
+
+ var mixin = {
+
+ formId: 'co-payment-form',
+
+ /**
+ * Sets custom template for Payflow Pro
+ *
+ * @param {Object} payment
+ * @returns {Object}
+ */
+ createComponent: function (payment) {
+
+ var component = this._super(payment);
+
+ if (component.component === 'Magento_Paypal/js/view/payment/method-renderer/payflowpro-method') {
+ component.template = 'Magento_PaypalCaptcha/payment/payflowpro-form';
+ $(window).off('clearTimeout')
+ .on('clearTimeout', this.clearTimeout.bind(this));
+ }
+
+ return component;
+ },
+
+ /**
+ * Overrides default window.clearTimeout() to catch errors from iframe and reload Captcha.
+ */
+ clearTimeout: function () {
+ var captcha = captchaList.getCaptchaByFormId(this.formId);
+
+ if (captcha !== null) {
+ captcha.refresh();
+ }
+ clearTimeout();
+ }
+ };
+
+ /**
+ * Overrides `Magento_Checkout/js/view/payment/list::createComponent`
+ */
+ return function (target) {
+ return target.extend(mixin);
+ };
+});
diff --git a/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html b/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html
new file mode 100644
index 0000000000000..fec5cf96b0324
--- /dev/null
+++ b/app/code/Magento/PaypalCaptcha/view/frontend/web/template/payment/payflowpro-form.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Persistent/Model/QuoteManager.php b/app/code/Magento/Persistent/Model/QuoteManager.php
index 35c2c70be30dc..8ae22e4c26c6f 100644
--- a/app/code/Magento/Persistent/Model/QuoteManager.php
+++ b/app/code/Magento/Persistent/Model/QuoteManager.php
@@ -7,6 +7,8 @@
/**
* Class QuoteManager
+ *
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class QuoteManager
{
@@ -87,6 +89,7 @@ public function setGuest($checkQuote = false)
->setCustomerLastname(null)
->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID)
->setIsPersistent(false)
+ ->setCustomerIsGuest(true)
->removeAllAddresses();
//Create guest addresses
$quote->getShippingAddress();
diff --git a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php
index 42baf7d692a7c..7e5a5769e00a5 100644
--- a/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php
+++ b/app/code/Magento/Persistent/Observer/CheckExpirePersistentQuoteObserver.php
@@ -1,6 +1,5 @@
_persistentSession->isPersistent() &&
!$this->_customerSession->isLoggedIn() &&
$this->_checkoutSession->getQuoteId() &&
- !$this->isRequestFromCheckoutPage($this->request)
+ !$this->isRequestFromCheckoutPage($this->request) &&
// persistent session does not expire on onepage checkout page
+ (
+ $this->_checkoutSession->getQuote()->getIsPersistent() ||
+ $this->_checkoutSession->getQuote()->getCustomerIsGuest()
+ )
) {
$this->_eventManager->dispatch('persistent_session_expired');
$this->quoteManager->expire();
diff --git a/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php b/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php
index db6b6d1ee370d..2803bc998dcbe 100644
--- a/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php
+++ b/app/code/Magento/Persistent/Observer/SetQuotePersistentDataObserver.php
@@ -1,6 +1,5 @@
_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn())
- && !$this->_persistentData->isShoppingCartPersist()
+ ($this->_persistentSession->isPersistent())
+ && $this->_persistentData->isShoppingCartPersist()
)
&& $this->quoteManager->isPersistent()
) {
diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontQuoteShippingDataPersistedForGuestTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontQuoteShippingDataPersistedForGuestTest.xml
new file mode 100644
index 0000000000000..908a037ed36a2
--- /dev/null
+++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontQuoteShippingDataPersistedForGuestTest.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ John1
+ Doe1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{US_Address_CA.postcode}}
+ grabTextPostCode
+
+
+
diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php
index 29a3196c5f45e..b6e88d4363b90 100644
--- a/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php
+++ b/app/code/Magento/Persistent/Test/Unit/Observer/CheckExpirePersistentQuoteObserverTest.php
@@ -1,12 +1,16 @@
sessionMock = $this->createMock(\Magento\Persistent\Helper\Session::class);
@@ -79,6 +88,10 @@ protected function setUp()
$this->checkoutSessionMock,
$this->requestMock
);
+ $this->quoteMock = $this->getMockBuilder(Quote::class)
+ ->setMethods(['getCustomerIsGuest', 'getIsPersistent'])
+ ->disableOriginalConstructor()
+ ->getMock();
}
public function testExecuteWhenCanNotApplyPersistentData()
@@ -128,6 +141,11 @@ public function testExecuteWhenPersistentIsEnabled(
->will($this->returnValue(true));
$this->persistentHelperMock->expects($this->once())->method('isEnabled')->will($this->returnValue(true));
$this->sessionMock->expects($this->once())->method('isPersistent')->will($this->returnValue(false));
+ $this->checkoutSessionMock
+ ->method('getQuote')
+ ->willReturn($this->quoteMock);
+ $this->quoteMock->method('getCustomerIsGuest')->willReturn(true);
+ $this->quoteMock->method('getIsPersistent')->willReturn(true);
$this->customerSessionMock
->expects($this->atLeastOnce())
->method('isLoggedIn')
diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php
index 6724743789cea..ffa829e8456cc 100644
--- a/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php
+++ b/app/code/Magento/Persistent/Test/Unit/Observer/SetQuotePersistentDataObserverTest.php
@@ -7,6 +7,9 @@
namespace Magento\Persistent\Test\Unit\Observer;
+/**
+ * Observer test for setting "is_persistent" value to quote
+ */
class SetQuotePersistentDataObserverTest extends \PHPUnit\Framework\TestCase
{
/**
@@ -83,7 +86,6 @@ public function testExecuteWhenQuoteNotExist()
->method('getEvent')
->will($this->returnValue($this->eventManagerMock));
$this->eventManagerMock->expects($this->once())->method('getQuote');
- $this->customerSessionMock->expects($this->never())->method('isLoggedIn');
$this->model->execute($this->observerMock);
}
@@ -98,8 +100,7 @@ public function testExecuteWhenSessionIsPersistent()
->expects($this->once())
->method('getQuote')
->will($this->returnValue($this->quoteMock));
- $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->will($this->returnValue(false));
- $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(false));
+ $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->will($this->returnValue(true));
$this->quoteManagerMock->expects($this->once())->method('isPersistent')->will($this->returnValue(true));
$this->quoteMock->expects($this->once())->method('setIsPersistent')->with(true);
$this->model->execute($this->observerMock);
diff --git a/app/code/Magento/Persistent/composer.json b/app/code/Magento/Persistent/composer.json
index 73184a0648d24..0bfbc30e28f69 100644
--- a/app/code/Magento/Persistent/composer.json
+++ b/app/code/Magento/Persistent/composer.json
@@ -12,7 +12,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.3",
+ "version": "100.2.4",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php b/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php
index 9950526182e3e..53705ee9f6e72 100644
--- a/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php
+++ b/app/code/Magento/ProductVideo/Controller/Adminhtml/Product/Gallery/RetrieveImage.php
@@ -107,6 +107,9 @@ public function execute()
{
$baseTmpMediaPath = $this->mediaConfig->getBaseTmpMediaPath();
try {
+ if (!$this->getRequest()->isPost()) {
+ throw new LocalizedException(__('Invalid request type.'));
+ }
$remoteFileUrl = $this->getRequest()->getParam('remote_image');
$this->validateRemoteFile($remoteFileUrl);
$localFileName = Uploader::getCorrectFileName(basename($remoteFileUrl));
diff --git a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php
index c56e5e3139517..d554b5dd68db2 100644
--- a/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php
+++ b/app/code/Magento/ProductVideo/Model/Plugin/Catalog/Product/Gallery/CreateHandler.php
@@ -20,6 +20,8 @@ class CreateHandler extends AbstractHandler
const ADDITIONAL_STORE_DATA_KEY = 'additional_store_data';
/**
+ * Execute before Plugin
+ *
* @param \Magento\Catalog\Model\Product\Gallery\CreateHandler $mediaGalleryCreateHandler
* @param \Magento\Catalog\Model\Product $product
* @param array $arguments
@@ -45,6 +47,8 @@ public function beforeExecute(
}
/**
+ * Execute plugin
+ *
* @param \Magento\Catalog\Model\Product\Gallery\CreateHandler $mediaGalleryCreateHandler
* @param \Magento\Catalog\Model\Product $product
* @return \Magento\Catalog\Model\Product
@@ -59,6 +63,9 @@ public function afterExecute(
);
if (!empty($mediaCollection)) {
+ if ($product->getIsDuplicate() === true) {
+ $mediaCollection = $this->makeAllNewVideos($product->getId(), $mediaCollection);
+ }
$newVideoCollection = $this->collectNewVideos($mediaCollection);
$this->saveVideoData($newVideoCollection, 0);
@@ -71,6 +78,8 @@ public function afterExecute(
}
/**
+ * Saves video data
+ *
* @param array $videoDataCollection
* @param int $storeId
* @return void
@@ -84,6 +93,8 @@ protected function saveVideoData(array $videoDataCollection, $storeId)
}
/**
+ * Saves additioanal video data
+ *
* @param array $videoDataCollection
* @return void
*/
@@ -100,6 +111,8 @@ protected function saveAdditionalStoreData(array $videoDataCollection)
}
/**
+ * Saves video data
+ *
* @param array $item
* @return void
*/
@@ -112,6 +125,8 @@ protected function saveVideoValuesItem(array $item)
}
/**
+ * Excludes current store data
+ *
* @param array $mediaCollection
* @param int $currentStoreId
* @return array
@@ -127,6 +142,8 @@ function ($item) use ($currentStoreId) {
}
/**
+ * Prepare video data for saving
+ *
* @param array $rowData
* @return array
*/
@@ -144,6 +161,8 @@ protected function prepareVideoRowDataForSave(array $rowData)
}
/**
+ * Loads video data
+ *
* @param array $mediaCollection
* @param int $excludedStore
* @return array
@@ -166,6 +185,8 @@ protected function loadStoreViewVideoData(array $mediaCollection, $excludedStore
}
/**
+ * Collect video data
+ *
* @param array $mediaCollection
* @return array
*/
@@ -183,6 +204,8 @@ protected function collectVideoData(array $mediaCollection)
}
/**
+ * Extract video data
+ *
* @param array $rowData
* @return array
*/
@@ -195,6 +218,8 @@ protected function extractVideoDataFromRowData(array $rowData)
}
/**
+ * Collect items for additional data adding
+ *
* @param array $mediaCollection
* @return array
*/
@@ -210,6 +235,8 @@ protected function collectVideoEntriesIdsToAdditionalLoad(array $mediaCollection
}
/**
+ * Add additional data
+ *
* @param array $mediaCollection
* @param array $data
* @return array
@@ -230,6 +257,8 @@ protected function addAdditionalStoreData(array $mediaCollection, array $data):
}
/**
+ * Creates additional video data
+ *
* @param array $storeData
* @param int $valueId
* @return array
@@ -248,6 +277,8 @@ protected function createAdditionalStoreDataCollection(array $storeData, $valueI
}
/**
+ * Collect new videos
+ *
* @param array $mediaCollection
* @return array
*/
@@ -263,6 +294,8 @@ private function collectNewVideos(array $mediaCollection): array
}
/**
+ * Checks if gallery item is video
+ *
* @param $item
* @return bool
*/
@@ -274,6 +307,8 @@ private function isVideoItem($item): bool
}
/**
+ * Checks if video is new
+ *
* @param $item
* @return bool
*/
@@ -283,4 +318,23 @@ private function isNewVideo($item): bool
|| empty($item['video_url_default'])
|| empty($item['video_title_default']);
}
+
+ /**
+ * Mark all videos as new
+ *
+ * @param int $entityId
+ * @param array $mediaCollection
+ * @return array
+ */
+ private function makeAllNewVideos($entityId, array $mediaCollection): array
+ {
+ foreach ($mediaCollection as $key => $video) {
+ if ($this->isVideoItem($video)) {
+ unset($video['video_url_default'], $video['video_title_default']);
+ $video['entity_id'] = $entityId;
+ $mediaCollection[$key] = $video;
+ }
+ }
+ return $mediaCollection;
+ }
}
diff --git a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php
index 28c6db7d31379..cc38e55d26af2 100644
--- a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php
+++ b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php
@@ -6,6 +6,7 @@
namespace Magento\ProductVideo\Test\Unit\Controller\Adminhtml\Product\Gallery;
use Magento\Framework\App\Filesystem\DirectoryList;
+use Magento\Framework\App\Request\Http;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -101,14 +102,16 @@ protected function setUp()
$this->adapterFactoryMock->expects($this->once())->method('create')->willReturn($this->abstractAdapter);
$this->curlMock = $this->createMock(\Magento\Framework\HTTP\Adapter\Curl::class);
$this->storageFileMock = $this->createMock(\Magento\MediaStorage\Model\ResourceModel\File\Storage\File::class);
- $this->request = $this->createMock(\Magento\Framework\App\RequestInterface::class);
+ $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class)
+ ->setMethods(['isPost'])
+ ->getMockForAbstractClass();
+ $this->request->expects($this->any())->method('isPost')->willReturn(true);
$this->fileDriverMock = $this->createMock(\Magento\Framework\Filesystem\DriverInterface::class);
- $this->contextMock->expects($this->any())->method('getRequest')->will($this->returnValue($this->request));
$managerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class)
->disableOriginalConstructor()
->setMethods(['get'])
->getMockForAbstractClass();
- $this->contextMock->expects($this->any())->method('getRequest')->will($this->returnValue($this->request));
+ $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->request);
$this->contextMock->expects($this->any())->method('getObjectManager')->willReturn($managerMock);
$this->image = $objectManager->getObject(
diff --git a/app/code/Magento/ProductVideo/composer.json b/app/code/Magento/ProductVideo/composer.json
index 7c5017eef4a5a..d92ab26d39fe5 100644
--- a/app/code/Magento/ProductVideo/composer.json
+++ b/app/code/Magento/ProductVideo/composer.json
@@ -16,7 +16,7 @@
"magento/module-config": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.5",
+ "version": "100.2.6",
"license": [
"proprietary"
],
diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
index 3966321f6072c..b7f4adb857a91 100644
--- a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
+++ b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
@@ -179,12 +179,14 @@ define([
* @private
*/
clearEvents: function () {
- this.fotoramaItem.off(
- 'fotorama:show.' + this.PV +
- ' fotorama:showend.' + this.PV +
- ' fotorama:fullscreenenter.' + this.PV +
- ' fotorama:fullscreenexit.' + this.PV
- );
+ if (this.fotoramaItem !== undefined) {
+ this.fotoramaItem.off(
+ 'fotorama:show.' + this.PV +
+ ' fotorama:showend.' + this.PV +
+ ' fotorama:fullscreenenter.' + this.PV +
+ ' fotorama:fullscreenexit.' + this.PV
+ );
+ }
},
/**
diff --git a/app/code/Magento/Quote/Api/Data/CartInterface.php b/app/code/Magento/Quote/Api/Data/CartInterface.php
index 551833e3effb1..b87869de6b3df 100644
--- a/app/code/Magento/Quote/Api/Data/CartInterface.php
+++ b/app/code/Magento/Quote/Api/Data/CartInterface.php
@@ -223,14 +223,14 @@ public function setBillingAddress(\Magento\Quote\Api\Data\AddressInterface $bill
/**
* Returns the reserved order ID for the cart.
*
- * @return int|null Reserved order ID. Otherwise, null.
+ * @return string|null Reserved order ID. Otherwise, null.
*/
public function getReservedOrderId();
/**
* Sets the reserved order ID for the cart.
*
- * @param int $reservedOrderId
+ * @param string $reservedOrderId
* @return $this
*/
public function setReservedOrderId($reservedOrderId);
diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php
index 640f89844546e..064b0c60e199c 100644
--- a/app/code/Magento/Quote/Model/Quote.php
+++ b/app/code/Magento/Quote/Model/Quote.php
@@ -7,6 +7,7 @@
use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Customer\Api\Data\GroupInterface;
+use Magento\Directory\Model\AllowedCountries;
use Magento\Framework\Api\AttributeValueFactory;
use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface;
use Magento\Framework\Model\AbstractExtensibleModel;
@@ -14,6 +15,7 @@
use Magento\Quote\Model\Quote\Address;
use Magento\Quote\Model\Quote\Address\Total as AddressTotal;
use Magento\Sales\Model\Status;
+use Magento\Store\Model\ScopeInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Sales\Model\OrderIncrementIdChecker;
@@ -360,6 +362,11 @@ class Quote extends AbstractExtensibleModel implements \Magento\Quote\Api\Data\C
*/
private $orderIncrementIdChecker;
+ /**
+ * @var AllowedCountries
+ */
+ private $allowedCountriesReader;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -402,6 +409,7 @@ class Quote extends AbstractExtensibleModel implements \Magento\Quote\Api\Data\C
* @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection
* @param array $data
* @param OrderIncrementIdChecker|null $orderIncrementIdChecker
+ * @param AllowedCountries|null $allowedCountriesReader
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -445,7 +453,8 @@ public function __construct(
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
array $data = [],
- OrderIncrementIdChecker $orderIncrementIdChecker = null
+ OrderIncrementIdChecker $orderIncrementIdChecker = null,
+ AllowedCountries $allowedCountriesReader = null
) {
$this->quoteValidator = $quoteValidator;
$this->_catalogProduct = $catalogProduct;
@@ -482,6 +491,8 @@ public function __construct(
$this->shippingAssignmentFactory = $shippingAssignmentFactory;
$this->orderIncrementIdChecker = $orderIncrementIdChecker ?: ObjectManager::getInstance()
->get(OrderIncrementIdChecker::class);
+ $this->allowedCountriesReader = $allowedCountriesReader
+ ?: ObjectManager::getInstance()->get(AllowedCountries::class);
parent::__construct(
$context,
$registry,
@@ -941,7 +952,7 @@ public function assignCustomerWithAddressChange(
/** @var \Magento\Quote\Model\Quote\Address $billingAddress */
$billingAddress = $this->_quoteAddressFactory->create();
$billingAddress->importCustomerAddressData($defaultBillingAddress);
- $this->setBillingAddress($billingAddress);
+ $this->assignAddress($billingAddress);
}
}
@@ -959,7 +970,7 @@ public function assignCustomerWithAddressChange(
$shippingAddress = $this->_quoteAddressFactory->create();
}
}
- $this->setShippingAddress($shippingAddress);
+ $this->assignAddress($shippingAddress, false);
}
return $this;
@@ -1354,7 +1365,7 @@ public function addShippingAddress(\Magento\Quote\Api\Data\AddressInterface $add
}
/**
- * Retrieve quote items collection
+ * Retrieve quote items collection.
*
* @param bool $useCache
* @return \Magento\Eav\Model\Entity\Collection\AbstractCollection
@@ -1362,10 +1373,10 @@ public function addShippingAddress(\Magento\Quote\Api\Data\AddressInterface $add
*/
public function getItemsCollection($useCache = true)
{
- if ($this->hasItemsCollection()) {
+ if ($this->hasItemsCollection() && $useCache) {
return $this->getData('items_collection');
}
- if (null === $this->_items) {
+ if (null === $this->_items || !$useCache) {
$this->_items = $this->_quoteItemCollectionFactory->create();
$this->extensionAttributesJoinProcessor->process($this->_items);
$this->_items->setQuote($this);
@@ -2218,6 +2229,11 @@ public function validateMinimumAmount($multishipping = false)
if (!$minOrderActive) {
return true;
}
+ $includeDiscount = $this->_scopeConfig->getValue(
+ 'sales/minimum_order/include_discount_amount',
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
$minOrderMulti = $this->_scopeConfig->isSetFlag(
'sales/minimum_order/multi_address',
\Magento\Store\Model\ScopeInterface::SCOPE_STORE,
@@ -2251,7 +2267,10 @@ public function validateMinimumAmount($multishipping = false)
$taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0;
foreach ($address->getQuote()->getItemsCollection() as $item) {
/** @var \Magento\Quote\Model\Quote\Item $item */
- $amount = $item->getBaseRowTotal() - $item->getBaseDiscountAmount() + $taxes;
+ $amount = $includeDiscount ?
+ $item->getBaseRowTotal() - $item->getBaseDiscountAmount() + $taxes :
+ $item->getBaseRowTotal() + $taxes;
+
if ($amount < $minAmount) {
return false;
}
@@ -2261,7 +2280,9 @@ public function validateMinimumAmount($multishipping = false)
$baseTotal = 0;
foreach ($addresses as $address) {
$taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0;
- $baseTotal += $address->getBaseSubtotalWithDiscount() + $taxes;
+ $baseTotal += $includeDiscount ?
+ $address->getBaseSubtotalWithDiscount() + $taxes :
+ $address->getBaseSubtotal() + $taxes;
}
if ($baseTotal < $minAmount) {
return false;
@@ -2560,4 +2581,34 @@ public function setExtensionAttributes(\Magento\Quote\Api\Data\CartExtensionInte
{
return $this->_setExtensionAttributes($extensionAttributes);
}
+
+ /**
+ * Check is address allowed for store
+ *
+ * @param Address $address
+ * @param int|null $storeId
+ * @return bool
+ */
+ private function isAddressAllowedForWebsite(Address $address, $storeId): bool
+ {
+ $allowedCountries = $this->allowedCountriesReader->getAllowedCountries(ScopeInterface::SCOPE_STORE, $storeId);
+
+ return in_array($address->getCountryId(), $allowedCountries);
+ }
+
+ /**
+ * Assign address to quote
+ *
+ * @param Address $address
+ * @param bool $isBillingAddress
+ * @return void
+ */
+ private function assignAddress(Address $address, bool $isBillingAddress = true)
+ {
+ if ($this->isAddressAllowedForWebsite($address, $this->getStoreId())) {
+ $isBillingAddress
+ ? $this->setBillingAddress($address)
+ : $this->setShippingAddress($address);
+ }
+ }
}
diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php
index f0419c7f96095..38a97783ca012 100644
--- a/app/code/Magento/Quote/Model/Quote/Address.php
+++ b/app/code/Magento/Quote/Model/Quote/Address.php
@@ -1150,6 +1150,11 @@ public function validateMinimumAmount()
return true;
}
+ $includeDiscount = $this->_scopeConfig->getValue(
+ 'sales/minimum_order/include_discount_amount',
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
$amount = $this->_scopeConfig->getValue(
'sales/minimum_order/amount',
\Magento\Store\Model\ScopeInterface::SCOPE_STORE,
@@ -1160,9 +1165,12 @@ public function validateMinimumAmount()
\Magento\Store\Model\ScopeInterface::SCOPE_STORE,
$storeId
);
+
$taxes = $taxInclude ? $this->getBaseTaxAmount() : 0;
- return ($this->getBaseSubtotalWithDiscount() + $taxes >= $amount);
+ return $includeDiscount ?
+ ($this->getBaseSubtotalWithDiscount() + $taxes >= $amount) :
+ ($this->getBaseSubtotal() + $taxes >= $amount);
}
/**
diff --git a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php
index c5b8dc1c4b124..81b0bed2592ea 100644
--- a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php
+++ b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php
@@ -12,6 +12,9 @@
use Magento\Quote\Model\QuoteAddressValidator;
use Magento\Customer\Api\AddressRepositoryInterface;
+/**
+ * Saves billing address for quotes.
+ */
class BillingAddressPersister
{
/**
@@ -37,17 +40,17 @@ public function __construct(
}
/**
+ * Save address for billing.
+ *
* @param CartInterface $quote
* @param AddressInterface $address
* @param bool $useForShipping
* @return void
- * @throws NoSuchEntityException
- * @throws InputException
*/
public function save(CartInterface $quote, AddressInterface $address, $useForShipping = false)
{
/** @var \Magento\Quote\Model\Quote $quote */
- $this->addressValidator->validate($address);
+ $this->addressValidator->validateForCart($quote, $address);
$customerAddressId = $address->getCustomerAddressId();
$shippingAddress = null;
$addressData = [];
diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total.php b/app/code/Magento/Quote/Model/Quote/Address/Total.php
index 42224c970ed27..00060c15c10d8 100644
--- a/app/code/Magento/Quote/Model/Quote/Address/Total.php
+++ b/app/code/Magento/Quote/Model/Quote/Address/Total.php
@@ -6,6 +6,8 @@
namespace Magento\Quote\Model\Quote\Address;
/**
+ * Class Total
+ *
* @method string getCode()
*
* @api
@@ -54,6 +56,8 @@ public function __construct(
*/
public function setTotalAmount($code, $amount)
{
+ $amount = is_float($amount) ? round($amount, 4) : $amount;
+
$this->totalAmounts[$code] = $amount;
if ($code != 'subtotal') {
$code = $code . '_amount';
@@ -72,6 +76,8 @@ public function setTotalAmount($code, $amount)
*/
public function setBaseTotalAmount($code, $amount)
{
+ $amount = is_float($amount) ? round($amount, 4) : $amount;
+
$this->baseTotalAmounts[$code] = $amount;
if ($code != 'subtotal') {
$code = $code . '_amount';
@@ -167,6 +173,7 @@ public function getAllBaseTotalAmounts()
/**
* Set the full info, which is used to capture tax related information.
+ *
* If a string is used, it is assumed to be serialized.
*
* @param array|string $info
diff --git a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php
index 84f1fc1c35adf..e9a63dad6e169 100644
--- a/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php
+++ b/app/code/Magento/Quote/Model/Quote/Address/Total/Shipping.php
@@ -9,6 +9,9 @@
use Magento\Quote\Api\Data\AddressInterface;
use Magento\Quote\Model\Quote\Address\FreeShippingInterface;
+/**
+ * Collect totals for shipping.
+ */
class Shipping extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal
{
/**
@@ -111,7 +114,7 @@ public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Qu
{
$amount = $total->getShippingAmount();
$shippingDescription = $total->getShippingDescription();
- $title = ($amount != 0 && $shippingDescription)
+ $title = ($shippingDescription)
? __('Shipping & Handling (%1)', $shippingDescription)
: __('Shipping & Handling');
@@ -227,7 +230,7 @@ private function getAssignmentWeightData(AddressInterface $address, array $items
* @param bool $addressFreeShipping
* @param float $itemWeight
* @param float $itemQty
- * @param $freeShipping
+ * @param bool $freeShipping
* @return float
*/
private function getItemRowWeight(
diff --git a/app/code/Magento/Quote/Model/Quote/Item/Compare.php b/app/code/Magento/Quote/Model/Quote/Item/Compare.php
index ddaa636ef32b3..76ba324518dc1 100644
--- a/app/code/Magento/Quote/Model/Quote/Item/Compare.php
+++ b/app/code/Magento/Quote/Model/Quote/Item/Compare.php
@@ -50,7 +50,7 @@ protected function getOptionValues($value)
if (is_string($value) && $this->jsonValidator->isValid($value)) {
$value = $this->serializer->unserialize($value);
if (is_array($value)) {
- unset($value['qty'], $value['uenc']);
+ unset($value['qty'], $value['uenc'], $value['related_product'], $value['item']);
$value = array_filter($value, function ($optionValue) {
return !empty($optionValue);
});
diff --git a/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php b/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php
index 32687499274f8..6192d3471ccb0 100644
--- a/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php
+++ b/app/code/Magento/Quote/Model/Quote/Item/ToOrderItem.php
@@ -48,6 +48,8 @@ public function __construct(
}
/**
+ * Convert quote item(quote address item) into order item.
+ *
* @param Item|AddressItem $item
* @param array $data
* @return OrderItemInterface
@@ -63,6 +65,16 @@ public function convert($item, $data = [])
'to_order_item',
$item
);
+ if ($item instanceof \Magento\Quote\Model\Quote\Address\Item) {
+ $orderItemData = array_merge(
+ $orderItemData,
+ $this->objectCopyService->getDataFromFieldset(
+ 'quote_convert_address_item',
+ 'to_order_item',
+ $item
+ )
+ );
+ }
if (!$item->getNoDiscount()) {
$data = array_merge(
$data,
diff --git a/app/code/Magento/Quote/Model/QuoteAddressValidator.php b/app/code/Magento/Quote/Model/QuoteAddressValidator.php
index 9a86829bfc4ce..06f21ee119bd5 100644
--- a/app/code/Magento/Quote/Model/QuoteAddressValidator.php
+++ b/app/code/Magento/Quote/Model/QuoteAddressValidator.php
@@ -6,10 +6,13 @@
namespace Magento\Quote\Model;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Quote\Api\Data\AddressInterface;
+use Magento\Quote\Api\Data\CartInterface;
/**
* Quote shipping/billing address validator service.
*
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class QuoteAddressValidator
{
@@ -28,7 +31,7 @@ class QuoteAddressValidator
protected $customerRepository;
/**
- * @var \Magento\Customer\Model\Session
+ * @deprecated This class is not a part of HTML presentation layer and should not use sessions.
*/
protected $customerSession;
@@ -50,44 +53,68 @@ public function __construct(
}
/**
- * Validates the fields in a specified address data object.
+ * Validate address.
*
- * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object.
- * @return bool
- * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer.
- * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid.
+ * @param AddressInterface $address
+ * @param int|null $customerId Cart belongs to
+ * @return void
+ * @throws NoSuchEntityException The specified customer ID or address ID is not valid.
*/
- public function validate(\Magento\Quote\Api\Data\AddressInterface $addressData)
+ private function doValidate(AddressInterface $address, $customerId)
{
//validate customer id
- if ($addressData->getCustomerId()) {
- $customer = $this->customerRepository->getById($addressData->getCustomerId());
- if (!$customer->getId()) {
- throw new \Magento\Framework\Exception\NoSuchEntityException(
- __('Invalid customer id %1', $addressData->getCustomerId())
- );
+ if ($customerId) {
+ try {
+ $this->customerRepository->getById($customerId);
+ } catch (NoSuchEntityException $exception) {
+ throw new NoSuchEntityException(__('Invalid customer id %1', $customerId));
}
}
- if ($addressData->getCustomerAddressId()) {
+ if ($address->getCustomerAddressId()) {
+ //Existing address cannot belong to a guest
+ if (!$customerId) {
+ throw new NoSuchEntityException(__('Invalid customer address id %1', $address->getCustomerAddressId()));
+ }
+ //Validating address ID
try {
- $this->addressRepository->getById($addressData->getCustomerAddressId());
+ $this->addressRepository->getById($address->getCustomerAddressId());
} catch (NoSuchEntityException $e) {
- throw new \Magento\Framework\Exception\NoSuchEntityException(
- __('Invalid address id %1', $addressData->getId())
- );
+ throw new NoSuchEntityException(__('Invalid address id %1', $address->getId()));
}
-
+ //Finding available customer's addresses
$applicableAddressIds = array_map(function ($address) {
/** @var \Magento\Customer\Api\Data\AddressInterface $address */
return $address->getId();
- }, $this->customerRepository->getById($addressData->getCustomerId())->getAddresses());
- if (!in_array($addressData->getCustomerAddressId(), $applicableAddressIds)) {
- throw new \Magento\Framework\Exception\NoSuchEntityException(
- __('Invalid customer address id %1', $addressData->getCustomerAddressId())
- );
+ }, $this->customerRepository->getById($customerId)->getAddresses());
+ if (!in_array($address->getCustomerAddressId(), $applicableAddressIds)) {
+ throw new NoSuchEntityException(__('Invalid customer address id %1', $address->getCustomerAddressId()));
}
}
+ }
+
+ /**
+ * Validates the fields in a specified address data object.
+ *
+ * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object.
+ * @return bool
+ */
+ public function validate(AddressInterface $addressData): bool
+ {
+ $this->doValidate($addressData, $addressData->getCustomerId());
+
return true;
}
+
+ /**
+ * Validate address to be used for cart.
+ *
+ * @param CartInterface $cart
+ * @param AddressInterface $address
+ * @return void
+ */
+ public function validateForCart(CartInterface $cart, AddressInterface $address)
+ {
+ $this->doValidate($address, $cart->getCustomerIsGuest() ? null : $cart->getCustomer()->getId());
+ }
}
diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php
index e2ee8bbad01b9..79170ad90832c 100644
--- a/app/code/Magento/Quote/Model/QuoteManagement.php
+++ b/app/code/Magento/Quote/Model/QuoteManagement.php
@@ -25,6 +25,7 @@
/**
* Class QuoteManagement
*
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.TooManyFields)
*/
@@ -36,9 +37,9 @@ class QuoteManagement implements \Magento\Quote\Api\CartManagementInterface
protected $eventManager;
/**
- * @var QuoteValidator
+ * @var SubmitQuoteValidator
*/
- protected $quoteValidator;
+ private $submitQuoteValidator;
/**
* @var OrderFactory
@@ -147,7 +148,7 @@ class QuoteManagement implements \Magento\Quote\Api\CartManagementInterface
/**
* @param EventManager $eventManager
- * @param QuoteValidator $quoteValidator
+ * @param SubmitQuoteValidator $submitQuoteValidator
* @param OrderFactory $orderFactory
* @param OrderManagement $orderManagement
* @param CustomerManagement $customerManagement
@@ -172,7 +173,7 @@ class QuoteManagement implements \Magento\Quote\Api\CartManagementInterface
*/
public function __construct(
EventManager $eventManager,
- QuoteValidator $quoteValidator,
+ SubmitQuoteValidator $submitQuoteValidator,
OrderFactory $orderFactory,
OrderManagement $orderManagement,
CustomerManagement $customerManagement,
@@ -195,7 +196,7 @@ public function __construct(
\Magento\Customer\Api\AddressRepositoryInterface $addressRepository = null
) {
$this->eventManager = $eventManager;
- $this->quoteValidator = $quoteValidator;
+ $this->submitQuoteValidator = $submitQuoteValidator;
$this->orderFactory = $orderFactory;
$this->orderManagement = $orderManagement;
$this->customerManagement = $customerManagement;
@@ -281,6 +282,7 @@ public function assignCustomer($cartId, $customerId, $storeId)
throw new StateException(
__('Cannot assign customer to the given cart. Customer already has active cart.')
);
+ // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock
} catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
}
@@ -355,6 +357,13 @@ public function placeOrder($cartId, PaymentInterface $paymentMethod = null)
if ($quote->getCheckoutMethod() === self::METHOD_GUEST) {
$quote->setCustomerId(null);
$quote->setCustomerEmail($quote->getBillingAddress()->getEmail());
+ if ($quote->getCustomerFirstname() === null && $quote->getCustomerLastname() === null) {
+ $quote->setCustomerFirstname($quote->getBillingAddress()->getFirstname());
+ $quote->setCustomerLastname($quote->getBillingAddress()->getLastname());
+ if ($quote->getCustomerMiddlename() === null) {
+ $quote->setCustomerMiddlename($quote->getBillingAddress()->getMiddlename());
+ }
+ }
$quote->setCustomerIsGuest(true);
$quote->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID);
}
@@ -446,7 +455,7 @@ protected function resolveItems(QuoteEntity $quote)
protected function submitQuote(QuoteEntity $quote, $orderData = [])
{
$order = $this->orderFactory->create();
- $this->quoteValidator->validateBeforeSubmit($quote);
+ $this->submitQuoteValidator->validateQuote($quote);
if (!$quote->getCustomerIsGuest()) {
if ($quote->getCustomerId()) {
$this->_prepareCustomerQuote($quote);
@@ -501,6 +510,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = [])
$order->setCustomerFirstname($quote->getCustomerFirstname());
$order->setCustomerMiddlename($quote->getCustomerMiddlename());
$order->setCustomerLastname($quote->getCustomerLastname());
+ $this->submitQuoteValidator->validateOrder($order);
$this->eventManager->dispatch(
'sales_model_service_quote_submit_before',
@@ -521,19 +531,7 @@ protected function submitQuote(QuoteEntity $quote, $orderData = [])
);
$this->quoteRepository->save($quote);
} catch (\Exception $e) {
- if (!empty($this->addressesToSync)) {
- foreach ($this->addressesToSync as $addressId) {
- $this->addressRepository->deleteById($addressId);
- }
- }
- $this->eventManager->dispatch(
- 'sales_model_service_quote_submit_failure',
- [
- 'order' => $order,
- 'quote' => $quote,
- 'exception' => $e
- ]
- );
+ $this->rollbackAddresses($quote, $order, $e);
throw $e;
}
return $order;
@@ -600,4 +598,43 @@ protected function _prepareCustomerQuote($quote)
$shipping->setIsDefaultBilling(true);
}
}
+
+ /**
+ * Remove related to order and quote addresses and submit exception to further processing.
+ *
+ * @param Quote $quote
+ * @param \Magento\Sales\Api\Data\OrderInterface $order
+ * @param \Exception $e
+ * @throws \Exception
+ * @return void
+ */
+ private function rollbackAddresses(
+ QuoteEntity $quote,
+ \Magento\Sales\Api\Data\OrderInterface $order,
+ \Exception $e
+ ) {
+ try {
+ if (!empty($this->addressesToSync)) {
+ foreach ($this->addressesToSync as $addressId) {
+ $this->addressRepository->deleteById($addressId);
+ }
+ }
+ $this->eventManager->dispatch(
+ 'sales_model_service_quote_submit_failure',
+ [
+ 'order' => $order,
+ 'quote' => $quote,
+ 'exception' => $e,
+ ]
+ );
+ // phpcs:ignore Magento2.Exceptions.ThrowCatch
+ } catch (\Exception $consecutiveException) {
+ $message = sprintf(
+ "An exception occurred on 'sales_model_service_quote_submit_failure' event: %s",
+ $consecutiveException->getMessage()
+ );
+ // phpcs:ignore Magento2.Exceptions.DirectThrow
+ throw new \Exception($message, 0, $e);
+ }
+ }
}
diff --git a/app/code/Magento/Quote/Model/QuoteRepository.php b/app/code/Magento/Quote/Model/QuoteRepository.php
index 01c21bbbe50a7..30931821ddc7d 100644
--- a/app/code/Magento/Quote/Model/QuoteRepository.php
+++ b/app/code/Magento/Quote/Model/QuoteRepository.php
@@ -3,27 +3,33 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Quote\Model;
+use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface;
+use Magento\Framework\Api\Search\FilterGroup;
+use Magento\Framework\Api\SearchCriteria\CollectionProcessor;
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
+use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\App\ObjectManager;
-use Magento\Framework\Api\SortOrder;
+use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Quote\Api\Data\CartInterface;
-use Magento\Quote\Model\Quote;
-use Magento\Store\Model\StoreManagerInterface;
-use Magento\Framework\Api\Search\FilterGroup;
-use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection;
-use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory;
-use Magento\Framework\Exception\InputException;
-use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface;
+use Magento\Quote\Api\Data\CartInterfaceFactory;
+use Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory;
use Magento\Quote\Model\QuoteRepository\SaveHandler;
use Magento\Quote\Model\QuoteRepository\LoadHandler;
+use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection;
+use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory;
+use Magento\Store\Model\StoreManagerInterface;
/**
+ * Quote repository.
+ *
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface
+class QuoteRepository implements CartRepositoryInterface
{
/**
* @var Quote[]
@@ -37,6 +43,7 @@ class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface
/**
* @var QuoteFactory
+ * @deprecated
*/
protected $quoteFactory;
@@ -46,13 +53,13 @@ class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface
protected $storeManager;
/**
- * @var \Magento\Quote\Model\ResourceModel\Quote\Collection
+ * @var QuoteCollection
* @deprecated 100.2.0
*/
protected $quoteCollection;
/**
- * @var \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory
+ * @var CartSearchResultsInterfaceFactory
*/
protected $searchResultsDataFactory;
@@ -77,43 +84,51 @@ class QuoteRepository implements \Magento\Quote\Api\CartRepositoryInterface
private $collectionProcessor;
/**
- * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory
+ * @var QuoteCollectionFactory
*/
private $quoteCollectionFactory;
+ /**
+ * @var CartInterfaceFactory
+ */
+ private $cartFactory;
+
/**
* Constructor
*
* @param QuoteFactory $quoteFactory
* @param StoreManagerInterface $storeManager
- * @param \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteCollection
- * @param \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory $searchResultsDataFactory
+ * @param QuoteCollection $quoteCollection
+ * @param CartSearchResultsInterfaceFactory $searchResultsDataFactory
* @param JoinProcessorInterface $extensionAttributesJoinProcessor
* @param CollectionProcessorInterface|null $collectionProcessor
- * @param \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory|null $quoteCollectionFactory
+ * @param QuoteCollectionFactory|null $quoteCollectionFactory
+ * @param CartInterfaceFactory|null $cartFactory
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
QuoteFactory $quoteFactory,
StoreManagerInterface $storeManager,
- \Magento\Quote\Model\ResourceModel\Quote\Collection $quoteCollection,
- \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory $searchResultsDataFactory,
+ QuoteCollection $quoteCollection,
+ CartSearchResultsInterfaceFactory $searchResultsDataFactory,
JoinProcessorInterface $extensionAttributesJoinProcessor,
CollectionProcessorInterface $collectionProcessor = null,
- \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory $quoteCollectionFactory = null
+ QuoteCollectionFactory $quoteCollectionFactory = null,
+ CartInterfaceFactory $cartFactory = null
) {
$this->quoteFactory = $quoteFactory;
$this->storeManager = $storeManager;
$this->searchResultsDataFactory = $searchResultsDataFactory;
$this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor;
- $this->collectionProcessor = $collectionProcessor ?: \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Framework\Api\SearchCriteria\CollectionProcessor::class);
- $this->quoteCollectionFactory = $quoteCollectionFactory ?: \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Quote\Model\ResourceModel\Quote\CollectionFactory::class);
+ $this->collectionProcessor = $collectionProcessor ?: ObjectManager::getInstance()
+ ->get(CollectionProcessor::class);
+ $this->quoteCollectionFactory = $quoteCollectionFactory ?: ObjectManager::getInstance()
+ ->get(QuoteCollectionFactory::class);
+ $this->cartFactory = $cartFactory ?: ObjectManager::getInstance()->get(CartInterfaceFactory::class);
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function get($cartId, array $sharedStoreIds = [])
{
@@ -126,7 +141,7 @@ public function get($cartId, array $sharedStoreIds = [])
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getForCustomer($customerId, array $sharedStoreIds = [])
{
@@ -140,7 +155,7 @@ public function getForCustomer($customerId, array $sharedStoreIds = [])
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getActive($cartId, array $sharedStoreIds = [])
{
@@ -152,7 +167,7 @@ public function getActive($cartId, array $sharedStoreIds = [])
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getActiveForCustomer($customerId, array $sharedStoreIds = [])
{
@@ -164,9 +179,9 @@ public function getActiveForCustomer($customerId, array $sharedStoreIds = [])
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
- public function save(\Magento\Quote\Api\Data\CartInterface $quote)
+ public function save(CartInterface $quote)
{
if ($quote->getId()) {
$currentQuote = $this->get($quote->getId(), [$quote->getStoreId()]);
@@ -184,9 +199,9 @@ public function save(\Magento\Quote\Api\Data\CartInterface $quote)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
- public function delete(\Magento\Quote\Api\Data\CartInterface $quote)
+ public function delete(CartInterface $quote)
{
$quoteId = $quote->getId();
$customerId = $quote->getCustomerId();
@@ -203,13 +218,13 @@ public function delete(\Magento\Quote\Api\Data\CartInterface $quote)
* @param int $identifier
* @param int[] $sharedStoreIds
* @throws NoSuchEntityException
- * @return Quote
+ * @return CartInterface
*/
protected function loadQuote($loadMethod, $loadField, $identifier, array $sharedStoreIds = [])
{
- /** @var Quote $quote */
- $quote = $this->quoteFactory->create();
- if ($sharedStoreIds) {
+ /** @var CartInterface $quote */
+ $quote = $this->cartFactory->create();
+ if ($sharedStoreIds && method_exists($quote, 'setSharedStoreIds')) {
$quote->setSharedStoreIds($sharedStoreIds);
}
$quote->setStoreId($this->storeManager->getStore()->getId())->$loadMethod($identifier);
@@ -220,9 +235,9 @@ protected function loadQuote($loadMethod, $loadField, $identifier, array $shared
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
- public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria)
+ public function getList(SearchCriteriaInterface $searchCriteria)
{
$this->quoteCollection = $this->quoteCollectionFactory->create();
/** @var \Magento\Quote\Api\Data\CartSearchResultsInterface $searchData */
@@ -265,6 +280,7 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, QuoteCol
/**
* Get new SaveHandler dependency for application code.
+ *
* @return SaveHandler
* @deprecated 100.1.0
*/
@@ -277,6 +293,8 @@ private function getSaveHandler()
}
/**
+ * Get load handler instance.
+ *
* @return LoadHandler
* @deprecated 100.1.0
*/
diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php
index 309c89e3702f5..959604592c848 100644
--- a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php
+++ b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php
@@ -325,10 +325,10 @@ private function getOptionProductIds(
/**
* Check is valid product.
*
- * @param ProductInterface $product
+ * @param ProductInterface|null $product
* @return bool
*/
- private function isValidProduct(ProductInterface $product): bool
+ private function isValidProduct(ProductInterface $product = null): bool
{
$result = ($product && (int)$product->getStatus() !== ProductStatus::STATUS_DISABLED);
diff --git a/app/code/Magento/Quote/Model/ShippingAddressManagement.php b/app/code/Magento/Quote/Model/ShippingAddressManagement.php
index 0e2be5c9e3692..71a93e4604200 100644
--- a/app/code/Magento/Quote/Model/ShippingAddressManagement.php
+++ b/app/code/Magento/Quote/Model/ShippingAddressManagement.php
@@ -78,7 +78,7 @@ public function __construct(
}
/**
- * {@inheritDoc}
+ * @inheritdoc
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $address)
@@ -94,7 +94,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres
$saveInAddressBook = $address->getSaveInAddressBook() ? 1 : 0;
$sameAsBilling = $address->getSameAsBilling() ? 1 : 0;
$customerAddressId = $address->getCustomerAddressId();
- $this->addressValidator->validate($address);
+ $this->addressValidator->validateForCart($quote, $address);
$quote->setShippingAddress($address);
$address = $quote->getShippingAddress();
@@ -122,7 +122,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres
}
/**
- * {@inheritDoc}
+ * @inheritdoc
*/
public function get($cartId)
{
diff --git a/app/code/Magento/Quote/Model/SubmitQuoteValidator.php b/app/code/Magento/Quote/Model/SubmitQuoteValidator.php
new file mode 100644
index 0000000000000..76d31f94d2a62
--- /dev/null
+++ b/app/code/Magento/Quote/Model/SubmitQuoteValidator.php
@@ -0,0 +1,71 @@
+quoteValidator = $quoteValidator;
+ $this->orderAddressValidator = $orderAddressValidator;
+ }
+
+ /**
+ * Validates quote.
+ *
+ * @param Quote $quote
+ * @return void
+ * @throws LocalizedException
+ */
+ public function validateQuote(Quote $quote)
+ {
+ $this->quoteValidator->validateBeforeSubmit($quote);
+ }
+
+ /**
+ * Validates order.
+ *
+ * @param Order $order
+ * @return void
+ * @throws LocalizedException
+ */
+ public function validateOrder(Order $order)
+ {
+ foreach ($order->getAddresses() as $address) {
+ $errors = $this->orderAddressValidator->validate($address);
+ if (!empty($errors)) {
+ throw new LocalizedException(
+ __("Failed address validation:\n%1", implode("\n", $errors))
+ );
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php
new file mode 100644
index 0000000000000..0ac589b11b53b
--- /dev/null
+++ b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php
@@ -0,0 +1,71 @@
+quoteRepository = $quoteRepository;
+ $this->checkoutSession = $checkoutSession;
+ }
+
+ /**
+ * Update store id in active quote after store view switching.
+ *
+ * @param StoreSwitcherInterface $subject
+ * @param string $result
+ * @param StoreInterface $fromStore store where we came from
+ * @param StoreInterface $targetStore store where to go to
+ * @param string $redirectUrl original url requested for redirect after switching
+ * @return string url to be redirected after switching
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterSwitch(
+ StoreSwitcherInterface $subject,
+ string $result,
+ StoreInterface $fromStore,
+ StoreInterface $targetStore,
+ string $redirectUrl
+ ): string {
+ $quote = $this->checkoutSession->getQuote();
+ if ($quote->getIsActive()) {
+ $quote->setStoreId($targetStore->getId());
+ $quote->getItemsCollection(false);
+ $this->quoteRepository->save($quote);
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml
index a2f08353a4f3b..e5e7c3834bf7d 100644
--- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml
+++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml
@@ -22,6 +22,9 @@
+
+
+
@@ -70,7 +73,10 @@
+
+
+
@@ -79,13 +85,11 @@
-
-
@@ -94,8 +98,6 @@
-
-
@@ -113,10 +115,37 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 e25b770b7a81e..8784310d540bd 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
@@ -216,6 +216,7 @@ public function testValidateMiniumumAmountVirtual()
$scopeConfigValues = [
['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true],
['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20],
+ ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true],
['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true],
];
@@ -240,6 +241,31 @@ public function testValidateMiniumumAmount()
$scopeConfigValues = [
['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true],
['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20],
+ ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true],
+ ['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true],
+ ];
+
+ $this->quote->expects($this->once())
+ ->method('getStoreId')
+ ->willReturn($storeId);
+ $this->quote->expects($this->once())
+ ->method('getIsVirtual')
+ ->willReturn(false);
+
+ $this->scopeConfig->expects($this->once())
+ ->method('isSetFlag')
+ ->willReturnMap($scopeConfigValues);
+
+ $this->assertTrue($this->address->validateMinimumAmount());
+ }
+
+ public function testValidateMiniumumAmountWithoutDiscount()
+ {
+ $storeId = 1;
+ $scopeConfigValues = [
+ ['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true],
+ ['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20],
+ ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, false],
['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true],
];
@@ -263,6 +289,7 @@ public function testValidateMiniumumAmountNegative()
$scopeConfigValues = [
['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true],
['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20],
+ ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true],
['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true],
];
diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php
index 08f5f6a808561..c0ffbc997590e 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php
@@ -67,27 +67,22 @@ public function testValidateInvalidCustomer()
{
$customerId = 100;
$address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class);
- $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class);
$address->expects($this->atLeastOnce())->method('getCustomerId')->willReturn($customerId);
$this->customerRepositoryMock->expects($this->once())->method('getById')->with($customerId)
- ->willReturn($customerMock);
+ ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException());
$this->model->validate($address);
}
/**
* @expectedException \Magento\Framework\Exception\NoSuchEntityException
- * @expectedExceptionMessage Invalid address id 101
+ * @expectedExceptionMessage Invalid customer address id 101
*/
public function testValidateInvalidAddress()
{
$address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class);
$this->customerRepositoryMock->expects($this->never())->method('getById');
- $address->expects($this->atLeastOnce())->method('getCustomerAddressId')->willReturn(101);
- $address->expects($this->once())->method('getId')->willReturn(101);
-
- $this->addressRepositoryMock->expects($this->once())->method('getById')
- ->willThrowException(new \Magento\Framework\Exception\NoSuchEntityException());
+ $address->expects($this->exactly(2))->method('getCustomerAddressId')->willReturn(101);
$this->model->validate($address);
}
@@ -115,7 +110,6 @@ public function testValidateWithValidAddress()
$customerAddress = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class);
$this->customerRepositoryMock->expects($this->exactly(2))->method('getById')->willReturn($customerMock);
- $customerMock->expects($this->once())->method('getId')->willReturn($addressCustomer);
$this->addressRepositoryMock->expects($this->once())->method('getById')->willReturn($this->quoteAddressMock);
$this->quoteAddressMock->expects($this->any())->method('getCustomerId')->willReturn($addressCustomer);
diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php
index 145a18fb34ca3..aca490ae2b1a1 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php
@@ -13,6 +13,8 @@
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.TooManyFields)
+ * @SuppressWarnings(PHPMD.TooManyPublicMethods)
+ * @SuppressWarnings(PHPMD.ExcessiveClassLength)
*/
class QuoteManagementTest extends \PHPUnit\Framework\TestCase
{
@@ -22,9 +24,9 @@ class QuoteManagementTest extends \PHPUnit\Framework\TestCase
protected $model;
/**
- * @var \Magento\Quote\Model\QuoteValidator|\PHPUnit_Framework_MockObject_MockObject
+ * @var \Magento\Quote\Model\SubmitQuoteValidator|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $quoteValidator;
+ protected $submitQuoteValidator;
/**
* @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject
@@ -143,7 +145,7 @@ protected function setUp()
{
$objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
- $this->quoteValidator = $this->createMock(\Magento\Quote\Model\QuoteValidator::class);
+ $this->submitQuoteValidator = $this->createMock(\Magento\Quote\Model\SubmitQuoteValidator::class);
$this->eventManager = $this->getMockForAbstractClass(\Magento\Framework\Event\ManagerInterface::class);
$this->orderFactory = $this->createPartialMock(
\Magento\Sales\Api\Data\OrderInterfaceFactory::class,
@@ -210,7 +212,7 @@ protected function setUp()
\Magento\Quote\Model\QuoteManagement::class,
[
'eventManager' => $this->eventManager,
- 'quoteValidator' => $this->quoteValidator,
+ 'submitQuoteValidator' => $this->submitQuoteValidator,
'orderFactory' => $this->orderFactory,
'orderManagement' => $this->orderManagement,
'customerManagement' => $this->customerManagement,
@@ -560,7 +562,9 @@ public function testSubmit()
$shippingAddress
);
- $this->quoteValidator->expects($this->once())->method('validateBeforeSubmit')->with($quote);
+ $this->submitQuoteValidator->expects($this->once())
+ ->method('validateQuote')
+ ->with($quote);
$this->quoteAddressToOrder->expects($this->once())
->method('convert')
->with($shippingAddress, $orderData)
@@ -642,7 +646,7 @@ public function testPlaceOrderIfCustomerIsGuest()
$addressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getEmail']);
$addressMock->expects($this->once())->method('getEmail')->willReturn($email);
- $this->quoteMock->expects($this->once())->method('getBillingAddress')->with()->willReturn($addressMock);
+ $this->quoteMock->expects($this->any())->method('getBillingAddress')->with()->willReturn($addressMock);
$this->quoteMock->expects($this->once())->method('setCustomerIsGuest')->with(true)->willReturnSelf();
$this->quoteMock->expects($this->once())
@@ -655,7 +659,7 @@ public function testPlaceOrderIfCustomerIsGuest()
->setConstructorArgs(
[
'eventManager' => $this->eventManager,
- 'quoteValidator' => $this->quoteValidator,
+ 'quoteValidator' => $this->submitQuoteValidator,
'orderFactory' => $this->orderFactory,
'orderManagement' => $this->orderManagement,
'customerManagement' => $this->customerManagement,
@@ -712,7 +716,7 @@ public function testPlaceOrder()
->setConstructorArgs(
[
'eventManager' => $this->eventManager,
- 'quoteValidator' => $this->quoteValidator,
+ 'quoteValidator' => $this->submitQuoteValidator,
'orderFactory' => $this->orderFactory,
'orderManagement' => $this->orderManagement,
'customerManagement' => $this->customerManagement,
@@ -934,6 +938,9 @@ protected function prepareOrderFactory(
return $order;
}
+ /**
+ * @throws NoSuchEntityException
+ */
public function testGetCartForCustomer()
{
$customerId = 100;
@@ -978,6 +985,9 @@ protected function setPropertyValue(&$object, $property, $value)
return $object;
}
+ /**
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
public function testSubmitForCustomer()
{
$orderData = [];
@@ -1010,7 +1020,8 @@ public function testSubmitForCustomer()
$shippingAddress
);
- $this->quoteValidator->expects($this->once())->method('validateBeforeSubmit')->with($quote);
+ $this->submitQuoteValidator->method('validateQuote')
+ ->with($quote);
$this->quoteAddressToOrder->expects($this->once())
->method('convert')
->with($shippingAddress, $orderData)
diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php
index 3101c7d0677a9..095e1760df86f 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php
@@ -5,17 +5,31 @@
*/
namespace Magento\Quote\Test\Unit\Model;
+use Magento\Framework\Api\SearchCriteria;
+use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
use Magento\Framework\Api\SortOrder;
use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface;
+use Magento\Framework\ObjectManagerInterface;
+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Quote\Api\Data\CartInterface;
+use Magento\Quote\Api\Data\CartInterfaceFactory;
+use Magento\Quote\Api\Data\CartSearchResultsInterface;
+use Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory;
+use Magento\Quote\Model\Quote;
+use Magento\Quote\Model\QuoteRepository;
use Magento\Quote\Model\QuoteRepository\LoadHandler;
use Magento\Quote\Model\QuoteRepository\SaveHandler;
+use Magento\Quote\Model\ResourceModel\Quote\Collection;
use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory;
+use Magento\Store\Model\Store;
+use Magento\Store\Model\StoreManagerInterface;
+use PHPUnit\Framework\TestCase;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.TooManyMethods)
*/
-class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase
+class QuoteRepositoryTest extends TestCase
{
/**
* @var \Magento\Quote\Api\CartRepositoryInterface
@@ -23,32 +37,32 @@ class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase
private $model;
/**
- * @var \Magento\Quote\Model\QuoteFactory|\PHPUnit_Framework_MockObject_MockObject
+ * @var CartInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject
*/
- private $quoteFactoryMock;
+ private $cartFactoryMock;
/**
- * @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
private $storeManagerMock;
/**
- * @var \Magento\Store\Model\Store|\PHPUnit_Framework_MockObject_MockObject
+ * @var Store|\PHPUnit_Framework_MockObject_MockObject
*/
private $storeMock;
/**
- * @var \Magento\Quote\Model\Quote|\PHPUnit_Framework_MockObject_MockObject
+ * @var Quote|\PHPUnit_Framework_MockObject_MockObject
*/
private $quoteMock;
/**
- * @var \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject
+ * @var CartSearchResultsInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject
*/
private $searchResultsDataFactory;
/**
- * @var \Magento\Quote\Model\ResourceModel\Quote\Collection|\PHPUnit_Framework_MockObject_MockObject
+ * @var Collection|\PHPUnit_Framework_MockObject_MockObject
*/
private $quoteCollectionMock;
@@ -78,21 +92,21 @@ class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase
private $objectManagerMock;
/**
- * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject
+ * @var CollectionFactory|\PHPUnit_Framework_MockObject_MockObject
*/
private $quoteCollectionFactoryMock;
protected function setUp()
{
- $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+ $objectManager = new ObjectManager($this);
- $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class);
+ $this->objectManagerMock = $this->createMock(ObjectManagerInterface::class);
\Magento\Framework\App\ObjectManager::setInstance($this->objectManagerMock);
- $this->quoteFactoryMock = $this->createPartialMock(\Magento\Quote\Model\QuoteFactory::class, ['create']);
- $this->storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class);
+ $this->cartFactoryMock = $this->createPartialMock(CartInterfaceFactory::class, ['create']);
+ $this->storeManagerMock = $this->createMock(StoreManagerInterface::class);
$this->quoteMock = $this->createPartialMock(
- \Magento\Quote\Model\Quote::class,
+ Quote::class,
[
'load',
'loadByIdWithoutStore',
@@ -108,35 +122,35 @@ protected function setUp()
'getData'
]
);
- $this->storeMock = $this->createMock(\Magento\Store\Model\Store::class);
+ $this->storeMock = $this->createMock(Store::class);
$this->searchResultsDataFactory = $this->createPartialMock(
- \Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory::class,
+ CartSearchResultsInterfaceFactory::class,
['create']
);
$this->quoteCollectionMock =
- $this->createMock(\Magento\Quote\Model\ResourceModel\Quote\Collection::class);
+ $this->createMock(Collection::class);
$this->extensionAttributesJoinProcessorMock = $this->createMock(
- \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface::class
+ JoinProcessorInterface::class
);
$this->collectionProcessor = $this->createMock(
- \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class
+ CollectionProcessorInterface::class
);
$this->quoteCollectionFactoryMock = $this->createPartialMock(
- \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory::class,
+ CollectionFactory::class,
['create']
);
$this->model = $objectManager->getObject(
- \Magento\Quote\Model\QuoteRepository::class,
+ QuoteRepository::class,
[
- 'quoteFactory' => $this->quoteFactoryMock,
'storeManager' => $this->storeManagerMock,
'searchResultsDataFactory' => $this->searchResultsDataFactory,
'quoteCollection' => $this->quoteCollectionMock,
'extensionAttributesJoinProcessor' => $this->extensionAttributesJoinProcessorMock,
'collectionProcessor' => $this->collectionProcessor,
- 'quoteCollectionFactory' => $this->quoteCollectionFactoryMock
+ 'quoteCollectionFactory' => $this->quoteCollectionFactoryMock,
+ 'cartFactory' => $this->cartFactoryMock
]
);
@@ -161,7 +175,7 @@ public function testGetWithExceptionById()
{
$cartId = 14;
- $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
+ $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
$this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock);
$this->storeMock->expects($this->once())->method('getId')->willReturn(1);
$this->quoteMock->expects($this->never())->method('setSharedStoreIds');
@@ -178,7 +192,7 @@ public function testGet()
{
$cartId = 15;
- $this->quoteFactoryMock->expects(static::once())
+ $this->cartFactoryMock->expects(static::once())
->method('create')
->willReturn($this->quoteMock);
$this->storeManagerMock->expects(static::once())
@@ -211,7 +225,7 @@ public function testGetForCustomerAfterGet()
$cartId = 15;
$customerId = 23;
- $this->quoteFactoryMock->expects(static::exactly(2))
+ $this->cartFactoryMock->expects(static::exactly(2))
->method('create')
->willReturn($this->quoteMock);
$this->storeManagerMock->expects(static::exactly(2))
@@ -249,7 +263,7 @@ public function testGetWithSharedStoreIds()
$cartId = 16;
$sharedStoreIds = [1, 2];
- $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
+ $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
$this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock);
$this->storeMock->expects($this->once())->method('getId')->willReturn(1);
$this->quoteMock->expects($this->once())
@@ -275,7 +289,7 @@ public function testGetForCustomer()
$cartId = 17;
$customerId = 23;
- $this->quoteFactoryMock->expects(static::once())
+ $this->cartFactoryMock->expects(static::once())
->method('create')
->willReturn($this->quoteMock);
$this->storeManagerMock->expects(static::once())
@@ -310,7 +324,7 @@ public function testGetActiveWithExceptionById()
{
$cartId = 14;
- $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
+ $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
$this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock);
$this->storeMock->expects($this->once())->method('getId')->willReturn(1);
$this->quoteMock->expects($this->never())->method('setSharedStoreIds');
@@ -332,7 +346,7 @@ public function testGetActiveWithExceptionByIsActive()
{
$cartId = 15;
- $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
+ $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
$this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock);
$this->storeMock->expects($this->once())->method('getId')->willReturn(1);
$this->quoteMock->expects($this->never())->method('setSharedStoreIds');
@@ -355,7 +369,7 @@ public function testGetActive()
{
$cartId = 15;
- $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
+ $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
$this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock);
$this->storeMock->expects($this->once())->method('getId')->willReturn(1);
$this->quoteMock->expects($this->never())->method('setSharedStoreIds');
@@ -379,7 +393,7 @@ public function testGetActiveWithSharedStoreIds()
$cartId = 16;
$sharedStoreIds = [1, 2];
- $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
+ $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
$this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock);
$this->storeMock->expects($this->once())->method('getId')->willReturn(1);
$this->quoteMock->expects($this->once())
@@ -406,7 +420,7 @@ public function testGetActiveForCustomer()
$cartId = 17;
$customerId = 23;
- $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
+ $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
$this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock);
$this->storeMock->expects($this->once())->method('getId')->willReturn(1);
$this->quoteMock->expects($this->never())->method('setSharedStoreIds');
@@ -430,14 +444,14 @@ public function testSave()
{
$cartId = 100;
$quoteMock = $this->createPartialMock(
- \Magento\Quote\Model\Quote::class,
+ Quote::class,
['getId', 'getCustomerId', 'getStoreId', 'hasData', 'setData']
);
$quoteMock->expects($this->exactly(3))->method('getId')->willReturn($cartId);
$quoteMock->expects($this->once())->method('getCustomerId')->willReturn(2);
$quoteMock->expects($this->once())->method('getStoreId')->willReturn(5);
- $this->quoteFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
+ $this->cartFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteMock);
$this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock);
$this->storeMock->expects($this->once())->method('getId')->willReturn(1);
$this->quoteMock->expects($this->once())->method('getId')->willReturn($cartId);
@@ -481,8 +495,8 @@ public function testGetList()
->method('load')
->with($cartMock);
- $searchResult = $this->createMock(\Magento\Quote\Api\Data\CartSearchResultsInterface::class);
- $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteria::class);
+ $searchResult = $this->createMock(CartSearchResultsInterface::class);
+ $searchCriteriaMock = $this->createMock(SearchCriteria::class);
$this->searchResultsDataFactory
->expects($this->once())
->method('create')
@@ -495,7 +509,7 @@ public function testGetList()
$this->extensionAttributesJoinProcessorMock->expects($this->once())
->method('process')
->with(
- $this->isInstanceOf(\Magento\Quote\Model\ResourceModel\Quote\Collection::class)
+ $this->isInstanceOf(Collection::class)
);
$this->quoteCollectionMock->expects($this->atLeastOnce())->method('getItems')->willReturn([$cartMock]);
$searchResult->expects($this->once())->method('setTotalCount')->with($pageSize);
diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php
index 6f5e5937a32c8..9e921f744642f 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php
@@ -951,6 +951,7 @@ public function testValidateMiniumumAmount()
['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true],
['sales/minimum_order/multi_address', ScopeInterface::SCOPE_STORE, $storeId, true],
['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20],
+ ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true],
['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true],
];
$this->scopeConfig->expects($this->any())
@@ -977,6 +978,7 @@ public function testValidateMiniumumAmountNegative()
['sales/minimum_order/active', ScopeInterface::SCOPE_STORE, $storeId, true],
['sales/minimum_order/multi_address', ScopeInterface::SCOPE_STORE, $storeId, true],
['sales/minimum_order/amount', ScopeInterface::SCOPE_STORE, $storeId, 20],
+ ['sales/minimum_order/include_discount_amount', ScopeInterface::SCOPE_STORE, $storeId, true],
['sales/minimum_order/tax_including', ScopeInterface::SCOPE_STORE, $storeId, true],
];
$this->scopeConfig->expects($this->any())
diff --git a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php
index 59445c3999899..cc7cc49e11c81 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php
@@ -110,7 +110,7 @@ public function testSetAddressValidationFailed()
->with('cart654')
->will($this->returnValue($quoteMock));
- $this->validatorMock->expects($this->once())->method('validate')
+ $this->validatorMock->expects($this->once())->method('validateForCart')
->will($this->throwException(new \Magento\Framework\Exception\NoSuchEntityException(__('error345'))));
$this->service->assign('cart654', $this->quoteAddressMock);
@@ -143,8 +143,8 @@ public function testSetAddress()
->with($customerAddressId)
->willReturn($customerAddressMock);
- $this->validatorMock->expects($this->once())->method('validate')
- ->with($this->quoteAddressMock)
+ $this->validatorMock->expects($this->once())->method('validateForCart')
+ ->with($quoteMock, $this->quoteAddressMock)
->willReturn(true);
$quoteMock->expects($this->exactly(3))->method('getShippingAddress')->willReturn($this->quoteAddressMock);
@@ -218,8 +218,8 @@ public function testSetAddressWithInabilityToSaveQuote()
->with($customerAddressId)
->willReturn($customerAddressMock);
- $this->validatorMock->expects($this->once())->method('validate')
- ->with($this->quoteAddressMock)
+ $this->validatorMock->expects($this->once())->method('validateForCart')
+ ->with($quoteMock, $this->quoteAddressMock)
->willReturn(true);
$this->quoteAddressMock->expects($this->once())->method('getSaveInAddressBook')->willReturn(1);
diff --git a/app/code/Magento/Quote/Test/Unit/Plugin/UpdateQuoteItemStoreTest.php b/app/code/Magento/Quote/Test/Unit/Plugin/UpdateQuoteItemStoreTest.php
new file mode 100644
index 0000000000000..f6146c824aabc
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Unit/Plugin/UpdateQuoteItemStoreTest.php
@@ -0,0 +1,143 @@
+checkoutSessionMock = $this->createPartialMock(
+ Session::class,
+ ['getQuote']
+ );
+ $this->quoteMock = $this->createPartialMock(
+ Quote::class,
+ ['getIsActive', 'setStoreId', 'getItemsCollection']
+ );
+ $this->storeMock = $this->createPartialMock(
+ Store::class,
+ ['getId']
+ );
+ $this->quoteRepositoryMock = $this->createPartialMock(
+ QuoteRepository::class,
+ ['save']
+ );
+ $this->subjectMock = $this->createMock(StoreSwitcherInterface::class);
+
+ $this->checkoutSessionMock->expects($this->once())->method('getQuote')->willReturn($this->quoteMock);
+
+ $this->model = $objectManager->getObject(
+ UpdateQuoteItemStore::class,
+ [
+ 'quoteRepository' => $this->quoteRepositoryMock,
+ 'checkoutSession' => $this->checkoutSessionMock,
+ ]
+ );
+ }
+
+ /**
+ * Unit test for afterSwitch method with active quote.
+ *
+ * @return void
+ */
+ public function testWithActiveQuote()
+ {
+ $storeId = 1;
+ $this->quoteMock->expects($this->once())->method('getIsActive')->willReturn(true);
+ $this->storeMock->expects($this->once())->method('getId')->willReturn($storeId);
+ $this->quoteMock->expects($this->once())->method('setStoreId')->with($storeId)->willReturnSelf();
+ $quoteItem = $this->createMock(Item::class);
+ $this->quoteMock->expects($this->once())->method('getItemsCollection')->willReturnSelf($quoteItem);
+
+ $this->model->afterSwitch(
+ $this->subjectMock,
+ 'magento2.loc',
+ $this->storeMock,
+ $this->storeMock,
+ 'magento2.loc'
+ );
+ }
+
+ /**
+ * Unit test for afterSwitch method without active quote.
+ *
+ * @dataProvider getIsActive
+ * @param bool|null $isActive
+ * @return void
+ */
+ public function testWithoutActiveQuote($isActive)
+ {
+ $this->quoteMock->expects($this->once())->method('getIsActive')->willReturn($isActive);
+ $this->quoteRepositoryMock->expects($this->never())->method('save');
+
+ $this->model->afterSwitch(
+ $this->subjectMock,
+ 'magento2.loc',
+ $this->storeMock,
+ $this->storeMock,
+ 'magento2.loc'
+ );
+ }
+
+ /**
+ * Data provider for method testWithoutActiveQuote.
+ * @return array
+ */
+ public function getIsActive()
+ {
+ return [
+ [false],
+ [null],
+ ];
+ }
+}
diff --git a/app/code/Magento/Quote/composer.json b/app/code/Magento/Quote/composer.json
index 5391d7779b420..202a5b601ceab 100644
--- a/app/code/Magento/Quote/composer.json
+++ b/app/code/Magento/Quote/composer.json
@@ -23,7 +23,7 @@
"magento/module-webapi": "100.2.*"
},
"type": "magento2-module",
- "version": "101.0.6",
+ "version": "101.0.7",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Quote/etc/fieldset.xml b/app/code/Magento/Quote/etc/fieldset.xml
index 55ec76a647fcd..85ee20c7f8520 100644
--- a/app/code/Magento/Quote/etc/fieldset.xml
+++ b/app/code/Magento/Quote/etc/fieldset.xml
@@ -186,6 +186,11 @@
+
+
+
+
+
diff --git a/app/code/Magento/Quote/etc/frontend/di.xml b/app/code/Magento/Quote/etc/frontend/di.xml
index 25acd6763ba56..91f4cfbf60aba 100644
--- a/app/code/Magento/Quote/etc/frontend/di.xml
+++ b/app/code/Magento/Quote/etc/frontend/di.xml
@@ -12,6 +12,9 @@
Magento\Checkout\Model\Session\Proxy
+
+
+
diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php
index 1f90309721c23..9c80f6aa423b8 100644
--- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php
+++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php
@@ -6,23 +6,58 @@
namespace Magento\Reports\Block\Adminhtml\Sales\Sales;
+use Magento\Framework\DataObject;
use Magento\Reports\Block\Adminhtml\Grid\Column\Renderer\Currency;
+use Magento\Framework\App\ObjectManager;
+use Magento\Sales\Model\Order\ConfigFactory;
+use Magento\Sales\Model\Order;
/**
* Adminhtml sales report grid block
*
- * @author Magento Core Team
* @SuppressWarnings(PHPMD.DepthOfInheritance)
*/
class Grid extends \Magento\Reports\Block\Adminhtml\Grid\AbstractGrid
{
/**
- * GROUP BY criteria
- *
* @var string
*/
protected $_columnGroupBy = 'period';
+ /**
+ * @var ConfigFactory
+ */
+ private $configFactory;
+
+ /**
+ * @param \Magento\Backend\Block\Template\Context $context
+ * @param \Magento\Backend\Helper\Data $backendHelper
+ * @param \Magento\Reports\Model\ResourceModel\Report\Collection\Factory $resourceFactory
+ * @param \Magento\Reports\Model\Grouped\CollectionFactory $collectionFactory
+ * @param \Magento\Reports\Helper\Data $reportsData
+ * @param array $data
+ * @param ConfigFactory|null $configFactory
+ */
+ public function __construct(
+ \Magento\Backend\Block\Template\Context $context,
+ \Magento\Backend\Helper\Data $backendHelper,
+ \Magento\Reports\Model\ResourceModel\Report\Collection\Factory $resourceFactory,
+ \Magento\Reports\Model\Grouped\CollectionFactory $collectionFactory,
+ \Magento\Reports\Helper\Data $reportsData,
+ array $data = [],
+ ConfigFactory $configFactory = null
+ ) {
+ parent::__construct(
+ $context,
+ $backendHelper,
+ $resourceFactory,
+ $collectionFactory,
+ $reportsData,
+ $data
+ );
+ $this->configFactory = $configFactory ?: ObjectManager::getInstance()->get(ConfigFactory::class);
+ }
+
/**
* {@inheritdoc}
* @codeCoverageIgnore
@@ -328,4 +363,31 @@ protected function _prepareColumns()
return parent::_prepareColumns();
}
+
+ /**
+ * @inheritdoc
+ *
+ * Filter canceled statuses for orders.
+ *
+ * @return Grid
+ */
+ protected function _prepareCollection()
+ {
+ /** @var DataObject $filterData */
+ $filterData = $this->getData('filter_data');
+ if (!$filterData->hasData('order_statuses')) {
+ $orderConfig = $this->configFactory->create();
+ $statusValues = [];
+ $canceledStatuses = $orderConfig->getStateStatuses(Order::STATE_CANCELED);
+ $statusCodes = array_keys($orderConfig->getStatuses());
+ foreach ($statusCodes as $code) {
+ if (!isset($canceledStatuses[$code])) {
+ $statusValues[] = $code;
+ }
+ }
+ $filterData->setData('order_statuses', $statusValues);
+ }
+
+ return parent::_prepareCollection();
+ }
}
diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php
index 68f2722ca6dfb..b1f5628a0b7a3 100644
--- a/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php
+++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/AbstractReport.php
@@ -147,15 +147,19 @@ protected function _showLastExecutionTime($flagCode, $refreshCode)
}
$refreshStatsLink = $this->getUrl('reports/report_statistics');
- $directRefreshLink = $this->getUrl('reports/report_statistics/refreshRecent', ['code' => $refreshCode]);
+ $directRefreshLink = $this->getUrl('reports/report_statistics/refreshRecent');
$this->messageManager->addNotice(
__(
'Last updated: %1. To refresh last day\'s statistics , ' .
- 'click here .',
+ 'click here .',
$updatedAt,
$refreshStatsLink,
- $directRefreshLink
+ str_replace(
+ '"',
+ '"',
+ json_encode(['action' => $directRefreshLink, 'data' => ['code' => $refreshCode]])
+ )
)
);
return $this;
diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php
index 1f0f6e8e40535..957b1160d0281 100644
--- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php
+++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Statistics/RefreshRecent.php
@@ -6,15 +6,22 @@
*/
namespace Magento\Reports\Controller\Adminhtml\Report\Statistics;
+use Magento\Framework\Exception\NotFoundException;
+
class RefreshRecent extends \Magento\Reports\Controller\Adminhtml\Report\Statistics
{
/**
* Refresh statistics for last 25 hours
*
* @return void
+ * @throws NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found.'));
+ }
+
try {
$collectionsNames = $this->_getCollectionNames();
/** @var \DateTime $currentDate */
diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php
index 82ebc74a0468e..d89a118bff94b 100644
--- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php
+++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php
@@ -3,7 +3,6 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-
namespace Magento\Reports\Model\ResourceModel\Order;
use Magento\Framework\DB\Select;
@@ -81,7 +80,7 @@ class Collection extends \Magento\Sales\Model\ResourceModel\Order\Collection
* @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
* @param \Magento\Sales\Model\Order\Config $orderConfig
* @param \Magento\Sales\Model\ResourceModel\Report\OrderFactory $reportOrderFactory
- * @param null $connection
+ * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
* @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource
*
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
@@ -446,7 +445,7 @@ public function getDateRange($range, $customStart, $customEnd, $returnObjects =
break;
case 'custom':
- $dateStart = $customStart ? $customStart : $dateEnd;
+ $dateStart = $customStart ? $customStart : $dateStart;
$dateEnd = $customEnd ? $customEnd : $dateEnd;
break;
@@ -770,11 +769,12 @@ public function addOrdersCount()
*/
public function addRevenueToSelect($convertCurrency = false)
{
- $expr = $this->getTotalsExpression(
+ $expr = $this->getTotalsExpressionWithDiscountRefunded(
!$convertCurrency,
$this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0),
$this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0),
- $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0)
+ $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0),
+ $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0)
);
$this->getSelect()->columns(['revenue' => $expr]);
@@ -792,11 +792,12 @@ public function addSumAvgTotals($storeId = 0)
/**
* calculate average and total amount
*/
- $expr = $this->getTotalsExpression(
+ $expr = $this->getTotalsExpressionWithDiscountRefunded(
$storeId,
$this->getConnection()->getIfNullSql('main_table.base_subtotal_refunded', 0),
$this->getConnection()->getIfNullSql('main_table.base_subtotal_canceled', 0),
- $this->getConnection()->getIfNullSql('main_table.base_discount_canceled', 0)
+ $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_refunded)', 0),
+ $this->getConnection()->getIfNullSql('ABS(main_table.base_discount_canceled)', 0)
);
$this->getSelect()->columns(
@@ -809,13 +810,15 @@ public function addSumAvgTotals($storeId = 0)
}
/**
- * Get SQL expression for totals
+ * Get SQL expression for totals.
*
* @param int $storeId
* @param string $baseSubtotalRefunded
* @param string $baseSubtotalCanceled
* @param string $baseDiscountCanceled
* @return string
+ * @deprecated
+ * @see getTotalsExpressionWithDiscountRefunded
*/
protected function getTotalsExpression(
$storeId,
@@ -825,11 +828,41 @@ protected function getTotalsExpression(
) {
$template = ($storeId != 0)
? '(main_table.base_subtotal - %2$s - %1$s - ABS(main_table.base_discount_amount) - %3$s)'
- : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) - %3$s) '
- . ' * main_table.base_to_global_rate)';
+ : '((main_table.base_subtotal - %1$s - %2$s - ABS(main_table.base_discount_amount) + %3$s) '
+ . ' * main_table.base_to_global_rate)';
return sprintf($template, $baseSubtotalRefunded, $baseSubtotalCanceled, $baseDiscountCanceled);
}
+ /**
+ * Get SQL expression for totals with discount refunded.
+ *
+ * @param int $storeId
+ * @param string $baseSubtotalRefunded
+ * @param string $baseSubtotalCanceled
+ * @param string $baseDiscountRefunded
+ * @param string $baseDiscountCanceled
+ * @return string
+ */
+ private function getTotalsExpressionWithDiscountRefunded(
+ $storeId,
+ $baseSubtotalRefunded,
+ $baseSubtotalCanceled,
+ $baseDiscountRefunded,
+ $baseDiscountCanceled
+ ) {
+ $template = ($storeId != 0)
+ ? '(main_table.base_subtotal - %2$s - %1$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s))'
+ : '((main_table.base_subtotal - %1$s - %2$s - (ABS(main_table.base_discount_amount) - %3$s - %4$s)) '
+ . ' * main_table.base_to_global_rate)';
+ return sprintf(
+ $template,
+ $baseSubtotalRefunded,
+ $baseSubtotalCanceled,
+ $baseDiscountRefunded,
+ $baseDiscountCanceled
+ );
+ }
+
/**
* Sort order by total amount
*
diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php
index 1985db0b90e2a..2009cd3ff9d92 100644
--- a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php
+++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php
@@ -4,16 +4,16 @@
* See COPYING.txt for license details.
*/
+namespace Magento\Reports\Model\ResourceModel\Product\Downloads;
+
/**
* Product Downloads Report collection
*
* @author Magento Core Team
- */
-namespace Magento\Reports\Model\ResourceModel\Product\Downloads;
-
-/**
+ *
* @api
* @since 100.0.2
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
{
@@ -97,4 +97,14 @@ public function addFieldToFilter($field, $condition = null)
}
return $this;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function getSelectCountSql()
+ {
+ $countSelect = parent::getSelectCountSql();
+ $countSelect->reset(\Zend\Db\Sql\Select::GROUP);
+ return $countSelect;
+ }
}
diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml
new file mode 100644
index 0000000000000..8ddfd4092645f
--- /dev/null
+++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Page/AdminOrdersReportPage.xml b/app/code/Magento/Reports/Test/Mftf/Page/AdminOrdersReportPage.xml
new file mode 100644
index 0000000000000..f0b51f6e39357
--- /dev/null
+++ b/app/code/Magento/Reports/Test/Mftf/Page/AdminOrdersReportPage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportFilterSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportFilterSection.xml
new file mode 100644
index 0000000000000..33527e1262020
--- /dev/null
+++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportFilterSection.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportMainActionsSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportMainActionsSection.xml
new file mode 100644
index 0000000000000..c4a96537740ee
--- /dev/null
+++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportMainActionsSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportTableSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportTableSection.xml
new file mode 100644
index 0000000000000..e920d28bcf386
--- /dev/null
+++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportTableSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml
new file mode 100644
index 0000000000000..009e4b8e5f6f1
--- /dev/null
+++ b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Reports/composer.json b/app/code/Magento/Reports/composer.json
index 1abd03339a22a..f9c0c3568b077 100644
--- a/app/code/Magento/Reports/composer.json
+++ b/app/code/Magento/Reports/composer.json
@@ -22,7 +22,7 @@
"magento/framework": "101.0.*"
},
"type": "magento2-module",
- "version": "100.2.7",
+ "version": "100.2.8",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit.php b/app/code/Magento/Review/Block/Adminhtml/Edit.php
index d6868eae6fcbc..d4079bbed3e3c 100644
--- a/app/code/Magento/Review/Block/Adminhtml/Edit.php
+++ b/app/code/Magento/Review/Block/Adminhtml/Edit.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Review\Block\Adminhtml;
/**
@@ -56,6 +57,7 @@ public function __construct(
*
* @return void
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ * @SuppressWarnings(PHPMD.RequestAwareBlockMethod)
*/
protected function _construct()
{
@@ -159,16 +161,16 @@ protected function _construct()
}
if ($this->getRequest()->getParam('ret', false) == 'pending') {
- $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('catalog/*/pending') . '\')');
+ $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('review/*/pending') . '\')');
$this->buttonList->update(
'delete',
'onclick',
'deleteConfirm(' . '\'' . __(
'Are you sure you want to do this?'
- ) . '\' ' . '\'' . $this->getUrl(
+ ) . '\', ' . '\'' . $this->getUrl(
'*/*/delete',
[$this->_objectId => $this->getRequest()->getParam($this->_objectId), 'ret' => 'pending']
- ) . '\'' . ')'
+ ) . '\'' . ', {data: {}})'
);
$this->_coreRegistry->register('ret', 'pending');
}
diff --git a/app/code/Magento/Review/Block/Product/ReviewRenderer.php b/app/code/Magento/Review/Block/Product/ReviewRenderer.php
index 3cd15aba30420..3183196ebf30c 100644
--- a/app/code/Magento/Review/Block/Product/ReviewRenderer.php
+++ b/app/code/Magento/Review/Block/Product/ReviewRenderer.php
@@ -9,7 +9,11 @@
use Magento\Catalog\Block\Product\ReviewRendererInterface;
use Magento\Catalog\Model\Product;
+use Magento\Review\Observer\PredispatchReviewObserver;
+/**
+ * Class ReviewRenderer
+ */
class ReviewRenderer extends \Magento\Framework\View\Element\Template implements ReviewRendererInterface
{
/**
@@ -43,6 +47,19 @@ public function __construct(
parent::__construct($context, $data);
}
+ /**
+ * Review module availability
+ *
+ * @return string
+ */
+ public function isReviewEnabled() : string
+ {
+ return $this->_scopeConfig->getValue(
+ PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+ );
+ }
+
/**
* Get review summary html
*
diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php
index 75015d65e1a18..68f178911dc7c 100644
--- a/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php
+++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Delete.php
@@ -18,20 +18,23 @@ public function execute()
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
$reviewId = $this->getRequest()->getParam('id', false);
- try {
- $this->reviewFactory->create()->setId($reviewId)->aggregate()->delete();
+ if ($this->getRequest()->isPost()) {
+ try {
+ $this->reviewFactory->create()->setId($reviewId)->aggregate()->delete();
- $this->messageManager->addSuccess(__('The review has been deleted.'));
- if ($this->getRequest()->getParam('ret') == 'pending') {
- $resultRedirect->setPath('review/*/pending');
- } else {
- $resultRedirect->setPath('review/*/');
+ $this->messageManager->addSuccess(__('The review has been deleted.'));
+ if ($this->getRequest()->getParam('ret') == 'pending') {
+ $resultRedirect->setPath('review/*/pending');
+ } else {
+ $resultRedirect->setPath('review/*/');
+ }
+
+ return $resultRedirect;
+ } catch (\Magento\Framework\Exception\LocalizedException $e) {
+ $this->messageManager->addError($e->getMessage());
+ } catch (\Exception $e) {
+ $this->messageManager->addException($e, __('Something went wrong deleting this review.'));
}
- return $resultRedirect;
- } catch (\Magento\Framework\Exception\LocalizedException $e) {
- $this->messageManager->addError($e->getMessage());
- } catch (\Exception $e) {
- $this->messageManager->addException($e, __('Something went wrong deleting this review.'));
}
return $resultRedirect->setPath('review/*/edit/', ['id' => $reviewId]);
diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php
index c792540000233..954c393276c14 100644
--- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php
+++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassDelete.php
@@ -13,9 +13,14 @@ class MassDelete extends ProductController
{
/**
* @return \Magento\Backend\Model\View\Result\Redirect
+ * @throws \Magento\Framework\Exception\NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.'));
+ }
+
$reviewsIds = $this->getRequest()->getParam('reviews');
if (!is_array($reviewsIds)) {
$this->messageManager->addError(__('Please select review(s).'));
diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php
index 2769a35ba9a48..a5850a6896321 100644
--- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php
+++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassUpdateStatus.php
@@ -13,9 +13,14 @@ class MassUpdateStatus extends ProductController
{
/**
* @return \Magento\Backend\Model\View\Result\Redirect
+ * @throws \Magento\Framework\Exception\NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.'));
+ }
+
$reviewsIds = $this->getRequest()->getParam('reviews');
if (!is_array($reviewsIds)) {
$this->messageManager->addError(__('Please select review(s).'));
diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php b/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php
index eca37d3fe24da..759ec36b9e834 100644
--- a/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php
+++ b/app/code/Magento/Review/Controller/Adminhtml/Product/MassVisibleIn.php
@@ -13,9 +13,14 @@ class MassVisibleIn extends ProductController
{
/**
* @return \Magento\Backend\Model\View\Result\Redirect
+ * @throws \Magento\Framework\Exception\NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new \Magento\Framework\Exception\NotFoundException(__('Page not found.'));
+ }
+
$reviewsIds = $this->getRequest()->getParam('reviews');
if (!is_array($reviewsIds)) {
$this->messageManager->addError(__('Please select review(s).'));
diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php
index 7159b1825dc4d..857f36b19a19c 100644
--- a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php
+++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php
@@ -9,9 +9,14 @@
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\Exception\LocalizedException;
+/**
+ * Save Review action.
+ */
class Save extends ProductController
{
/**
+ * Save Review action.
+ *
* @return \Magento\Backend\Model\View\Result\Redirect
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
@@ -63,7 +68,7 @@ public function execute()
if ($nextId) {
$resultRedirect->setPath('review/*/edit', ['id' => $nextId]);
} elseif ($this->getRequest()->getParam('ret') == 'pending') {
- $resultRedirect->setPath('*/*/pending');
+ $resultRedirect->setPath('review/*/pending');
} else {
$resultRedirect->setPath('*/*/');
}
diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php b/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php
index 5535c3de26e43..c5610d135222a 100644
--- a/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php
+++ b/app/code/Magento/Review/Controller/Adminhtml/Rating/Delete.php
@@ -5,6 +5,7 @@
*/
namespace Magento\Review\Controller\Adminhtml\Rating;
+use Magento\Framework\Exception\NotFoundException;
use Magento\Review\Controller\Adminhtml\Rating as RatingController;
use Magento\Framework\Controller\ResultFactory;
@@ -12,19 +13,25 @@ class Delete extends RatingController
{
/**
* @return \Magento\Backend\Model\View\Result\Redirect
+ * @throws NotFoundException
*/
public function execute()
{
+ if (!$this->getRequest()->isPost()) {
+ throw new NotFoundException(__('Page not found'));
+ }
+
/** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
- if ($this->getRequest()->getParam('id') > 0) {
+ $ratingId = (int)$this->getRequest()->getParam('id');
+ if ($ratingId) {
try {
/** @var \Magento\Review\Model\Rating $model */
$model = $this->_objectManager->create(\Magento\Review\Model\Rating::class);
- $model->load($this->getRequest()->getParam('id'))->delete();
- $this->messageManager->addSuccess(__('You deleted the rating.'));
+ $model->load($ratingId)->delete();
+ $this->messageManager->addSuccessMessage(__('You deleted the rating.'));
} catch (\Exception $e) {
- $this->messageManager->addError($e->getMessage());
+ $this->messageManager->addErrorMessage($e->getMessage());
$resultRedirect->setPath('review/rating/edit', ['id' => $this->getRequest()->getParam('id')]);
return $resultRedirect;
}
diff --git a/app/code/Magento/Review/Controller/Product/Post.php b/app/code/Magento/Review/Controller/Product/Post.php
index be18f8fe25bbe..67c38f25d7ce3 100644
--- a/app/code/Magento/Review/Controller/Product/Post.php
+++ b/app/code/Magento/Review/Controller/Product/Post.php
@@ -22,7 +22,7 @@ public function execute()
{
/** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */
$resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
- if (!$this->formKeyValidator->validate($this->getRequest())) {
+ if (!$this->getRequest()->isPost() || !$this->formKeyValidator->validate($this->getRequest())) {
$resultRedirect->setUrl($this->_redirect->getRefererUrl());
return $resultRedirect;
}
diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating.php b/app/code/Magento/Review/Model/ResourceModel/Rating.php
index 3f54c17f6ff7c..5567c21ba12ee 100644
--- a/app/code/Magento/Review/Model/ResourceModel/Rating.php
+++ b/app/code/Magento/Review/Model/ResourceModel/Rating.php
@@ -5,6 +5,9 @@
*/
namespace Magento\Review\Model\ResourceModel;
+use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\App\ObjectManager;
+
/**
* Rating resource model
*
@@ -12,6 +15,7 @@
*
* @author Magento Core Team
* @since 100.0.2
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
@@ -34,13 +38,19 @@ class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
*/
protected $_logger;
+ /**
+ * @var ScopeConfigInterface
+ */
+ private $scopeConfig;
+
/**
* @param \Magento\Framework\Model\ResourceModel\Db\Context $context
* @param \Psr\Log\LoggerInterface $logger
* @param \Magento\Framework\Module\Manager $moduleManager
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
- * @param \Magento\Review\Model\ResourceModel\Review\Summary $reviewSummary
+ * @param Review\Summary $reviewSummary
* @param string $connectionName
+ * @param ScopeConfigInterface|null $scopeConfig
*/
public function __construct(
\Magento\Framework\Model\ResourceModel\Db\Context $context,
@@ -48,12 +58,14 @@ public function __construct(
\Magento\Framework\Module\Manager $moduleManager,
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Review\Model\ResourceModel\Review\Summary $reviewSummary,
- $connectionName = null
+ $connectionName = null,
+ ScopeConfigInterface $scopeConfig = null
) {
$this->moduleManager = $moduleManager;
$this->_storeManager = $storeManager;
$this->_logger = $logger;
$this->_reviewSummary = $reviewSummary;
+ $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class);
parent::__construct($context, $connectionName);
}
@@ -178,6 +190,8 @@ protected function _afterSave(\Magento\Framework\Model\AbstractModel $object)
}
/**
+ * Process rating codes.
+ *
* @param \Magento\Framework\Model\AbstractModel $object
* @return $this
*/
@@ -201,6 +215,8 @@ protected function processRatingCodes(\Magento\Framework\Model\AbstractModel $ob
}
/**
+ * Process rating stores.
+ *
* @param \Magento\Framework\Model\AbstractModel $object
* @return $this
*/
@@ -224,6 +240,8 @@ protected function processRatingStores(\Magento\Framework\Model\AbstractModel $o
}
/**
+ * Delete rating data.
+ *
* @param int $ratingId
* @param string $table
* @param array $storeIds
@@ -247,6 +265,8 @@ protected function deleteRatingData($ratingId, $table, array $storeIds)
}
/**
+ * Insert rating data.
+ *
* @param string $table
* @param array $data
* @return void
@@ -269,6 +289,7 @@ protected function insertRatingData($table, array $data)
/**
* Perform actions after object delete
+ *
* Prepare rating data for reaggregate all data for reviews
*
* @param \Magento\Framework\Model\AbstractModel $object
@@ -277,7 +298,12 @@ protected function insertRatingData($table, array $data)
protected function _afterDelete(\Magento\Framework\Model\AbstractModel $object)
{
parent::_afterDelete($object);
- if (!$this->moduleManager->isEnabled('Magento_Review')) {
+ if (!$this->moduleManager->isEnabled('Magento_Review') &&
+ !$this->scopeConfig->getValue(
+ \Magento\Review\Observer\PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+ )
+ ) {
return $this;
}
$data = $this->_getEntitySummaryData($object);
@@ -425,9 +451,11 @@ public function getReviewSummary($object, $onlyForCurrentStore = true)
$data = $connection->fetchAll($select, [':review_id' => $object->getReviewId()]);
+ $currentStore = $this->_storeManager->isSingleStoreMode() ? $this->_storeManager->getStore()->getId() : null;
+
if ($onlyForCurrentStore) {
foreach ($data as $row) {
- if ($row['store_id'] == $this->_storeManager->getStore()->getId()) {
+ if ($row['store_id'] !== $currentStore) {
$object->addData($row);
}
}
diff --git a/app/code/Magento/Review/Observer/PredispatchReviewObserver.php b/app/code/Magento/Review/Observer/PredispatchReviewObserver.php
new file mode 100644
index 0000000000000..bdca0f5ecb1ec
--- /dev/null
+++ b/app/code/Magento/Review/Observer/PredispatchReviewObserver.php
@@ -0,0 +1,72 @@
+scopeConfig = $scopeConfig;
+ $this->url = $url;
+ }
+ /**
+ * Redirect review routes to 404 when review module is disabled.
+ *
+ * @param Observer $observer
+ */
+ public function execute(Observer $observer)
+ {
+ if (!$this->scopeConfig->getValue(
+ self::XML_PATH_REVIEW_ACTIVE,
+ ScopeInterface::SCOPE_STORE
+ )
+ ) {
+ $defaultNoRouteUrl = $this->scopeConfig->getValue(
+ 'web/default/no_route',
+ ScopeInterface::SCOPE_STORE
+ );
+ $redirectUrl = $this->url->getUrl($defaultNoRouteUrl);
+ $observer->getControllerAction()
+ ->getResponse()
+ ->setRedirect($redirectUrl);
+ }
+ }
+}
diff --git a/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php b/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php
index 1526e80f8190a..73e85a7cdc179 100644
--- a/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php
+++ b/app/code/Magento/Review/Test/Unit/Controller/Product/PostTest.php
@@ -105,7 +105,8 @@ class PostTest extends \PHPUnit\Framework\TestCase
protected function setUp()
{
$this->redirect = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class);
- $this->request = $this->createPartialMock(\Magento\Framework\App\Request\Http::class, ['getParam']);
+ $this->request = $this->createPartialMock(\Magento\Framework\App\Request\Http::class, ['getParam', 'isPost']);
+ $this->request->expects($this->any())->method('isPost')->willReturn(true);
$this->response = $this->createPartialMock(\Magento\Framework\App\Response\Http::class, ['setRedirect']);
$this->formKeyValidator = $this->createPartialMock(
\Magento\Framework\Data\Form\FormKey\Validator::class,
@@ -215,12 +216,12 @@ public function testExecute()
$this->reviewSession->expects($this->any())->method('getFormData')
->with(true)
->willReturn($reviewData);
- $this->request->expects($this->at(0))->method('getParam')
- ->with('category', false)
- ->willReturn(false);
- $this->request->expects($this->at(1))->method('getParam')
- ->with('id')
- ->willReturn(1);
+ $this->request->expects($this->any())->method('getParam')->willReturnMap(
+ [
+ ['category', false, false],
+ ['id', null, 1],
+ ]
+ );
$product = $this->createPartialMock(
\Magento\Catalog\Model\Product::class,
['__wakeup', 'isVisibleInCatalog', 'isVisibleInSiteVisibility', 'getId', 'getWebsiteIds']
diff --git a/app/code/Magento/Review/Test/Unit/Observer/PredispatchReviewObserverTest.php b/app/code/Magento/Review/Test/Unit/Observer/PredispatchReviewObserverTest.php
new file mode 100644
index 0000000000000..2a8f8d8e38a64
--- /dev/null
+++ b/app/code/Magento/Review/Test/Unit/Observer/PredispatchReviewObserverTest.php
@@ -0,0 +1,147 @@
+configMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->urlMock = $this->getMockBuilder(UrlInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->responseMock = $this->getMockBuilder(ResponseInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['setRedirect'])
+ ->getMockForAbstractClass();
+ $this->redirectMock = $this->getMockBuilder(RedirectInterface::class)
+ ->getMock();
+ $this->objectManager = new ObjectManager($this);
+ $this->mockObject = $this->objectManager->getObject(
+ PredispatchReviewObserver::class,
+ [
+ 'scopeConfig' => $this->configMock,
+ 'url' => $this->urlMock
+ ]
+ );
+ }
+
+ /**
+ * Test with enabled review active config.
+ */
+ public function testReviewEnabled()
+ {
+ $observerMock = $this->getMockBuilder(Observer::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getResponse', 'getData', 'setRedirect'])
+ ->getMockForAbstractClass();
+
+ $this->configMock->method('getValue')
+ ->with(PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE, ScopeInterface::SCOPE_STORE)
+ ->willReturn(true);
+ $observerMock->expects($this->never())
+ ->method('getData')
+ ->with('controller_action')
+ ->willReturnSelf();
+
+ $observerMock->expects($this->never())
+ ->method('getResponse')
+ ->willReturnSelf();
+
+ $this->assertNull($this->mockObject->execute($observerMock));
+ }
+
+ /**
+ * Test with disabled review active config.
+ */
+ public function testReviewDisabled()
+ {
+ $observerMock = $this->getMockBuilder(Observer::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getControllerAction', 'getResponse'])
+ ->getMockForAbstractClass();
+
+ $this->configMock->expects($this->at(0))
+ ->method('getValue')
+ ->with(PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE, ScopeInterface::SCOPE_STORE)
+ ->willReturn(false);
+
+ $expectedRedirectUrl = 'https://test.com/index';
+
+ $this->configMock->expects($this->at(1))
+ ->method('getValue')
+ ->with('web/default/no_route', ScopeInterface::SCOPE_STORE)
+ ->willReturn($expectedRedirectUrl);
+
+ $this->urlMock->expects($this->once())
+ ->method('getUrl')
+ ->willReturn($expectedRedirectUrl);
+
+ $observerMock->expects($this->once())
+ ->method('getControllerAction')
+ ->willReturnSelf();
+
+ $observerMock->expects($this->once())
+ ->method('getResponse')
+ ->willReturn($this->responseMock);
+
+ $this->responseMock->expects($this->once())
+ ->method('setRedirect')
+ ->with($expectedRedirectUrl);
+
+ $this->assertNull($this->mockObject->execute($observerMock));
+ }
+}
diff --git a/app/code/Magento/Review/composer.json b/app/code/Magento/Review/composer.json
index c1d687c665199..4cc5cfc8d3f03 100644
--- a/app/code/Magento/Review/composer.json
+++ b/app/code/Magento/Review/composer.json
@@ -18,7 +18,7 @@
"magento/module-review-sample-data": "Sample Data version:100.2.*"
},
"type": "magento2-module",
- "version": "100.2.7",
+ "version": "100.2.8",
"license": [
"OSL-3.0",
"AFL-3.0"
diff --git a/app/code/Magento/Review/etc/acl.xml b/app/code/Magento/Review/etc/acl.xml
index 397cc1cce61d6..09b80750da14d 100644
--- a/app/code/Magento/Review/etc/acl.xml
+++ b/app/code/Magento/Review/etc/acl.xml
@@ -17,7 +17,7 @@
-
+
diff --git a/app/code/Magento/Review/etc/adminhtml/menu.xml b/app/code/Magento/Review/etc/adminhtml/menu.xml
index e3532483f88af..8b56f36bce68e 100644
--- a/app/code/Magento/Review/etc/adminhtml/menu.xml
+++ b/app/code/Magento/Review/etc/adminhtml/menu.xml
@@ -9,6 +9,7 @@
+
diff --git a/app/code/Magento/Review/etc/adminhtml/system.xml b/app/code/Magento/Review/etc/adminhtml/system.xml
index c0574e9491782..a24ed29dc2c23 100644
--- a/app/code/Magento/Review/etc/adminhtml/system.xml
+++ b/app/code/Magento/Review/etc/adminhtml/system.xml
@@ -10,7 +10,11 @@
Product Reviews
-
+
+ Enabled
+ Magento\Config\Model\Config\Source\Yesno
+
+
Allow Guests to Write Reviews
Magento\Config\Model\Config\Source\Yesno
diff --git a/app/code/Magento/Review/etc/config.xml b/app/code/Magento/Review/etc/config.xml
index 78dc87960f090..9fd9443be67ef 100644
--- a/app/code/Magento/Review/etc/config.xml
+++ b/app/code/Magento/Review/etc/config.xml
@@ -9,6 +9,7 @@
+ 1
1
diff --git a/app/code/Magento/Review/etc/frontend/events.xml b/app/code/Magento/Review/etc/frontend/events.xml
index bc94277d69709..8e883ce328a2c 100644
--- a/app/code/Magento/Review/etc/frontend/events.xml
+++ b/app/code/Magento/Review/etc/frontend/events.xml
@@ -12,4 +12,7 @@
+
+
+
diff --git a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml
index d7c5c19d4d813..6fcf5b0c82b4f 100644
--- a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml
+++ b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml
@@ -9,7 +9,7 @@
-
+
- .review .action.submit
@@ -18,8 +18,8 @@
-
-
+
+
diff --git a/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml b/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml
index 0a7ddd8b8903d..8a853cdd2e409 100644
--- a/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml
+++ b/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml
@@ -9,7 +9,7 @@
-
+
-
diff --git a/app/code/Magento/Review/view/frontend/layout/customer_account.xml b/app/code/Magento/Review/view/frontend/layout/customer_account.xml
index 54d171cbf1322..9f759dba41782 100644
--- a/app/code/Magento/Review/view/frontend/layout/customer_account.xml
+++ b/app/code/Magento/Review/view/frontend/layout/customer_account.xml
@@ -8,7 +8,7 @@
-
+
review/customer
My Product Reviews
diff --git a/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml b/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml
index 73174f0570e28..2e898a539a954 100644
--- a/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml
+++ b/app/code/Magento/Review/view/frontend/layout/customer_account_index.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml b/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml
index 2857e859aa06c..b5f7562963314 100644
--- a/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml
+++ b/app/code/Magento/Review/view/frontend/layout/review_customer_index.xml
@@ -9,7 +9,7 @@
-
+
diff --git a/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml b/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml
index d51c89a1abe1a..d3adbd7950cf9 100644
--- a/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml
+++ b/app/code/Magento/Review/view/frontend/layout/review_customer_view.xml
@@ -9,7 +9,7 @@
-
+
diff --git a/app/code/Magento/Review/view/frontend/layout/review_product_list.xml b/app/code/Magento/Review/view/frontend/layout/review_product_list.xml
index c83cfe95d7964..8c5c1297cdda3 100644
--- a/app/code/Magento/Review/view/frontend/layout/review_product_list.xml
+++ b/app/code/Magento/Review/view/frontend/layout/review_product_list.xml
@@ -9,15 +9,15 @@
-
+
-
+
-
-
+
+
diff --git a/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml b/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml
index af8d2dc2f506f..36fa71ea5125a 100644
--- a/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml
+++ b/app/code/Magento/Review/view/frontend/layout/review_product_listajax.xml
@@ -7,8 +7,8 @@
-->
-
-
-getDisplayIfEmpty()): ?>
+isReviewEnabled() && $block->getDisplayIfEmpty()): ?>