diff --git a/.htaccess b/.htaccess index d22b5a1395cae..71a5cf708dbc5 100644 --- a/.htaccess +++ b/.htaccess @@ -27,6 +27,11 @@ #AddType x-mapp-php5 .php #AddHandler x-mapp-php5 .php +############################################ +## enable usage of methods arguments in backtrace + + SetEnv MAGE_DEBUG_SHOW_ARGS 1 + ############################################ ## default index file @@ -364,6 +369,15 @@ Require all denied + + + order allow,deny + deny from all + + = 2.4> + Require all denied + + # For 404s and 403s that aren't handled by the application, show plain 404 response ErrorDocument 404 /pub/errors/404.php diff --git a/.htaccess.sample b/.htaccess.sample index c9ddff2cca4cf..c9e83a53cc8bd 100644 --- a/.htaccess.sample +++ b/.htaccess.sample @@ -27,6 +27,11 @@ #AddType x-mapp-php5 .php #AddHandler x-mapp-php5 .php +############################################ +## enable usage of methods arguments in backtrace + + SetEnv MAGE_DEBUG_SHOW_ARGS 1 + ############################################ ## default index file @@ -341,6 +346,15 @@ Require all denied + + + order allow,deny + deny from all + + = 2.4> + Require all denied + + # For 404s and 403s that aren't handled by the application, show plain 404 response ErrorDocument 404 /pub/errors/404.php diff --git a/app/bootstrap.php b/app/bootstrap.php index 0b13d12cece58..ddbcaffd42962 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -8,6 +8,7 @@ * Environment initialization */ error_reporting(E_ALL); +stream_wrapper_unregister('phar'); #ini_set('display_errors', 1); /* PHP version validation */ diff --git a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php index 6f0e42bdcbef1..82f70d92e4930 100644 --- a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php +++ b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php @@ -8,6 +8,11 @@ namespace Magento\AdminNotification\Block\Grid\Renderer; +/** + * Renderer class for action in the admin notifications grid + * + * @package Magento\AdminNotification\Block\Grid\Renderer + */ class Actions extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { /** @@ -37,7 +42,9 @@ public function __construct( */ public function render(\Magento\Framework\DataObject $row) { - $readDetailsHtml = $row->getUrl() ? '' . + $readDetailsHtml = $row->getUrl() ? '' . __('Read Details') . '' : ''; $markAsReadHtml = !$row->getIsRead() ? 'getSkipJsReloadPrice()) { - $extraParams .= ' onchange="opConfig.reloadPrice()"'; - } - $extraParams .= ' data-selector="' . $select->getName() . '"'; - $select->setExtraParams($extraParams); - - if ($configValue) { - $select->setValue($configValue); - } - - return $select->getHtml(); + $optionBlock = $this->multipleFactory->create(); } - - if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO || - $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX + if ($optionType === Option::OPTION_TYPE_RADIO || + $optionType === Option::OPTION_TYPE_CHECKBOX ) { - $selectHtml = ''; - $require = $_option->getIsRequire() ? ' required' : ''; - $arraySign = ''; - switch ($_option->getType()) { - case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO: - $type = 'radio'; - $class = 'radio admin__control-radio'; - if (!$_option->getIsRequire()) { - $selectHtml .= '' . - 'getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . - ' value="" checked="checked" />' . - __('None') . ''; - } - break; - case \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX: - $type = 'checkbox'; - $class = 'checkbox admin__control-checkbox'; - $arraySign = '[]'; - break; - } - $count = 1; - foreach ($_option->getValues() as $_value) { - $count++; - - $priceStr = $this->_formatPrice( - [ - 'is_percent' => $_value->getPriceType() == 'percent', - 'pricing_value' => $_value->getPrice($_value->getPriceType() == 'percent'), - ] - ); - - $htmlValue = $_value->getOptionTypeId(); - if ($arraySign) { - $checked = is_array($configValue) && in_array($htmlValue, $configValue) ? 'checked' : ''; - } else { - $checked = $configValue == $htmlValue ? 'checked' : ''; - } - - $dataSelector = 'options[' . $_option->getId() . ']'; - if ($arraySign) { - $dataSelector .= '[' . $htmlValue . ']'; - } - - $selectHtml .= '' . - 'getSkipJsReloadPrice() ? '' : ' onclick="opConfig.reloadPrice()"') . - ' name="options[' . - $_option->getId() . - ']' . - $arraySign . - '" id="options_' . - $_option->getId() . - '_' . - $count . - '" value="' . - $htmlValue . - '" ' . - $checked . - ' data-selector="' . $dataSelector . '"' . - ' price="' . - $this->pricingHelper->currencyByStore($_value->getPrice(true), $store, false) . - '" />' . - '' . - $_value->getTitle() . - ' ' . - $priceStr . - ''; - $selectHtml .= ''; - } - $selectHtml .= ''; - - return $selectHtml; + $optionBlock = $this->checkableFactory->create(); } + return $optionBlock + ->setOption($option) + ->setProduct($this->getProduct()) + ->setSkipJsReloadPrice(1) + ->_toHtml(); } } diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php new file mode 100644 index 0000000000000..3d856f85dbd94 --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Checkable.php @@ -0,0 +1,68 @@ + $value->getPriceType() === 'percent', + 'pricing_value' => $value->getPrice($value->getPriceType() === 'percent') + ] + ); + } + + /** + * Returns current currency for store + * + * @param ProductCustomOptionValuesInterface $value + * @return float|string + */ + public function getCurrencyByStore(ProductCustomOptionValuesInterface $value) + { + /** @noinspection PhpMethodParametersCountMismatchInspection */ + return $this->pricingHelper->currencyByStore( + $value->getPrice(true), + $this->getProduct()->getStore(), + false + ); + } + + /** + * Returns preconfigured value for given option + * + * @param Option $option + * @return string|array|null + */ + public function getPreconfiguredValue(Option $option) + { + return $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + } +} diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php new file mode 100644 index 0000000000000..09a931dfa0693 --- /dev/null +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Select/Multiple.php @@ -0,0 +1,112 @@ +getOption(); + $optionType = $option->getType(); + $configValue = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + $require = $option->getIsRequire() ? ' required' : ''; + $extraParams = ''; + /** @var Select $select */ + $select = $this->getLayout()->createBlock( + Select::class + )->setData( + [ + 'id' => 'select_' . $option->getId(), + 'class' => $require . ' product-custom-option admin__control-select' + ] + ); + $select = $this->insertSelectOption($select, $option); + $select = $this->processSelectOption($select, $option); + if ($optionType === Option::OPTION_TYPE_MULTIPLE) { + $extraParams = ' multiple="multiple"'; + } + if (!$this->getSkipJsReloadPrice()) { + $extraParams .= ' onchange="opConfig.reloadPrice()"'; + } + $extraParams .= ' data-selector="' . $select->getName() . '"'; + $select->setExtraParams($extraParams); + if ($configValue) { + $select->setValue($configValue); + } + return $select->getHtml(); + } + + /** + * Returns select with inserted option give as a parameter + * + * @param Select $select + * @param Option $option + * @return Select + */ + private function insertSelectOption(Select $select, Option $option): Select + { + $require = $option->getIsRequire() ? ' required' : ''; + if ($option->getType() === Option::OPTION_TYPE_DROP_DOWN) { + $select->setName('options[' . $option->getId() . ']')->addOption('', __('-- Please Select --')); + } else { + $select->setName('options[' . $option->getId() . '][]'); + $select->setClass('multiselect admin__control-multiselect' . $require . ' product-custom-option'); + } + + return $select; + } + + /** + * Returns select with formated option prices + * + * @param Select $select + * @param Option $option + * @return Select + */ + private function processSelectOption(Select $select, Option $option): Select + { + $store = $this->getProduct()->getStore(); + foreach ($option->getValues() as $_value) { + $isPercentPriceType = $_value->getPriceType() === 'percent'; + $priceStr = $this->_formatPrice( + [ + 'is_percent' => $isPercentPriceType, + 'pricing_value' => $_value->getPrice($isPercentPriceType) + ], + false + ); + $select->addOption( + $_value->getOptionTypeId(), + $_value->getTitle() . ' ' . strip_tags($priceStr) . '', + [ + 'price' => $this->pricingHelper->currencyByStore( + $_value->getPrice(true), + $store, + false + ) + ] + ); + } + + return $select; + } +} diff --git a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php index e2b0a91574021..10675a7b7c7e2 100644 --- a/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php +++ b/app/code/Magento/Catalog/Model/Config/CatalogClone/Media/Image.php @@ -5,6 +5,9 @@ */ namespace Magento\Catalog\Model\Config\CatalogClone\Media; +use Magento\Framework\Escaper; +use Magento\Framework\App\ObjectManager; + /** * Clone model for media images related config fields * @@ -26,6 +29,11 @@ class Image extends \Magento\Framework\App\Config\Value */ protected $_attributeCollectionFactory; + /** + * @var Escaper + */ + private $escaper; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -36,6 +44,9 @@ class Image extends \Magento\Framework\App\Config\Value * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param Escaper|null $escaper + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -46,8 +57,10 @@ public function __construct( \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + Escaper $escaper = null ) { + $this->escaper = $escaper ?? ObjectManager::getInstance()->get(Escaper::class); $this->_attributeCollectionFactory = $attributeCollectionFactory; $this->_eavConfig = $eavConfig; parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); @@ -71,10 +84,9 @@ public function getPrefixes() $prefixes = []; foreach ($collection as $attribute) { - /* @var $attribute \Magento\Eav\Model\Entity\Attribute */ $prefixes[] = [ 'field' => $attribute->getAttributeCode() . '_', - 'label' => $attribute->getFrontend()->getLabel(), + 'label' => $this->escaper->escapeHtml($attribute->getFrontend()->getLabel()), ]; } diff --git a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php index 3ec8e968aa245..24775a791e59f 100644 --- a/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php +++ b/app/code/Magento/Catalog/Model/Product/ProductFrontendAction/Synchronizer.php @@ -16,6 +16,8 @@ use Magento\Framework\EntityManager\EntityManager; /** + * A Product Widget Synchronizer. + * * Service which allows to sync product widget information, such as product id with db. In order to reuse this info * on different devices */ @@ -85,9 +87,10 @@ public function __construct( } /** - * Find lifetime in configuration. Configuration is hold in Stores Configuration - * Also this configuration is generated by: - * @see \Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration + * Finds lifetime in configuration. + * + * Configuration is hold in Stores Configuration. Also this configuration is generated by + * {@see Magento\Catalog\Model\Widget\RecentlyViewedStorageConfiguration} * * @param string $namespace * @return int @@ -108,6 +111,8 @@ private function getLifeTimeByNamespace($namespace) } /** + * Filters actions. + * * In order to avoid suspicious actions, we need to filter them in DESC order, and slice only items that * can be persisted in database. * @@ -138,7 +143,9 @@ private function getProductIdsByActions(array $actions) $productIds = []; foreach ($actions as $action) { - $productIds[] = $action['product_id']; + if (isset($action['product_id'])) { + $productIds[] = $action['product_id']; + } } return $productIds; @@ -159,33 +166,37 @@ public function syncActions(array $productsData, $typeId) $customerId = $this->session->getCustomerId(); $visitorId = $this->visitor->getId(); $collection = $this->getActionsByType($typeId); - $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); - - /** - * Note that collection is also filtered by visitor id and customer id - * This collection shouldn't be flushed when visitor has products and then login - * It can remove only products for visitor, or only products for customer - * - * ['product_id' => 'added_at'] - * @var ProductFrontendActionInterface $item - */ - foreach ($collection as $item) { - $this->entityManager->delete($item); - } - - foreach ($productsData as $productId => $productData) { - /** @var ProductFrontendActionInterface $action */ - $action = $this->productFrontendActionFactory->create([ - 'data' => [ - 'visitor_id' => $customerId ? null : $visitorId, - 'customer_id' => $this->session->getCustomerId(), - 'added_at' => $productData['added_at'], - 'product_id' => $productId, - 'type_id' => $typeId - ] - ]); - - $this->entityManager->save($action); + $productIds = $this->getProductIdsByActions($productsData); + + if ($productIds) { + $collection->addFieldToFilter('product_id', $productIds); + + /** + * Note that collection is also filtered by visitor id and customer id + * This collection shouldn't be flushed when visitor has products and then login + * It can remove only products for visitor, or only products for customer + * + * ['product_id' => 'added_at'] + * @var ProductFrontendActionInterface $item + */ + foreach ($collection as $item) { + $this->entityManager->delete($item); + } + + foreach ($productsData as $productId => $productData) { + /** @var ProductFrontendActionInterface $action */ + $action = $this->productFrontendActionFactory->create([ + 'data' => [ + 'visitor_id' => $customerId ? null : $visitorId, + 'customer_id' => $this->session->getCustomerId(), + 'added_at' => $productData['added_at'], + 'product_id' => $productId, + 'type_id' => $typeId + ] + ]); + + $this->entityManager->save($action); + } } } diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml index 37fbf01a6b9aa..8149bc34087fb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml @@ -109,6 +109,7 @@ + @@ -120,6 +121,7 @@ + @@ -131,6 +133,7 @@ + diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php index 5b1d3bf7943fc..23f0aec5b69a2 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Config/CatalogClone/Media/ImageTest.php @@ -9,6 +9,11 @@ use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +/** + * Tests \Magento\Catalog\Model\Config\CatalogClone\Media\Image. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ImageTest extends \PHPUnit\Framework\TestCase { /** @@ -36,6 +41,14 @@ class ImageTest extends \PHPUnit\Framework\TestCase */ private $attribute; + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaperMock; + + /** + * @inheritdoc + */ protected function setUp() { $this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class) @@ -62,54 +75,79 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->escaperMock = $this->getMockBuilder( + \Magento\Framework\Escaper::class + ) + ->disableOriginalConstructor() + ->setMethods(['escapeHtml']) + ->getMock(); + $helper = new ObjectManager($this); $this->model = $helper->getObject( \Magento\Catalog\Model\Config\CatalogClone\Media\Image::class, [ 'eavConfig' => $this->eavConfig, - 'attributeCollectionFactory' => $this->attributeCollectionFactory + 'attributeCollectionFactory' => $this->attributeCollectionFactory, + 'escaper' => $this->escaperMock, ] ); } - public function testGetPrefixes() + /** + * @param string $actualLabel + * @param string $expectedLabel + * @return void + * + * @dataProvider getPrefixesDataProvider + */ + public function testGetPrefixes(string $actualLabel, string $expectedLabel): void { $entityTypeId = 3; /** @var \Magento\Eav\Model\Entity\Type|\PHPUnit_Framework_MockObject_MockObject $entityType */ $entityType = $this->getMockBuilder(\Magento\Eav\Model\Entity\Type::class) ->disableOriginalConstructor() ->getMock(); - $entityType->expects($this->once())->method('getId')->will($this->returnValue($entityTypeId)); + $entityType->expects($this->once())->method('getId')->willReturn($entityTypeId); /** @var AbstractFrontend|\PHPUnit_Framework_MockObject_MockObject $frontend */ $frontend = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend::class) ->setMethods(['getLabel']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $frontend->expects($this->once())->method('getLabel')->will($this->returnValue('testLabel')); + $frontend->expects($this->once())->method('getLabel')->willReturn($actualLabel); - $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with( - $this->equalTo($entityTypeId) - ); - $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with( - $this->equalTo('media_image') - ); + $this->attributeCollection->expects($this->once())->method('setEntityTypeFilter')->with($entityTypeId); + $this->attributeCollection->expects($this->once())->method('setFrontendInputTypeFilter')->with('media_image'); - $this->attribute->expects($this->once())->method('getAttributeCode')->will( - $this->returnValue('attributeCode') - ); - $this->attribute->expects($this->once())->method('getFrontend')->will( - $this->returnValue($frontend) - ); + $this->attribute->expects($this->once())->method('getAttributeCode')->willReturn('attributeCode'); + $this->attribute->expects($this->once())->method('getFrontend')->willReturn($frontend); - $this->attributeCollection->expects($this->any())->method('getIterator')->will( - $this->returnValue(new \ArrayIterator([$this->attribute])) - ); + $this->attributeCollection->expects($this->any())->method('getIterator') + ->willReturn(new \ArrayIterator([$this->attribute])); + + $this->eavConfig->expects($this->any())->method('getEntityType')->with(Product::ENTITY) + ->willReturn($entityType); - $this->eavConfig->expects($this->any())->method('getEntityType')->with( - $this->equalTo(Product::ENTITY) - )->will($this->returnValue($entityType)); + $this->escaperMock->expects($this->once())->method('escapeHtml')->with($actualLabel) + ->willReturn($expectedLabel); - $this->assertEquals([['field' => 'attributeCode_', 'label' => 'testLabel']], $this->model->getPrefixes()); + $this->assertEquals([['field' => 'attributeCode_', 'label' => $expectedLabel]], $this->model->getPrefixes()); + } + + /** + * @return array + */ + public function getPrefixesDataProvider(): array + { + return [ + [ + 'actual_label' => 'testLabel', + 'expected_label' => 'testLabel', + ], + [ + 'actual_label' => ' '<media-image-attributelabel', + ], + ]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php index fce4a02622d9e..38bed83cb9504 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/ProductFrontendAction/SynchronizerTest.php @@ -80,6 +80,7 @@ protected function setUp() public function testFilterProductActions() { + $typeId = 'recently_compared_product'; $productsData = [ 1 => [ 'added_at' => 12, @@ -87,7 +88,7 @@ public function testFilterProductActions() ], 2 => [ 'added_at' => 13, - 'product_id' => 2, + 'product_id' => '2', ], 3 => [ 'added_at' => 14, @@ -126,10 +127,12 @@ public function testFilterProductActions() $collection->expects($this->once()) ->method('addFilterByUserIdentities') ->with(1, 34); - $collection->expects($this->any()) + $collection->expects($this->at(1)) ->method('addFieldToFilter') - ->withConsecutive(['type_id'], ['product_id']); - + ->with('type_id', $typeId); + $collection->expects($this->at(2)) + ->method('addFieldToFilter') + ->with('product_id', [1, 2]); $iterator = new \IteratorIterator(new \ArrayIterator([$frontendAction])); $collection->expects($this->once()) ->method('getIterator') diff --git a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml new file mode 100644 index 0000000000000..0f3b4f481a288 --- /dev/null +++ b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml @@ -0,0 +1,100 @@ +getOption(); +if ($option) : ?> + getPreconfiguredValue($option); + $optionType = $option->getType(); + $arraySign = $optionType === Option::OPTION_TYPE_CHECKBOX ? '[]' : ''; + $count = 1; + ?> + + + getIsRequire()): ?> + + + + + + + + + + + getValues() as $value) : ?> + getOptionTypeId(), $configValue) ? 'checked' : ''; + } else { + $checked = $configValue == $value->getOptionTypeId() ? 'checked' : ''; + } + $dataSelector = 'options[' . $option->getId() . ']'; + if ($arraySign) { + $dataSelector .= '[' . $value->getOptionTypeId() . ']'; + } + ?> + + + + data-selector="" + price="getCurrencyByStore($value) ?>" + /> + + + escapeHtml($value->getTitle()) ?> + + formatPrice($value) ?> + + + + + \ No newline at end of file diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml index c96df9cdd3195..454031279d882 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/item/default.phtml @@ -49,7 +49,7 @@ $canApplyMsrp = $helper->isShowBeforeOrderConfirm($product) && $helper->isMinima = /* @escapeNotVerified */ $_formatedOptionValue['full_view'] ?> - = /* @escapeNotVerified */ $_formatedOptionValue['value'] ?> + = $block->escapeHtml($_formatedOptionValue['value'], ['span']) ?> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html index 357b0e550af0f..41d442a76d510 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html @@ -45,7 +45,7 @@ - + diff --git a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php index 1f1b5be9683ed..1217270d780e1 100644 --- a/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php +++ b/app/code/Magento/CheckoutAgreements/Model/AgreementsConfigProvider.php @@ -67,17 +67,18 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { $agreements = []; $agreements['checkoutAgreements'] = $this->getAgreementsConfig(); + return $agreements; } /** - * Returns agreements config + * Returns agreements config. * * @return array */ @@ -99,7 +100,7 @@ protected function getAgreementsConfig() 'content' => $agreement->getIsHtml() ? $agreement->getContent() : nl2br($this->escaper->escapeHtml($agreement->getContent())), - 'checkboxText' => $agreement->getCheckboxText(), + 'checkboxText' => $this->escaper->escapeHtml($agreement->getCheckboxText()), 'mode' => $agreement->getMode(), 'agreementId' => $agreement->getAgreementId() ]; diff --git a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php index c59a3d2433ec2..c8309bacb0a86 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php +++ b/app/code/Magento/CheckoutAgreements/Test/Unit/Model/AgreementsConfigProviderTest.php @@ -8,6 +8,9 @@ use Magento\CheckoutAgreements\Model\AgreementsProvider; use Magento\Store\Model\ScopeInterface; +/** + * Tests for AgreementsConfigProvider. + */ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase { /** @@ -35,6 +38,9 @@ class AgreementsConfigProviderTest extends \PHPUnit\Framework\TestCase */ private $agreementsFilterMock; + /** + * @inheritdoc + */ protected function setUp() { $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); @@ -59,10 +65,16 @@ protected function setUp() ); } + /** + * Test for getConfig if content is HTML. + * + * @return void + */ public function testGetConfigIfContentIsHtml() { $content = 'content'; $checkboxText = 'checkbox_text'; + $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; $expectedResult = [ @@ -71,12 +83,12 @@ public function testGetConfigIfContentIsHtml() 'agreements' => [ [ 'content' => $content, - 'checkboxText' => $checkboxText, + 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, - 'agreementId' => $agreementId - ] - ] - ] + 'agreementId' => $agreementId, + ], + ], + ], ]; $this->scopeConfigMock->expects($this->once()) @@ -94,6 +106,11 @@ public function testGetConfigIfContentIsHtml() ->with($searchCriteriaMock) ->willReturn([$agreement]); + $this->escaperMock->expects($this->once()) + ->method('escapeHtml') + ->with($checkboxText) + ->willReturn($escapedCheckboxText); + $agreement->expects($this->once())->method('getIsHtml')->willReturn(true); $agreement->expects($this->once())->method('getContent')->willReturn($content); $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); @@ -103,11 +120,17 @@ public function testGetConfigIfContentIsHtml() $this->assertEquals($expectedResult, $this->model->getConfig()); } + /** + * Test for getConfig if content is not HTML. + * + * @return void + */ public function testGetConfigIfContentIsNotHtml() { $content = 'content'; $escapedContent = 'escaped_content'; $checkboxText = 'checkbox_text'; + $escapedCheckboxText = 'escaped_checkbox_text'; $mode = \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO; $agreementId = 100; $expectedResult = [ @@ -116,12 +139,12 @@ public function testGetConfigIfContentIsNotHtml() 'agreements' => [ [ 'content' => $escapedContent, - 'checkboxText' => $checkboxText, + 'checkboxText' => $escapedCheckboxText, 'mode' => $mode, - 'agreementId' => $agreementId - ] - ] - ] + 'agreementId' => $agreementId, + ], + ], + ], ]; $this->scopeConfigMock->expects($this->once()) @@ -139,8 +162,11 @@ public function testGetConfigIfContentIsNotHtml() ->with($searchCriteriaMock) ->willReturn([$agreement]); - $this->escaperMock->expects($this->once())->method('escapeHtml')->with($content)->willReturn($escapedContent); - + $this->escaperMock->expects($this->at(0))->method('escapeHtml')->with($content)->willReturn($escapedContent); + $this->escaperMock->expects($this->at(1)) + ->method('escapeHtml') + ->with($checkboxText) + ->willReturn($escapedCheckboxText); $agreement->expects($this->once())->method('getIsHtml')->willReturn(false); $agreement->expects($this->once())->method('getContent')->willReturn($content); $agreement->expects($this->once())->method('getCheckboxText')->willReturn($checkboxText); diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php index a7f49e8a431a4..82d200beb6dc9 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php @@ -6,12 +6,13 @@ */ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; /** * Creates new folder. */ -class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images +class NewFolder extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images implements HttpPostActionInterface { /** * @var \Magento\Framework\Controller\Result\JsonFactory diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php index 31b01ce115c21..9bad371aa84d7 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php @@ -4,6 +4,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Action\HttpPostActionInterface; @@ -58,13 +61,20 @@ public function execute() __('Directory %1 is not under storage root path.', $path) ); } - $result = $this->getStorage()->uploadFile($path, $this->getRequest()->getParam('type')); + $uploaded = $this->getStorage()->uploadFile($path, $this->getRequest()->getParam('type')); + $response = [ + 'name' => $uploaded['name'], + 'type' => $uploaded['type'], + 'error' => $uploaded['error'], + 'size' => $uploaded['size'], + 'file' => $uploaded['file'] + ]; } catch (\Exception $e) { - $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; + $response = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); - return $resultJson->setData($result); + return $resultJson->setData($response); } } diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index ca563bd9d8f61..dfbbce99b6515 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -3,17 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Cms\Model\Wysiwyg\Images; use Magento\Cms\Helper\Wysiwyg\Images; use Magento\Framework\App\Filesystem\DirectoryList; /** - * Wysiwyg Images model + * Wysiwyg Images model. + * + * Tightly connected with controllers responsible for managing files so it uses session and is (sort of) a part + * of the presentation layer. * * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * * @api * @since 100.0.2 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..31215f1bdee2b 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 @@ -40,7 +40,11 @@ protected function setUp() $this->file = $objectManager->getObject( \Magento\Config\Block\System\Config\Form\Field\File::class, - ['data' => $this->testData] + [ + '_escaper' => $objectManager->getObject(\Magento\Framework\Escaper::class), + '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 8a005a52ab614..b752f79f73446 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 @@ -34,6 +34,7 @@ protected function setUp() \Magento\Config\Block\System\Config\Form\Field\Image::class, [ 'urlBuilder' => $this->urlBuilderMock, + '_escaper' => $objectManager->getObject(\Magento\Framework\Escaper::class) ] ); 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..e7ba2e8aaa2e7 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 @@ -21,7 +21,10 @@ protected function setUp() { $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $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' => $testHelper->getObject(\Magento\Framework\Escaper::class) + ] ); $this->_object->setData('html_id', 'spec_element'); $this->_formMock = $this->createPartialMock( 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 190ecccbfdb76..ecc95cbe3d48f 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())); ?> objectManagerHelper = new ObjectManagerHelper($this); + + $regexFactoryMock = $this->getMockBuilder(RegexFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $regexObject = new Regex('/^[A-Za-z0-9_.]+$/'); + + $regexFactoryMock->expects($this->any())->method('create') + ->willReturn($regexObject); + + $localeObjectMock = $this->getMockBuilder(Locale::class)->setMethods(['isValid']) + ->disableOriginalConstructor() + ->getMock(); + + $localeObjectMock->expects($this->any())->method('isValid') + ->with('en_US') + ->will($this->returnValue(true)); + + $this->inputValidator = $this->objectManagerHelper->getObject( + InputValidator::class, + [ + 'localeValidator' => $localeObjectMock, + 'versionValidatorFactory' => $regexFactoryMock + ] + ); + } + + /** + * @throws \Zend_Validate_Exception + */ + public function testValidate() + { + $input = $this->getMockBuilder(ArrayInput::class) + ->disableOriginalConstructor() + ->setMethods(['getOption', 'getArgument']) + ->getMock(); + + $input->expects($this->atLeastOnce())->method('getArgument')->willReturn(['all']); + + $input->expects($this->atLeastOnce())->method('getOption') + ->willReturnMap( + [ + [Options::AREA, ['all']], + [Options::EXCLUDE_AREA, ['none']], + [Options::THEME, ['all']], + [Options::EXCLUDE_THEME, ['none']], + [Options::EXCLUDE_LANGUAGE, ['none']], + [Options::CONTENT_VERSION, '12345'] + ] + ); + + /** @noinspection PhpParamsInspection */ + $this->inputValidator->validate($input); + } + + /** + * @covers \Magento\Deploy\Console\InputValidator::checkAreasInput() + */ + public function testCheckAreasInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['test']), + new InputOption(Options::EXCLUDE_AREA, null, 4, '', ['test']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains('--area (-a) and --exclude-area cannot be used at the same time', $e->getMessage()); + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + /** + * @covers \Magento\Deploy\Console\InputValidator::checkThemesInput() + */ + public function testCheckThemesInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_AREA, null, 4, '', ['none']), + new InputOption(Options::THEME, null, 4, '', ['blank']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['luma']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains('--theme (-t) and --exclude-theme cannot be used at the same time', $e->getMessage()); + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + public function testCheckLanguagesInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_AREA, '', 4, '', ['none']), + new InputOption(Options::THEME, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['none']), + new InputArgument(Options::LANGUAGES_ARGUMENT, 2, '', ['en_US']), + new InputOption(Options::EXCLUDE_LANGUAGE, null, 4, '', ['all']) + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains( + '--language (-l) and --exclude-language cannot be used at the same time', + $e->getMessage() + ); + + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } + + public function testCheckVersionInputException() + { + $options = [ + new InputOption(Options::AREA, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_AREA, null, 4, '', ['none']), + new InputOption(Options::THEME, null, 4, '', ['all']), + new InputOption(Options::EXCLUDE_THEME, null, 4, '', ['none']), + new InputArgument(Options::LANGUAGES_ARGUMENT, 2, '', ['en_US']), + new InputOption(Options::EXCLUDE_LANGUAGE, null, 4, '', ['none']), + new InputOption(Options::CONTENT_VERSION, null, 4, '', '/*!#') + ]; + + $inputDefinition = new InputDefinition($options); + + try { + $this->inputValidator->validate( + new ArrayInput([], $inputDefinition) + ); + } catch (\Exception $e) { + $this->assertContains( + 'Argument "' . + Options::CONTENT_VERSION + . '" has invalid value, content version should contain only characters, digits and dots', + $e->getMessage() + ); + + $this->assertInstanceOf(InvalidArgumentException::class, $e); + } + } +} diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml index a64185ce67958..532ecde456077 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -16,7 +16,16 @@ - + + + = $block->getChildHtml('preview_form') ?> 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..a1fb2e449d7bf 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 @@ -33,7 +33,10 @@ protected function setUp() $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_object = $testHelper->getObject( \Magento\OfflineShipping\Block\Adminhtml\Form\Field\Import::class, - ['data' => $testData] + [ + 'data' => $testData, + '_escaper' => $testHelper->getObject(\Magento\Framework\Escaper::class) + ] ); $this->_object->setForm($this->_formMock); } 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..b9ea53c154014 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 @@ -5,6 +5,8 @@ */ namespace Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + /** * Class AbstractEnableTest * @@ -43,8 +45,18 @@ protected function setUp() )->disableOriginalConstructor() ->getMockForAbstractClass(); + $objectManager = new ObjectManager($this); + $escaper = $objectManager->getObject(\Magento\Framework\Escaper::class); + $reflection = new \ReflectionClass($this->elementMock); + $reflection_property = $reflection->getProperty('_escaper'); + $reflection_property->setAccessible(true); + $reflection_property->setValue($this->elementMock, $escaper); + $this->abstractEnable = $objectManager->getObject( - \Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable\Stub::class + \Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable\Stub::class, + [ + '_escaper' => $objectManager->getObject(\Magento\Framework\Escaper::class) + ] ); } diff --git a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php index c5b8dc1c4b124..6fdb70350ed72 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,6 +40,8 @@ public function __construct( } /** + * Save address for billing. + * * @param CartInterface $quote * @param AddressInterface $address * @param bool $useForShipping @@ -47,7 +52,7 @@ public function __construct( 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/QuoteAddressValidator.php b/app/code/Magento/Quote/Model/QuoteAddressValidator.php index 9a86829bfc4ce..e7750f5879de5 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,80 @@ 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 + * @param AddressInterface $address + * @param int|null $customerId Cart belongs to + * @return void * @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. */ - public function validate(\Magento\Quote\Api\Data\AddressInterface $addressData) + private function doValidate(AddressInterface $address, ?int $customerId): void { //validate customer id - if ($addressData->getCustomerId()) { - $customer = $this->customerRepository->getById($addressData->getCustomerId()); + if ($customerId) { + $customer = $this->customerRepository->getById($customerId); if (!$customer->getId()) { throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid customer id %1', $addressData->getCustomerId()) + __('Invalid customer id %1', $customerId) ); } } - if ($addressData->getCustomerAddressId()) { + if ($address->getCustomerAddressId()) { + //Existing address cannot belong to a guest + if (!$customerId) { + throw new \Magento\Framework\Exception\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()) + __('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)) { + }, $this->customerRepository->getById($customerId)->getAddresses()); + if (!in_array($address->getCustomerAddressId(), $applicableAddressIds)) { throw new \Magento\Framework\Exception\NoSuchEntityException( - __('Invalid customer address id %1', $addressData->getCustomerAddressId()) + __('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 + * @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. + */ + public function validate(AddressInterface $addressData) + { + $this->doValidate($addressData, $addressData->getCustomerId()); + return true; } + + /** + * Validate address to be used for cart. + * + * @param CartInterface $cart + * @param AddressInterface $address + * @return void + * @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. + */ + public function validateForCart(CartInterface $cart, AddressInterface $address): void + { + $this->doValidate($address, $cart->getCustomerIsGuest() ? null : $cart->getCustomer()->getId()); + } } diff --git a/app/code/Magento/Quote/Model/ShippingAddressManagement.php b/app/code/Magento/Quote/Model/ShippingAddressManagement.php index d8e70c68ba33f..b9edcc13d0077 100644 --- a/app/code/Magento/Quote/Model/ShippingAddressManagement.php +++ b/app/code/Magento/Quote/Model/ShippingAddressManagement.php @@ -79,7 +79,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritDoc * @SuppressWarnings(PHPMD.NPathComplexity) */ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $address) @@ -95,7 +95,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(); @@ -123,7 +123,7 @@ public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $addres } /** - * {@inheritDoc} + * @inheritDoc */ public function get($cartId) { diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php deleted file mode 100644 index 08f5f6a808561..0000000000000 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteAddressValidatorTest.php +++ /dev/null @@ -1,128 +0,0 @@ -objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->addressRepositoryMock = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); - $this->quoteAddressMock = $this->createPartialMock( - \Magento\Quote\Model\Quote\Address::class, - ['getCustomerId', 'load', 'getId', '__wakeup'] - ); - $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); - $this->model = $this->objectManager->getObject( - \Magento\Quote\Model\QuoteAddressValidator::class, - [ - 'addressRepository' => $this->addressRepositoryMock, - 'customerRepository' => $this->customerRepositoryMock, - 'customerSession' => $this->customerSessionMock - ] - ); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage Invalid customer id 100 - */ - 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); - $this->model->validate($address); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage Invalid 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()); - - $this->model->validate($address); - } - - /** - * Neither customer id used nor address id exists - */ - public function testValidateNewAddress() - { - $this->customerRepositoryMock->expects($this->never())->method('getById'); - $this->addressRepositoryMock->expects($this->never())->method('getById'); - $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $this->assertTrue($this->model->validate($address)); - } - - public function testValidateWithValidAddress() - { - $addressCustomer = 100; - $customerAddressId = 42; - - $address = $this->createMock(\Magento\Quote\Api\Data\AddressInterface::class); - $address->expects($this->atLeastOnce())->method('getCustomerId')->willReturn($addressCustomer); - $address->expects($this->atLeastOnce())->method('getCustomerAddressId')->willReturn($customerAddressId); - $customerMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); - $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); - - $customerMock->expects($this->once())->method('getAddresses')->willReturn([$customerAddress]); - $customerAddress->expects($this->once())->method('getId')->willReturn(42); - - $this->assertTrue($this->model->validate($address)); - } -} diff --git a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php deleted file mode 100644 index 89fea2bec73a8..0000000000000 --- a/app/code/Magento/Quote/Test/Unit/Model/ShippingAddressManagementTest.php +++ /dev/null @@ -1,282 +0,0 @@ -objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->quoteRepositoryMock = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class); - $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); - - $this->quoteAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, [ - 'setSameAsBilling', - 'setCollectShippingRates', - '__wakeup', - 'collectTotals', - 'save', - 'getId', - 'getCustomerAddressId', - 'getSaveInAddressBook', - 'getSameAsBilling', - 'importCustomerAddressData', - 'setSaveInAddressBook', - ]); - $this->validatorMock = $this->createMock(\Magento\Quote\Model\QuoteAddressValidator::class); - $this->totalsCollectorMock = $this->createMock(\Magento\Quote\Model\Quote\TotalsCollector::class); - $this->addressRepository = $this->createMock(\Magento\Customer\Api\AddressRepositoryInterface::class); - - $this->amountErrorMessageMock = $this->createPartialMock( - \Magento\Quote\Model\Quote\Validator\MinimumOrderAmount\ValidationMessage::class, - ['getMessage'] - ); - - $this->service = $this->objectManager->getObject( - \Magento\Quote\Model\ShippingAddressManagement::class, - [ - 'quoteRepository' => $this->quoteRepositoryMock, - 'addressValidator' => $this->validatorMock, - 'logger' => $this->createMock(\Psr\Log\LoggerInterface::class), - 'scopeConfig' => $this->scopeConfigMock, - 'totalsCollector' => $this->totalsCollectorMock, - 'addressRepository' => $this->addressRepository - ] - ); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage error345 - */ - public function testSetAddressValidationFailed() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart654') - ->will($this->returnValue($quoteMock)); - - $this->validatorMock->expects($this->once())->method('validate') - ->will($this->throwException(new \Magento\Framework\Exception\NoSuchEntityException(__('error345')))); - - $this->service->assign('cart654', $this->quoteAddressMock); - } - - public function testSetAddress() - { - $addressId = 1; - $customerAddressId = 150; - - $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['getIsMultiShipping', 'isVirtual', 'validateMinimumAmount', 'setShippingAddress', 'getShippingAddress'] - ); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart867') - ->willReturn($quoteMock); - $quoteMock->expects($this->once())->method('isVirtual')->will($this->returnValue(false)); - $quoteMock->expects($this->once()) - ->method('setShippingAddress') - ->with($this->quoteAddressMock) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('getSaveInAddressBook')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getSameAsBilling')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getCustomerAddressId')->willReturn($customerAddressId); - - $customerAddressMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); - - $this->addressRepository->expects($this->once()) - ->method('getById') - ->with($customerAddressId) - ->willReturn($customerAddressMock); - - $this->validatorMock->expects($this->once())->method('validate') - ->with($this->quoteAddressMock) - ->willReturn(true); - - $quoteMock->expects($this->exactly(3))->method('getShippingAddress')->willReturn($this->quoteAddressMock); - $this->quoteAddressMock->expects($this->once()) - ->method('importCustomerAddressData') - ->with($customerAddressMock) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('setSameAsBilling')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once())->method('setSaveInAddressBook')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once()) - ->method('setCollectShippingRates') - ->with(true) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('save')->willReturnSelf(); - $this->quoteAddressMock->expects($this->once())->method('getId')->will($this->returnValue($addressId)); - - $this->assertEquals($addressId, $this->service->assign('cart867', $this->quoteAddressMock)); - } - - /** - * @expectedException \Magento\Framework\Exception\NoSuchEntityException - * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. - */ - public function testSetAddressForVirtualProduct() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart867') - ->will($this->returnValue($quoteMock)); - $quoteMock->expects($this->once())->method('isVirtual')->will($this->returnValue(true)); - $quoteMock->expects($this->never())->method('setShippingAddress'); - - $this->quoteAddressMock->expects($this->never())->method('getCustomerAddressId'); - $this->quoteAddressMock->expects($this->never())->method('setSaveInAddressBook'); - - $quoteMock->expects($this->never())->method('save'); - - $this->service->assign('cart867', $this->quoteAddressMock); - } - - /** - * @expectedException \Magento\Framework\Exception\InputException - * @expectedExceptionMessage The address failed to save. Verify the address and try again. - */ - public function testSetAddressWithInabilityToSaveQuote() - { - $this->quoteAddressMock->expects($this->once())->method('save')->willThrowException( - new \Exception('The address failed to save. Verify the address and try again.') - ); - - $customerAddressId = 150; - - $quoteMock = $this->createPartialMock( - \Magento\Quote\Model\Quote::class, - ['getIsMultiShipping', 'isVirtual', 'validateMinimumAmount', 'setShippingAddress', 'getShippingAddress'] - ); - $this->quoteRepositoryMock->expects($this->once()) - ->method('getActive') - ->with('cart867') - ->willReturn($quoteMock); - $quoteMock->expects($this->once())->method('isVirtual')->will($this->returnValue(false)); - $quoteMock->expects($this->once()) - ->method('setShippingAddress') - ->with($this->quoteAddressMock) - ->willReturnSelf(); - - $customerAddressMock = $this->createMock(\Magento\Customer\Api\Data\AddressInterface::class); - - $this->addressRepository->expects($this->once()) - ->method('getById') - ->with($customerAddressId) - ->willReturn($customerAddressMock); - - $this->validatorMock->expects($this->once())->method('validate') - ->with($this->quoteAddressMock) - ->willReturn(true); - - $this->quoteAddressMock->expects($this->once())->method('getSaveInAddressBook')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getSameAsBilling')->willReturn(1); - $this->quoteAddressMock->expects($this->once())->method('getCustomerAddressId')->willReturn($customerAddressId); - - $quoteMock->expects($this->exactly(2))->method('getShippingAddress')->willReturn($this->quoteAddressMock); - $this->quoteAddressMock->expects($this->once()) - ->method('importCustomerAddressData') - ->with($customerAddressMock) - ->willReturnSelf(); - - $this->quoteAddressMock->expects($this->once())->method('setSameAsBilling')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once())->method('setSaveInAddressBook')->with(1)->willReturnSelf(); - $this->quoteAddressMock->expects($this->once()) - ->method('setCollectShippingRates') - ->with(true) - ->willReturnSelf(); - - $this->service->assign('cart867', $this->quoteAddressMock); - } - - public function testGetAddress() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once())->method('getActive')->with('cartId')->will( - $this->returnValue($quoteMock) - ); - - $addressMock = $this->createMock(\Magento\Quote\Model\Quote\Address::class); - $quoteMock->expects($this->any())->method('getShippingAddress')->will($this->returnValue($addressMock)); - $quoteMock->expects($this->any())->method('isVirtual')->will($this->returnValue(false)); - $this->assertEquals($addressMock, $this->service->get('cartId')); - } - - /** - * @expectedException \Exception - * @expectedExceptionMessage The Cart includes virtual product(s) only, so a shipping address is not used. - */ - public function testGetAddressOfQuoteWithVirtualProducts() - { - $quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class); - $this->quoteRepositoryMock->expects($this->once())->method('getActive')->with('cartId')->will( - $this->returnValue($quoteMock) - ); - - $quoteMock->expects($this->any())->method('isVirtual')->will($this->returnValue(true)); - $quoteMock->expects($this->never())->method('getShippingAddress'); - - $this->service->get('cartId'); - } -} diff --git a/app/code/Magento/Rule/Block/Editable.php b/app/code/Magento/Rule/Block/Editable.php index 67e4671236ea0..d53213a7df876 100644 --- a/app/code/Magento/Rule/Block/Editable.php +++ b/app/code/Magento/Rule/Block/Editable.php @@ -9,6 +9,8 @@ use Magento\Framework\View\Element\AbstractBlock; /** + * Renderer for Editable sales rules + * * @api * @since 100.0.2 */ @@ -52,9 +54,9 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele if ($element->getShowAsText()) { $html = ' 0 + + + + + Send To Friend Form + + + + + + + + 1 + + + diff --git a/app/code/Magento/SendFriend/etc/module.xml b/app/code/Magento/SendFriend/etc/module.xml index 01c267b3c4fcb..7876ef88618c2 100644 --- a/app/code/Magento/SendFriend/etc/module.xml +++ b/app/code/Magento/SendFriend/etc/module.xml @@ -10,6 +10,7 @@ + diff --git a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml index 8065b7e236132..4d6f3d8c628b2 100644 --- a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml +++ b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml @@ -13,7 +13,7 @@ - + diff --git a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml index 4922a9f365ced..3e00353a9157d 100644 --- a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml +++ b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml @@ -108,6 +108,7 @@ = $block->getChildHtml('form_additional_info') ?> + = $block->getChildHtml('captcha'); ?> ' + - item.label + + $('').text(item.label).html() + '' + ''; } @@ -414,7 +414,7 @@ define([ if ($widget.inProductList) { $widget.productForm.append(input); input = ''; - listLabel = 'aria-label="' + item.label + '"'; + listLabel = 'aria-label="' + $('').text(item.label).html() + '"'; } else { listLabel = 'aria-labelledby="' + controlLabelId + '"'; } @@ -516,11 +516,12 @@ define([ id = this.id; type = parseInt(optionConfig[id].type, 10); - value = optionConfig[id].hasOwnProperty('value') ? optionConfig[id].value : ''; + value = optionConfig[id].hasOwnProperty('value') ? + $('').text(optionConfig[id].value).html() : ''; thumb = optionConfig[id].hasOwnProperty('thumb') ? optionConfig[id].thumb : ''; width = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.width : 110; height = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.height : 90; - label = this.label ? this.label : ''; + label = this.label ? $('').text(this.label).html() : ''; attr = ' id="' + controlId + '-item-' + id + '"' + ' index="' + index + '"' + @@ -1222,8 +1223,8 @@ define([ updateBaseImage: function (images, context, isInProductView) { var justAnImage = images[0], initialImages = this.options.mediaGalleryInitial, - gallery = context.find(this.options.mediaGallerySelector).data('gallery'), imagesToUpdate, + gallery = context.find(this.options.mediaGallerySelector).data('gallery'), isInitial; if (isInProductView) { diff --git a/app/code/Magento/Wishlist/Block/Customer/Sharing.php b/app/code/Magento/Wishlist/Block/Customer/Sharing.php index 6fbf5a23dca22..40fd00d6143a5 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Sharing.php +++ b/app/code/Magento/Wishlist/Block/Customer/Sharing.php @@ -11,9 +11,14 @@ */ namespace Magento\Wishlist\Block\Customer; +use Magento\Captcha\Block\Captcha; + /** + * Class Sharing + * * @api * @since 100.0.2 + * @package Magento\Wishlist\Block\Customer */ class Sharing extends \Magento\Framework\View\Element\Template { @@ -60,6 +65,20 @@ public function __construct( */ protected function _prepareLayout() { + if (!$this->getChildBlock('captcha')) { + $this->addChild( + 'captcha', + Captcha::class, + [ + 'cacheable' => false, + 'after' => '-', + 'form_id' => 'share_wishlist_form', + 'image_width' => 230, + 'image_height' => 230 + ] + ); + } + $this->pageConfig->getTitle()->set(__('Wish List Sharing')); } diff --git a/app/code/Magento/Wishlist/Controller/Index/Send.php b/app/code/Magento/Wishlist/Controller/Index/Send.php index c2389af6a2282..a4e8258b9d67e 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Send.php +++ b/app/code/Magento/Wishlist/Controller/Index/Send.php @@ -8,16 +8,28 @@ use Magento\Framework\App\Action; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ResponseInterface; use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Session\Generic as WishlistSession; use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\View\Result\Layout as ResultLayout; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Captcha\Model\DefaultModel as CaptchaModel; +use Magento\Framework\Exception\LocalizedException; +use Magento\Customer\Model\Customer; /** + * Class Send + * + * @package Magento\Wishlist\Controller\Index * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Send extends \Magento\Wishlist\Controller\AbstractIndex +class Send extends \Magento\Wishlist\Controller\AbstractIndex implements Action\HttpPostActionInterface { /** * @var \Magento\Customer\Helper\View @@ -69,6 +81,16 @@ class Send extends \Magento\Wishlist\Controller\AbstractIndex */ protected $storeManager; + /** + * @var CaptchaHelper + */ + private $captchaHelper; + + /** + * @var CaptchaStringResolver + */ + private $captchaStringResolver; + /** * @param Action\Context $context * @param \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator @@ -81,6 +103,8 @@ class Send extends \Magento\Wishlist\Controller\AbstractIndex * @param WishlistSession $wishlistSession * @param ScopeConfigInterface $scopeConfig * @param StoreManagerInterface $storeManager + * @param CaptchaHelper|null $captchaHelper + * @param CaptchaStringResolver|null $captchaStringResolver * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -94,7 +118,9 @@ public function __construct( \Magento\Customer\Helper\View $customerHelperView, WishlistSession $wishlistSession, ScopeConfigInterface $scopeConfig, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + ?CaptchaHelper $captchaHelper = null, + ?CaptchaStringResolver $captchaStringResolver = null ) { $this->_formKeyValidator = $formKeyValidator; $this->_customerSession = $customerSession; @@ -106,6 +132,10 @@ public function __construct( $this->wishlistSession = $wishlistSession; $this->scopeConfig = $scopeConfig; $this->storeManager = $storeManager; + $this->captchaHelper = $captchaHelper ?: ObjectManager::getInstance()->get(CaptchaHelper::class); + $this->captchaStringResolver = $captchaStringResolver ? + : ObjectManager::getInstance()->get(CaptchaStringResolver::class); + parent::__construct($context); } @@ -114,6 +144,7 @@ public function __construct( * * @return \Magento\Framework\Controller\Result\Redirect * @throws NotFoundException + * @throws \Zend_Validate_Exception * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -122,11 +153,25 @@ public function execute() { /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); + $captchaForName = 'share_wishlist_form'; + /** @var CaptchaModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha($captchaForName); + if (!$this->_formKeyValidator->validate($this->getRequest())) { $resultRedirect->setPath('*/*/'); return $resultRedirect; } + $isCorrectCaptcha = $this->validateCaptcha($captchaModel, $captchaForName); + + $this->logCaptchaAttempt($captchaModel); + + if (!$isCorrectCaptcha) { + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); + $resultRedirect->setPath('*/*/share'); + return $resultRedirect; + } + $wishlist = $this->wishlistProvider->getWishlist(); if (!$wishlist) { throw new NotFoundException(__('Page not found.')); @@ -288,4 +333,46 @@ protected function getWishlistItems(ResultLayout $resultLayout) ->getBlock('wishlist.email.items') ->toHtml(); } + + /** + * Log customer action attempts + * + * @param CaptchaModel $captchaModel + * @return void + */ + private function logCaptchaAttempt(CaptchaModel $captchaModel): void + { + /** @var Customer $customer */ + $customer = $this->_customerSession->getCustomer(); + $email = ''; + + if ($customer->getId()) { + $email = $customer->getEmail(); + } + + $captchaModel->logAttempt($email); + } + + /** + * Captcha validate logic + * + * @param CaptchaModel $captchaModel + * @param string $captchaFormName + * @return bool + */ + private function validateCaptcha(CaptchaModel $captchaModel, string $captchaFormName) : bool + { + if ($captchaModel->isRequired()) { + $word = $this->captchaStringResolver->resolve( + $this->getRequest(), + $captchaFormName + ); + + if (!$captchaModel->isCorrect($word)) { + return false; + } + } + + return true; + } } diff --git a/app/code/Magento/Wishlist/Model/Rss/Wishlist.php b/app/code/Magento/Wishlist/Model/Rss/Wishlist.php index 9ccbf80f99a0c..ff59e0cdd7b91 100644 --- a/app/code/Magento/Wishlist/Model/Rss/Wishlist.php +++ b/app/code/Magento/Wishlist/Model/Rss/Wishlist.php @@ -118,10 +118,8 @@ public function __construct( */ public function isAllowed() { - return $this->scopeConfig->isSetFlag( - 'rss/wishlist/active', - ScopeInterface::SCOPE_STORE - ); + return $this->scopeConfig->isSetFlag('rss/wishlist/active', ScopeInterface::SCOPE_STORE) + && $this->getWishlist()->getCustomerId() === $this->wishlistHelper->getCustomer()->getId(); } /** @@ -185,8 +183,8 @@ public function getRssData() } } else { $data = [ - 'title' => __('We cannot retrieve the Wish List.'), - 'description' => __('We cannot retrieve the Wish List.'), + 'title' => __('We cannot retrieve the Wish List.')->render(), + 'description' => __('We cannot retrieve the Wish List.')->render(), 'link' => $this->urlBuilder->getUrl(), 'charset' => 'UTF-8', ]; @@ -202,7 +200,7 @@ public function getRssData() */ public function getCacheKey() { - return 'rss_wishlist_data'; + return 'rss_wishlist_data_' . $this->getWishlist()->getId(); } /** @@ -224,7 +222,7 @@ public function getHeader() { $customerId = $this->getWishlist()->getCustomerId(); $customer = $this->customerFactory->create()->load($customerId); - $title = __('%1\'s Wishlist', $customer->getName()); + $title = __('%1\'s Wishlist', $customer->getName())->render(); $newUrl = $this->urlBuilder->getUrl( 'wishlist/shared/index', ['code' => $this->getWishlist()->getSharingCode()] diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php index a8c0fbb951cce..47148f7878134 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/SendTest.php @@ -5,32 +5,24 @@ */ namespace Magento\Wishlist\Test\Unit\Controller\Index; -use Magento\Customer\Helper\View as CustomerViewHelper; use Magento\Customer\Model\Data\Customer as CustomerData; -use Magento\Customer\Model\Session as CustomerSession; use Magento\Framework\App\Action\Context as ActionContext; -use Magento\Framework\App\Area; -use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\RequestInterface; use Magento\Framework\Controller\Result\Redirect as ResultRedirect; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Framework\Event\ManagerInterface as EventManagerInterface; -use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Mail\TransportInterface; use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\Session\Generic as WishlistSession; -use Magento\Framework\Translate\Inline\StateInterface as TranslateInlineStateInterface; use Magento\Framework\UrlInterface; -use Magento\Framework\View\Layout; use Magento\Framework\View\Result\Layout as ResultLayout; -use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; use Magento\Wishlist\Controller\Index\Send; use Magento\Wishlist\Controller\WishlistProviderInterface; -use Magento\Wishlist\Model\Config as WishlistConfig; -use Magento\Wishlist\Model\Wishlist; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Captcha\Model\DefaultModel as CaptchaModel; +use Magento\Customer\Model\Session; /** * @SuppressWarnings(PHPMD.TooManyFields) @@ -47,36 +39,12 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var FormKeyValidator |\PHPUnit_Framework_MockObject_MockObject */ protected $formKeyValidator; - /** @var CustomerSession |\PHPUnit_Framework_MockObject_MockObject */ - protected $customerSession; - /** @var WishlistProviderInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $wishlistProvider; - /** @var WishlistConfig |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlistConfig; - - /** @var TransportBuilder |\PHPUnit_Framework_MockObject_MockObject */ - protected $transportBuilder; - - /** @var TranslateInlineStateInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $inlineTranslation; - - /** @var CustomerViewHelper |\PHPUnit_Framework_MockObject_MockObject */ - protected $customerViewHelper; - - /** @var WishlistSession |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlistSession; - - /** @var ScopeConfigInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $scopeConfig; - /** @var Store |\PHPUnit_Framework_MockObject_MockObject */ protected $store; - /** @var StoreManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ - protected $storeManager; - /** @var ResultFactory |\PHPUnit_Framework_MockObject_MockObject */ protected $resultFactory; @@ -86,15 +54,9 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var ResultLayout |\PHPUnit_Framework_MockObject_MockObject */ protected $resultLayout; - /** @var Layout |\PHPUnit_Framework_MockObject_MockObject */ - protected $layout; - /** @var RequestInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $request; - /** @var Wishlist |\PHPUnit_Framework_MockObject_MockObject */ - protected $wishlist; - /** @var ManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $messageManager; @@ -110,6 +72,15 @@ class SendTest extends \PHPUnit\Framework\TestCase /** @var EventManagerInterface |\PHPUnit_Framework_MockObject_MockObject */ protected $eventManager; + /** @var CaptchaHelper |\PHPUnit_Framework_MockObject_MockObject */ + protected $captchaHelper; + + /** @var CaptchaModel |\PHPUnit_Framework_MockObject_MockObject */ + protected $captchaModel; + + /** @var Session |\PHPUnit_Framework_MockObject_MockObject */ + protected $customerSession; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -136,7 +107,7 @@ protected function setUp() $this->request = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) ->setMethods([ 'getPost', - 'getPostValue', + 'getPostValue' ]) ->getMockForAbstractClass(); @@ -172,90 +143,72 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->wishlistProvider = $this->getMockBuilder(\Magento\Wishlist\Controller\WishlistProviderInterface::class) - ->getMockForAbstractClass(); - - $this->wishlistConfig = $this->getMockBuilder(\Magento\Wishlist\Model\Config::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->transportBuilder = $this->getMockBuilder(\Magento\Framework\Mail\Template\TransportBuilder::class) + $customerMock = $this->getMockBuilder(\Magento\Customer\Model\Customer::class) ->disableOriginalConstructor() + ->setMethods([ + 'getEmail', + 'getId' + ]) ->getMock(); - $this->inlineTranslation = $this->getMockBuilder(\Magento\Framework\Translate\Inline\StateInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $customerMock->expects($this->any()) + ->method('getEmail') + ->willReturn('expamle@mail.com'); - $this->customerViewHelper = $this->getMockBuilder(\Magento\Customer\Helper\View::class) - ->disableOriginalConstructor() - ->getMock(); + $customerMock->expects($this->any()) + ->method('getId') + ->willReturn(false); - $this->wishlistSession = $this->getMockBuilder(\Magento\Framework\Session\Generic::class) + $this->customerSession = $this->getMockBuilder(\Magento\Customer\Model\Session::class) ->disableOriginalConstructor() - ->setMethods(['setSharingForm']) + ->setMethods([ + 'getCustomer', + 'getData' + ]) ->getMock(); - $this->scopeConfig = $this->getMockBuilder(\Magento\Framework\App\Config\ScopeConfigInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $this->customerSession->expects($this->any()) + ->method('getCustomer') + ->willReturn($customerMock); - $this->store = $this->getMockBuilder(\Magento\Store\Model\Store::class) - ->disableOriginalConstructor() - ->setMethods(['getStoreId']) - ->getMock(); + $this->customerSession->expects($this->any()) + ->method('getData') + ->willReturn(false); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($this->store); + $this->wishlistProvider = $this->getMockBuilder(\Magento\Wishlist\Controller\WishlistProviderInterface::class) + ->getMockForAbstractClass(); - $this->wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class) + $this->captchaHelper = $this->getMockBuilder(CaptchaHelper::class) ->disableOriginalConstructor() ->setMethods([ - 'getShared', - 'setShared', - 'getId', - 'getSharingCode', - 'save', - 'isSalable', + 'getCaptcha' ]) ->getMock(); - $this->customerData = $this->getMockBuilder(\Magento\Customer\Model\Data\Customer::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->layout = $this->getMockBuilder(\Magento\Framework\View\Layout::class) + $this->captchaModel = $this->getMockBuilder(CaptchaModel::class) ->disableOriginalConstructor() ->setMethods([ - 'getBlock', - 'setWishlistId', - 'toHtml', + 'isRequired', + 'logAttempt' ]) ->getMock(); - $this->transport = $this->getMockBuilder(\Magento\Framework\Mail\TransportInterface::class) - ->getMockForAbstractClass(); + $objectHelper = new ObjectManager($this); + + $this->captchaHelper->expects($this->once())->method('getCaptcha') + ->willReturn($this->captchaModel); + $this->captchaModel->expects($this->any())->method('isRequired') + ->willReturn(false); - $this->model = new Send( - $this->context, - $this->formKeyValidator, - $this->customerSession, - $this->wishlistProvider, - $this->wishlistConfig, - $this->transportBuilder, - $this->inlineTranslation, - $this->customerViewHelper, - $this->wishlistSession, - $this->scopeConfig, - $this->storeManager + $this->model = $objectHelper->getObject( + Send::class, + [ + 'context' => $this->context, + 'formKeyValidator' => $this->formKeyValidator, + 'wishlistProvider' => $this->wishlistProvider, + 'captchaHelper' => $this->captchaHelper, + '_customerSession' => $this->customerSession + ] ); } @@ -291,409 +244,4 @@ public function testExecuteNoWishlistAvailable() $this->model->execute(); } - - /** - * @param string $text - * @param int $textLimit - * @param string $emails - * @param int $emailsLimit - * @param int $shared - * @param string $postValue - * @param string $errorMessage - * - * @dataProvider dataProviderExecuteWithError - */ - public function testExecuteWithError( - $text, - $textLimit, - $emails, - $emailsLimit, - $shared, - $postValue, - $errorMessage - ) { - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->once()) - ->method('getShared') - ->willReturn($shared); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->once()) - ->method('getPostValue') - ->willReturn($postValue); - - $this->messageManager->expects($this->once()) - ->method('addError') - ->with($errorMessage) - ->willReturnSelf(); - - $this->wishlistSession->expects($this->any()) - ->method('setSharingForm') - ->with($postValue) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*/share') - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } - - /** - * 1. Text - * 2. Text limit - * 3. Emails - * 4. Emails limit - * 5. Shared wishlists counter - * 6. POST value - * 7. Error message (RESULT) - * - * @return array - */ - public function dataProviderExecuteWithError() - { - return [ - ['test text', 1, 'user1@example.com', 1, 0, '', 'Message length must not exceed 1 symbols'], - ['test text', 100, null, 1, 0, '', 'Please enter an email address.'], - ['test text', 100, '', 1, 0, '', 'Please enter an email address.'], - ['test text', 100, 'user1@example.com', 1, 1, '', 'This wish list can be shared 0 more times.'], - [ - 'test text', - 100, - 'u1@example.com, u2@example.com', - 3, - 2, - '', - 'This wish list can be shared 1 more times.' - ], - ['test text', 100, 'wrongEmailAddress', 1, 0, '', 'Please enter a valid email address.'], - ['test text', 100, 'user1@example.com, wrongEmailAddress', 2, 0, '', 'Please enter a valid email address.'], - ['test text', 100, 'wrongEmailAddress, user2@example.com', 2, 0, '', 'Please enter a valid email address.'], - ]; - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecuteWithException() - { - $text = 'test text'; - $textLimit = 100; - $emails = 'user1@example.com'; - $emailsLimit = 1; - $shared = 0; - $customerName = 'user1 user1'; - $wishlistId = 1; - $rssLink = 'rss link'; - $sharingCode = 'sharing code'; - $exceptionMessage = 'test exception message'; - $postValue = ''; - - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->exactly(2)) - ->method('getShared') - ->willReturn($shared); - $this->wishlist->expects($this->once()) - ->method('setShared') - ->with($shared) - ->willReturnSelf(); - $this->wishlist->expects($this->once()) - ->method('getId') - ->willReturn($wishlistId); - $this->wishlist->expects($this->once()) - ->method('getSharingCode') - ->willReturn($sharingCode); - $this->wishlist->expects($this->once()) - ->method('save') - ->willReturnSelf(); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->exactly(2)) - ->method('getParam') - ->with('rss_url') - ->willReturn(true); - $this->request->expects($this->once()) - ->method('getPostValue') - ->willReturn($postValue); - - $this->layout->expects($this->once()) - ->method('getBlock') - ->with('wishlist.email.rss') - ->willReturnSelf(); - $this->layout->expects($this->once()) - ->method('setWishlistId') - ->with($wishlistId) - ->willReturnSelf(); - $this->layout->expects($this->once()) - ->method('toHtml') - ->willReturn($rssLink); - - $this->resultLayout->expects($this->exactly(2)) - ->method('addHandle') - ->willReturnMap([ - ['wishlist_email_rss', null], - ['wishlist_email_items', null], - ]); - $this->resultLayout->expects($this->once()) - ->method('getLayout') - ->willReturn($this->layout); - - $this->inlineTranslation->expects($this->once()) - ->method('suspend') - ->willReturnSelf(); - $this->inlineTranslation->expects($this->once()) - ->method('resume') - ->willReturnSelf(); - - $this->customerSession->expects($this->once()) - ->method('getCustomerDataObject') - ->willReturn($this->customerData); - - $this->customerViewHelper->expects($this->once()) - ->method('getCustomerName') - ->with($this->customerData) - ->willReturn($customerName); - - // Throw Exception - $this->transportBuilder->expects($this->once()) - ->method('setTemplateIdentifier') - ->willThrowException(new \Exception($exceptionMessage)); - - $this->messageManager->expects($this->once()) - ->method('addError') - ->with($exceptionMessage) - ->willReturnSelf(); - - $this->wishlistSession->expects($this->any()) - ->method('setSharingForm') - ->with($postValue) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*/share') - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function testExecute() - { - $text = 'text'; - $textLimit = 100; - $emails = 'user1@example.com'; - $emailsLimit = 1; - $shared = 0; - $customerName = 'user1 user1'; - $wishlistId = 1; - $sharingCode = 'sharing code'; - $templateIdentifier = 'template identifier'; - $storeId = 1; - $viewOnSiteLink = 'view on site link'; - $from = 'user0@example.com'; - - $this->formKeyValidator->expects($this->once()) - ->method('validate') - ->with($this->request) - ->willReturn(true); - - $this->wishlist->expects($this->exactly(2)) - ->method('getShared') - ->willReturn($shared); - $this->wishlist->expects($this->once()) - ->method('setShared') - ->with(++$shared) - ->willReturnSelf(); - $this->wishlist->expects($this->exactly(2)) - ->method('getId') - ->willReturn($wishlistId); - $this->wishlist->expects($this->once()) - ->method('getSharingCode') - ->willReturn($sharingCode); - $this->wishlist->expects($this->once()) - ->method('save') - ->willReturnSelf(); - $this->wishlist->expects($this->once()) - ->method('isSalable') - ->willReturn(true); - - $this->wishlistProvider->expects($this->once()) - ->method('getWishlist') - ->willReturn($this->wishlist); - - $this->wishlistConfig->expects($this->once()) - ->method('getSharingEmailLimit') - ->willReturn($emailsLimit); - $this->wishlistConfig->expects($this->once()) - ->method('getSharingTextLimit') - ->willReturn($textLimit); - - $this->request->expects($this->exactly(2)) - ->method('getPost') - ->willReturnMap([ - ['emails', $emails], - ['message', $text], - ]); - $this->request->expects($this->exactly(2)) - ->method('getParam') - ->with('rss_url') - ->willReturn(true); - - $this->layout->expects($this->exactly(2)) - ->method('getBlock') - ->willReturnMap([ - ['wishlist.email.rss', $this->layout], - ['wishlist.email.items', $this->layout], - ]); - - $this->layout->expects($this->once()) - ->method('setWishlistId') - ->with($wishlistId) - ->willReturnSelf(); - $this->layout->expects($this->exactly(2)) - ->method('toHtml') - ->willReturn($text); - - $this->resultLayout->expects($this->exactly(2)) - ->method('addHandle') - ->willReturnMap([ - ['wishlist_email_rss', null], - ['wishlist_email_items', null], - ]); - $this->resultLayout->expects($this->exactly(2)) - ->method('getLayout') - ->willReturn($this->layout); - - $this->inlineTranslation->expects($this->once()) - ->method('suspend') - ->willReturnSelf(); - $this->inlineTranslation->expects($this->once()) - ->method('resume') - ->willReturnSelf(); - - $this->customerSession->expects($this->once()) - ->method('getCustomerDataObject') - ->willReturn($this->customerData); - - $this->customerViewHelper->expects($this->once()) - ->method('getCustomerName') - ->with($this->customerData) - ->willReturn($customerName); - - $this->scopeConfig->expects($this->exactly(2)) - ->method('getValue') - ->willReturnMap([ - ['wishlist/email/email_template', ScopeInterface::SCOPE_STORE, null, $templateIdentifier], - ['wishlist/email/email_identity', ScopeInterface::SCOPE_STORE, null, $from], - ]); - - $this->store->expects($this->once()) - ->method('getStoreId') - ->willReturn($storeId); - - $this->url->expects($this->once()) - ->method('getUrl') - ->with('*/shared/index', ['code' => $sharingCode]) - ->willReturn($viewOnSiteLink); - - $this->transportBuilder->expects($this->once()) - ->method('setTemplateIdentifier') - ->with($templateIdentifier) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateOptions') - ->with([ - 'area' => Area::AREA_FRONTEND, - 'store' => $storeId, - ]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setTemplateVars') - ->with([ - 'customer' => $this->customerData, - 'customerName' => $customerName, - 'salable' => 'yes', - 'items' => $text, - 'viewOnSiteLink' => $viewOnSiteLink, - 'message' => $text . $text, - 'store' => $this->store, - ]) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('setFrom') - ->with($from) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('addTo') - ->with($emails) - ->willReturnSelf(); - $this->transportBuilder->expects($this->once()) - ->method('getTransport') - ->willReturn($this->transport); - - $this->transport->expects($this->once()) - ->method('sendMessage') - ->willReturnSelf(); - - $this->eventManager->expects($this->once()) - ->method('dispatch') - ->with('wishlist_share', ['wishlist' => $this->wishlist]) - ->willReturnSelf(); - - $this->messageManager->expects($this->once()) - ->method('addSuccess') - ->with(__('Your wish list has been shared.')) - ->willReturnSelf(); - - $this->resultRedirect->expects($this->once()) - ->method('setPath') - ->with('*/*', ['wishlist_id' => $wishlistId]) - ->willReturnSelf(); - - $this->assertEquals($this->resultRedirect, $this->model->execute()); - } } diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php index 85f6c504457d3..fc43baa0a67de 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/Rss/WishlistTest.php @@ -278,15 +278,35 @@ protected function processWishlistItemDescription($wishlistModelMock, $staticArg public function testIsAllowed() { + $customerId = 1; + $customerServiceMock = $this->createMock(\Magento\Customer\Api\Data\CustomerInterface::class); + $wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class)->setMethods( + ['getId', '__wakeup', 'getCustomerId', 'getItemCollection', 'getSharingCode'] + )->disableOriginalConstructor()->getMock(); + $wishlist->expects($this->once())->method('getCustomerId')->willReturn($customerId); + $this->wishlistHelperMock->expects($this->any())->method('getWishlist') + ->will($this->returnValue($wishlist)); + $this->wishlistHelperMock->expects($this->any()) + ->method('getCustomer') + ->will($this->returnValue($customerServiceMock)); + $customerServiceMock->expects($this->once())->method('getId')->willReturn($customerId); $this->scopeConfig->expects($this->once())->method('isSetFlag') ->with('rss/wishlist/active', \Magento\Store\Model\ScopeInterface::SCOPE_STORE) ->will($this->returnValue(true)); + $this->assertTrue($this->model->isAllowed()); } public function testGetCacheKey() { - $this->assertEquals('rss_wishlist_data', $this->model->getCacheKey()); + $wishlistId = 1; + $wishlist = $this->getMockBuilder(\Magento\Wishlist\Model\Wishlist::class)->setMethods( + ['getId', '__wakeup', 'getCustomerId', 'getItemCollection', 'getSharingCode'] + )->disableOriginalConstructor()->getMock(); + $wishlist->expects($this->once())->method('getId')->willReturn($wishlistId); + $this->wishlistHelperMock->expects($this->any())->method('getWishlist') + ->will($this->returnValue($wishlist)); + $this->assertEquals('rss_wishlist_data_1', $this->model->getCacheKey()); } public function testGetCacheLifetime() diff --git a/app/code/Magento/Wishlist/composer.json b/app/code/Magento/Wishlist/composer.json index ad2fe8e2b04d1..c9c74c8859e42 100644 --- a/app/code/Magento/Wishlist/composer.json +++ b/app/code/Magento/Wishlist/composer.json @@ -16,7 +16,8 @@ "magento/module-sales": "*", "magento/module-store": "*", "magento/module-theme": "*", - "magento/module-ui": "*" + "magento/module-ui": "*", + "magento/module-captcha": "*" }, "suggest": { "magento/module-configurable-product": "*", diff --git a/app/code/Magento/Wishlist/etc/config.xml b/app/code/Magento/Wishlist/etc/config.xml index 6588c41a0a7dd..dd88e63bc90ad 100644 --- a/app/code/Magento/Wishlist/etc/config.xml +++ b/app/code/Magento/Wishlist/etc/config.xml @@ -19,5 +19,21 @@ 255 + + + + + Share Wishlist Form + + + + + + + + 1 + + + diff --git a/app/code/Magento/Wishlist/etc/module.xml b/app/code/Magento/Wishlist/etc/module.xml index c5ece20d7956b..ab48ee89b7474 100644 --- a/app/code/Magento/Wishlist/etc/module.xml +++ b/app/code/Magento/Wishlist/etc/module.xml @@ -10,6 +10,7 @@ + diff --git a/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml b/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml index 430ebd384c82b..ff01cb4532cc7 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/sharing.phtml @@ -40,6 +40,7 @@ + = $block->getChildHtml('captcha'); ?> diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php b/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php index 4883d7819c288..b040397139451 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/Constraint/AssertCaptchaFieldOnContactUsForm.php @@ -6,7 +6,7 @@ namespace Magento\Captcha\Test\Constraint; -use Magento\Contact\Test\Page\ContactIndex; +use Magento\Captcha\Test\Page\ContactIndexCaptcha as ContactIndex; use Magento\Mtf\Constraint\AbstractConstraint; /** diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml b/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml index 060fc5f346fda..742eabb61f371 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/Page/ContactIndex.xml @@ -6,7 +6,7 @@ */ --> - + diff --git a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php index d8c9bf1f719de..0de71c3a416c8 100644 --- a/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php +++ b/dev/tests/functional/tests/app/Magento/Captcha/Test/TestCase/CaptchaOnContactUsTest.php @@ -8,7 +8,7 @@ use Magento\Captcha\Test\Constraint\AssertCaptchaFieldOnContactUsForm; use Magento\Contact\Test\Fixture\Comment; -use Magento\Contact\Test\Page\ContactIndex; +use Magento\Captcha\Test\Page\ContactIndexCaptcha as ContactIndex; use Magento\Mtf\TestCase\Injectable; use Magento\Mtf\TestStep\TestStepFactory; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php new file mode 100644 index 0000000000000..50b1256c0f124 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php @@ -0,0 +1,125 @@ +management = $objectManager->get(GuestShippingInformationManagementInterface::class); + $this->cartRepo = $objectManager->get(CartRepositoryInterface::class); + $this->customerRepo = $objectManager->get(CustomerRepositoryInterface::class); + $this->shippingFactory = $objectManager->get(ShippingInformationInterfaceFactory::class); + $this->searchCriteria = $objectManager->get(SearchCriteriaBuilder::class); + $this->maskFactory = $objectManager->get(QuoteIdMaskFactory::class); + } + + /** + * Test using another address for quote. + * + * @param bool $swapShipping Whether to swap shipping or billing addresses. + * @return void + * + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/Customer/_files/customer_with_addresses.php + * @dataProvider getAddressesVariation + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage The shipping information was unable to be saved. Verify the input data and try again. + */ + public function testDifferentAddresses(bool $swapShipping) + { + $carts = $this->cartRepo->getList( + $this->searchCriteria->addFilter('reserved_order_id', 'test01')->create() + )->getItems(); + $cart = array_pop($carts); + $otherCustomer = $this->customerRepo->get('customer_with_addresses@test.com'); + $otherAddresses = $otherCustomer->getAddresses(); + $otherAddress = array_pop($otherAddresses); + + //Setting invalid IDs. + /** @var ShippingAssignmentInterface $shippingAssignment */ + $shippingAssignment = $cart->getExtensionAttributes()->getShippingAssignments()[0]; + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + $billingAddress = $cart->getBillingAddress(); + if ($swapShipping) { + $address = $shippingAddress; + } else { + $address = $billingAddress; + } + $address->setCustomerAddressId($otherAddress->getId()); + $address->setCustomerId($otherCustomer->getId()); + $address->setId(null); + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingFactory->create(); + $shippingInformation->setBillingAddress($billingAddress); + $shippingInformation->setShippingAddress($shippingAddress); + $shippingInformation->setShippingMethodCode('flatrate'); + /** @var QuoteIdMask $idMask */ + $idMask = $this->maskFactory->create(); + $idMask->load($cart->getId(), 'quote_id'); + $this->management->saveAddressInformation($idMask->getMaskedId(), $shippingInformation); + } + + /** + * Different variations for addresses test. + * + * @return array + */ + public function getAddressesVariation(): array + { + return [ + 'Shipping address swap' => [true], + 'Billing address swap' => [false] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php new file mode 100644 index 0000000000000..7440fb7fd3d98 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/ShippingInformationManagementTest.php @@ -0,0 +1,104 @@ +management = $objectManager->get(ShippingInformationManagementInterface::class); + $this->cartRepo = $objectManager->get(CartRepositoryInterface::class); + $this->customerRepo = $objectManager->get(CustomerRepositoryInterface::class); + $this->shippingFactory = $objectManager->get(ShippingInformationInterfaceFactory::class); + } + + /** + * Test using another address for quote. + * + * @param bool $swapShipping Whether to swap shipping or billing addresses. + * @return void + * + * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php + * @magentoDataFixture Magento/Customer/_files/customer_with_addresses.php + * @dataProvider getAddressesVariation + * @expectedException \Magento\Framework\Exception\InputException + * @expectedExceptionMessage The shipping information was unable to be saved. Verify the input data and try again. + */ + public function testDifferentAddresses(bool $swapShipping) + { + $cart = $this->cartRepo->getForCustomer(1); + $otherCustomer = $this->customerRepo->get('customer_with_addresses@test.com'); + $otherAddresses = $otherCustomer->getAddresses(); + $otherAddress = array_pop($otherAddresses); + + //Setting invalid IDs. + /** @var ShippingAssignmentInterface $shippingAssignment */ + $shippingAssignment = $cart->getExtensionAttributes()->getShippingAssignments()[0]; + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + $billingAddress = $cart->getBillingAddress(); + if ($swapShipping) { + $address = $shippingAddress; + } else { + $address = $billingAddress; + } + $address->setCustomerAddressId($otherAddress->getId()); + $address->setCustomerId($otherCustomer->getId()); + $address->setId(null); + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingFactory->create(); + $shippingInformation->setBillingAddress($billingAddress); + $shippingInformation->setShippingAddress($shippingAddress); + $shippingInformation->setShippingMethodCode('flatrate'); + $this->management->saveAddressInformation($cart->getId(), $shippingInformation); + } + + /** + * Different variations for addresses test. + * + * @return array + */ + public function getAddressesVariation(): array + { + return [ + 'Shipping address swap' => [true], + 'Billing address swap' => [false] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php index a1a29706756b5..c574869a83cab 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php @@ -66,6 +66,7 @@ public function testExecute() $this->mediaDirectory->getRelativePath($this->fullDirectoryPath . $directoryName) ); $this->model->getRequest()->setParams(['node' => $this->imagesHelper->idEncode($directoryName)]); + $this->model->getRequest()->setMethod('POST'); $this->model->execute(); $this->assertFalse( diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php index bab14a8663eae..00f56e5700415 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php @@ -4,9 +4,14 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Controller\Result\Json as JsonResponse; +use Magento\Framework\App\Response\HttpFactory as ResponseFactory; +use Magento\Framework\App\Response\Http as Response; /** * Test for \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\Upload class. @@ -43,6 +48,11 @@ class UploadTest extends \PHPUnit\Framework\TestCase */ private $objectManager; + /** + * @var HttpFactory + */ + private $responseFactory; + /** * @inheritdoc */ @@ -56,6 +66,7 @@ protected function setUp() $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->fullDirectoryPath = $imagesHelper->getStorageRoot() . DIRECTORY_SEPARATOR . $directoryName; $this->mediaDirectory->create($this->mediaDirectory->getRelativePath($this->fullDirectoryPath)); + $this->responseFactory = $this->objectManager->get(ResponseFactory::class); $this->model = $this->objectManager->get(\Magento\Cms\Controller\Adminhtml\Wysiwyg\Images\Upload::class); $fixtureDir = realpath(__DIR__ . '/../../../../../Catalog/_files'); $tmpFile = $this->filesystem->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath() . $this->fileName; @@ -81,8 +92,13 @@ protected function setUp() public function testExecute() { $this->model->getRequest()->setParams(['type' => 'image/png']); + $this->model->getRequest()->setMethod('POST'); $this->model->getStorage()->getSession()->setCurrentPath($this->fullDirectoryPath); - $this->model->execute(); + /** @var JsonResponse $jsonResponse */ + $jsonResponse = $this->model->execute(); + /** @var Response $response */ + $jsonResponse->renderResult($response = $this->responseFactory->create()); + $data = json_decode($response->getBody(), true); $this->assertTrue( $this->mediaDirectory->isExist( @@ -91,6 +107,12 @@ public function testExecute() ) ) ); + //Asserting that response contains only data needed by clients. + $keys = ['name', 'type', 'error', 'size', 'file']; + sort($keys); + $dataKeys = array_keys($data); + sort($dataKeys); + $this->assertEquals($keys, $dataKeys); } /** diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php new file mode 100644 index 0000000000000..54dbdf25dd645 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/SendTest.php @@ -0,0 +1,93 @@ +createMock(LoggerInterface::class); + $session = Bootstrap::getObjectManager()->create( + Session::class, + [$logger] + ); + $this->accountManagement = Bootstrap::getObjectManager()->create(AccountManagementInterface::class); + $this->formKey = Bootstrap::getObjectManager()->create(FormKey::class); + $customer = $this->accountManagement->authenticate('customer@example.com', 'password'); + $session->setCustomerDataAsLoggedIn($customer); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testExecutePost() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'emails' => 'example1@gmail.com, example2@gmail.com, example3@gmail.com' + ] + ); + + $this->dispatch('wishlist/index/send'); + $this->assertRedirect($this->stringContains('wishlist/index/index')); + $this->assertSessionMessages( + $this->equalTo(['Your wish list has been shared.']), + \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS + ); + } + /** + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture default_store customer/captcha/forms user_forgotpassword,user_login,share_wishlist_form + * + */ + public function testCaptchaFailed() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'emails' => 'example1@gmail.com, example2@gmail.com, example3@gmail.com', + 'captcha' => [ + 'share_wishlist_form' => 'wrong_captcha_word' + ] + ] + ); + + $this->dispatch('wishlist/index/send'); + $this->assertRedirect($this->stringContains('wishlist/index/share')); + $this->assertSessionMessages( + $this->equalTo(['Incorrect CAPTCHA']), + \Magento\Framework\Message\MessageInterface::TYPE_ERROR + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php new file mode 100644 index 0000000000000..60b570b9d13d1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses.php @@ -0,0 +1,75 @@ +create(CustomerRepositoryInterface::class); +/** @var Customer $customer */ +$customer = $objectManager->create(Customer::class); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +$customer->setWebsiteId(1) + ->setEmail('customer_with_addresses@test.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setTaxvat('12') + ->setGender(0); + +$customer->isObjectNew(true); +$customer->save(); +$customerRegistry->remove($customer->getId()); + +//Creating address +/** @var Address $customerAddress */ +$customerAddress = $objectManager->create(Address::class); +$customerAddress->isObjectNew(true); +$customerAddress->setData( + [ + 'attribute_set_id' => 2, + 'telephone' => 3468676, + 'postcode' => 75477, + 'country_id' => 'US', + 'city' => 'CityM', + 'company' => 'CompanyName', + 'street' => 'CustomerAddress1', + 'lastname' => 'Smith', + 'firstname' => 'John', + 'parent_id' => $customer->getId(), + 'region_id' => 1, + ] +); +$customerAddress->save(); +/** @var AddressRepositoryInterface $addressRepository */ +$addressRepository = $objectManager->get(AddressRepositoryInterface::class); +$customerAddress = $addressRepository->getById($customerAddress->getId()); +$customerAddress->setCustomerId($customer->getId()); +$customerAddress->isDefaultBilling(true); +$customerAddress->setIsDefaultShipping(true); +$customerAddress = $addressRepository->save($customerAddress); +$customerRegistry->remove($customerAddress->getCustomerId()); +/** @var AddressRegistry $addressRegistry */ +$addressRegistry = $objectManager->get(AddressRegistry::class); +$addressRegistry->remove($customerAddress->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php new file mode 100644 index 0000000000000..e0c62bffc70d2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_addresses_rollback.php @@ -0,0 +1,34 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var CustomerRepositoryInterface $customerRepo */ +$customerRepo = $objectManager->get(CustomerRepositoryInterface::class); +try { + $customer = $customerRepo->get('customer_with_addresses@test.com'); + /** @var AddressRepositoryInterface $addressRepo */ + $addressRepo = $objectManager->get(AddressRepositoryInterface::class); + foreach ($customer->getAddresses() as $address) { + $addressRepo->delete($address); + } + $customerRepo->delete($customer); +} catch (NoSuchEntityException $exception) { + //Already deleted +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php b/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php new file mode 100644 index 0000000000000..9a611b8f2b9ea --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Rss/Controller/Feed/IndexTest.php @@ -0,0 +1,91 @@ +urlBuilder = $this->_objectManager->get(\Magento\Rss\Model\UrlBuilder::class); + $this->customerRepository = $this->_objectManager->get( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); + $this->wishlist = $this->_objectManager->get(\Magento\Wishlist\Model\Wishlist::class); + $this->customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); + } + + /** + * Check Rss response. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + * @magentoConfigFixture current_store rss/wishlist/active 1 + * @magentoConfigFixture current_store rss/config/active 1 + */ + public function testRssResponse() + { + $firstCustomerId = 1; + $this->customerSession->setCustomerId($firstCustomerId); + $customer = $this->customerRepository->getById($firstCustomerId); + $customerEmail = $customer->getEmail(); + $wishlistId = $this->wishlist->loadByCustomerId($firstCustomerId)->getId(); + $this->dispatch($this->getLink($firstCustomerId, $customerEmail, $wishlistId)); + $body = $this->getResponse()->getBody(); + $this->assertContains('John Smith\'s Wishlist', $body); + } + + /** + * Check Rss with incorrect wishlist id. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + * @magentoConfigFixture current_store rss/wishlist/active 1 + * @magentoConfigFixture current_store rss/config/active 1 + */ + public function testRssResponseWithIncorrectWishlistId() + { + $firstCustomerId = 1; + $secondCustomerId = 2; + $this->customerSession->setCustomerId($firstCustomerId); + $customer = $this->customerRepository->getById($firstCustomerId); + $customerEmail = $customer->getEmail(); + $wishlistId = $this->wishlist->loadByCustomerId($secondCustomerId, true)->getId(); + $this->dispatch($this->getLink($firstCustomerId, $customerEmail, $wishlistId)); + $body = $this->getResponse()->getBody(); + $this->assertContains('404 Not Found', $body); + } + + private function getLink($customerId, $customerEmail, $wishlistId) + { + + return 'rss/feed/index/type/wishlist/data/' + . base64_encode($customerId . ',' . $customerEmail) + . '/wishlist_id/' . $wishlistId; + } +} diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php new file mode 100644 index 0000000000000..8794dfdff8fd7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Controller/Product/CustomerSendmailTest.php @@ -0,0 +1,171 @@ +accountManagement = $this->_objectManager->create(AccountManagementInterface::class); + $this->formKey = $this->_objectManager->create(FormKey::class); + $logger = $this->createMock(LoggerInterface::class); + $this->session = $this->_objectManager->create( + Session::class, + [$logger] + ); + $this->captchaHelper = $this->_objectManager->create(CaptchaHelper::class); + $customer = $this->accountManagement->authenticate('customer@example.com', 'password'); + $this->session->setCustomerDataAsLoggedIn($customer); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testExecute() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default_store customer/captcha/forms product_sendtofriend_form + */ + public function testWithCaptchaFailed() + { + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'captcha' => [ + 'product_sendtofriend_form' => 'test' + ], + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['Incorrect CAPTCHA']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default_store customer/captcha/forms product_sendtofriend_form + * + */ + public function testWithCaptchaSuccess() + { + /** @var DefaultModel $captchaModel */ + $captchaModel = $this->captchaHelper->getCaptcha('product_sendtofriend_form'); + $captchaModel->generate(); + $word = $captchaModel->getWord(); + $this->getRequest() + ->setMethod('POST') + ->setPostValue( + [ + 'form_key' => $this->formKey->getFormKey(), + 'sender' => [ + 'name' => 'customer', + 'email' => 'customer@example.com', + 'message' => 'example message' + ], + 'id' => 1, + 'captcha' => [ + 'product_sendtofriend_form' => $word + ], + 'recipients' => [ + 'name' => ['John'], + 'email' => ['example1@gmail.com'] + ] + ] + ); + + $this->dispatch('sendfriend/product/sendmail'); + $this->assertSessionMessages( + $this->equalTo(['The link to a friend was sent.']), + MessageInterface::TYPE_SUCCESS + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php index 940d05eb4d5d7..e020d31838f06 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/IndexTest.php @@ -159,6 +159,7 @@ public function testSendAction() ]; $this->getRequest()->setPostValue($request); + $this->getRequest()->setMethod('POST'); $this->_objectManager->get(\Magento\Framework\Registry::class)->register( 'wishlist', diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php new file mode 100644 index 0000000000000..a71c9c9ba3c6f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php @@ -0,0 +1,25 @@ +create( + \Magento\Wishlist\Model\Wishlist::class +); +$wishlistForFirstCustomer->loadByCustomerId($firstCustomerIdFromFixture, true); +$item = $wishlistForFirstCustomer->addNewItem($product, new \Magento\Framework\DataObject([])); +$wishlistForFirstCustomer->save(); + +$secondCustomerIdFromFixture = 2; +$wishlistForSecondCustomer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Wishlist\Model\Wishlist::class +); +$wishlistForSecondCustomer->loadByCustomerId($secondCustomerIdFromFixture, true); +$item = $wishlistForSecondCustomer->addNewItem($product, new \Magento\Framework\DataObject([])); +$wishlistForSecondCustomer->save(); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers_rollback.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers_rollback.php new file mode 100644 index 0000000000000..4baba3b9f77c8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/two_wishlists_for_two_diff_customers_rollback.php @@ -0,0 +1,19 @@ +create(\Magento\Wishlist\Model\Wishlist::class); +$wishlist->loadByCustomerId(1); +$wishlist->delete(); +$wishlist->loadByCustomerId(2); +$wishlist->delete(); + +require __DIR__ . '/../../../Magento/Customer/_files/two_customers_rollback.php'; +require __DIR__ . '/../../../Magento/Catalog/_files/product_simple_rollback.php'; diff --git a/lib/internal/Magento/Framework/App/Http.php b/lib/internal/Magento/Framework/App/Http.php index 3c6dee49f97b4..23024a44c2def 100644 --- a/lib/internal/Magento/Framework/App/Http.php +++ b/lib/internal/Magento/Framework/App/Http.php @@ -6,6 +6,7 @@ namespace Magento\Framework\App; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Debug; use Magento\Framework\ObjectManager\ConfigLoaderInterface; use Magento\Framework\App\Request\Http as RequestHttp; use Magento\Framework\App\Response\Http as ResponseHttp; @@ -79,7 +80,7 @@ class Http implements \Magento\Framework\AppInterface * @param ResponseHttp $response * @param ConfigLoaderInterface $configLoader * @param State $state - * @param Filesystem $filesystem, + * @param Filesystem $filesystem * @param \Magento\Framework\Registry $registry */ public function __construct( @@ -149,7 +150,7 @@ public function launch() } /** - * {@inheritdoc} + * @inheritdoc */ public function catchException(Bootstrap $bootstrap, \Exception $exception) { @@ -198,6 +199,7 @@ private function buildContentFromException(\Exception $exception) { /** @var \Exception[] $exceptions */ $exceptions = []; + do { $exceptions[] = $exception; } while ($exception = $exception->getPrevious()); @@ -214,7 +216,12 @@ private function buildContentFromException(\Exception $exception) $index, get_class($exception), $exception->getMessage(), - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); } @@ -312,7 +319,15 @@ private function handleInitException(\Exception $exception) */ private function handleGenericReport(Bootstrap $bootstrap, \Exception $exception) { - $reportData = [$exception->getMessage(), $exception->getTraceAsString()]; + $reportData = [ + $exception->getMessage(), + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) + ]; $params = $bootstrap->getParams(); if (isset($params['REQUEST_URI'])) { $reportData['url'] = $params['REQUEST_URI']; diff --git a/lib/internal/Magento/Framework/App/StaticResource.php b/lib/internal/Magento/Framework/App/StaticResource.php index 575074fdb58ac..86b2b15d3c446 100644 --- a/lib/internal/Magento/Framework/App/StaticResource.php +++ b/lib/internal/Magento/Framework/App/StaticResource.php @@ -10,6 +10,7 @@ use Magento\Framework\Filesystem; use Magento\Framework\Config\ConfigOptionsListConstants; use Psr\Log\LoggerInterface; +use Magento\Framework\Debug; /** * Entry point for retrieving static resources like JS, CSS, images by requested public path @@ -138,7 +139,7 @@ public function launch() } /** - * {@inheritdoc} + * @inheritdoc */ public function catchException(Bootstrap $bootstrap, \Exception $exception) { @@ -146,7 +147,15 @@ public function catchException(Bootstrap $bootstrap, \Exception $exception) if ($bootstrap->isDeveloperMode()) { $this->response->setHttpResponseCode(404); $this->response->setHeader('Content-Type', 'text/plain'); - $this->response->setBody($exception->getMessage() . "\n" . $exception->getTraceAsString()); + $this->response->setBody( + $exception->getMessage() . "\n" . + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) + ); $this->response->sendResponse(); } else { require $this->getFilesystem()->getDirectoryRead(DirectoryList::PUB)->getAbsolutePath('errors/404.php'); diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index fb62b720e0b1c..edec7e135ae9f 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -2966,7 +2966,7 @@ public function prepareSqlCondition($fieldName, $condition) if (isset($condition['to'])) { $query .= empty($query) ? '' : ' AND '; $to = $this->_prepareSqlDateCondition($condition, 'to'); - $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); + $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName); } } elseif (array_key_exists($key, $conditionKeyMap)) { $value = $condition[$key]; diff --git a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php index 3638ff921fa9d..14f4df7208b04 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php @@ -170,7 +170,11 @@ public function setId($id) */ public function getHtmlId() { - return $this->getForm()->getHtmlIdPrefix() . $this->getData('html_id') . $this->getForm()->getHtmlIdSuffix(); + return $this->_escaper->escapeHtml( + $this->getForm()->getHtmlIdPrefix() . + $this->getData('html_id') . + $this->getForm()->getHtmlIdSuffix() + ); } /** @@ -180,7 +184,7 @@ public function getHtmlId() */ public function getName() { - $name = $this->getData('name'); + $name = $this->_escaper->escapeHtml($this->getData('name')); if ($suffix = $this->getForm()->getFieldNameSuffix()) { $name = $this->getForm()->addSuffixToName($name, $suffix); } @@ -339,7 +343,7 @@ protected function _getUiId($suffix = null) if ($this->_renderer instanceof \Magento\Framework\View\Element\AbstractBlock) { return $this->_renderer->getUiId($this->getType(), $this->getName(), $suffix); } else { - return ' data-ui-id="form-element-' . $this->getName() . ($suffix ?: '') . '"'; + return ' data-ui-id="form-element-' . $this->_escaper->escapeHtml($this->getName()) . ($suffix ?: '') . '"'; } } diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php index a85c1f4aa450c..d9dafddc571b8 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php @@ -33,11 +33,12 @@ class AbstractElementTest extends \PHPUnit\Framework\TestCase protected function setUp() { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->_factoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\Factory::class); $this->_collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); - $this->_escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $this->_escaperMock = $objectManager->getObject(\Magento\Framework\Escaper::class); $this->_model = $this->getMockForAbstractClass( \Magento\Framework\Data\Form\Element\AbstractElement::class, @@ -423,9 +424,6 @@ public function testGetHtmlContainerIdWithFieldContainerIdPrefix() */ public function testAddElementValues(array $initialData, $expectedValue) { - $this->_escaperMock->expects($this->any()) - ->method('escapeHtml') - ->will($this->returnArgument(0)); $this->_model->setValues($initialData['initial_values']); $this->_model->addElementValues($initialData['add_values'], $initialData['overwrite']); diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php index a2a40ee03b044..cf3cd0345e174 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php @@ -23,9 +23,10 @@ class LinkTest extends \PHPUnit\Framework\TestCase protected function setUp() { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $factoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\Factory::class); $collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); - $escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $escaperMock = $objectManager->getObject(\Magento\Framework\Escaper::class); $this->_link = new \Magento\Framework\Data\Form\Element\Link( $factoryMock, $collectionFactoryMock, diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php index 47eae73d8cd8c..c515e0aca01df 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\Data\Test\Unit\Form\Element; +use Magento\Framework\Escaper; + class MultiselectTest extends \PHPUnit\Framework\TestCase { /** @@ -15,7 +17,13 @@ class MultiselectTest extends \PHPUnit\Framework\TestCase protected function setUp() { $testHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->_model = $testHelper->getObject(\Magento\Framework\Data\Form\Element\Editablemultiselect::class); + $escaper = new Escaper(); + $this->_model = $testHelper->getObject( + \Magento\Framework\Data\Form\Element\Editablemultiselect::class, + [ + '_escaper' => $escaper + ] + ); $this->_model->setForm(new \Magento\Framework\DataObject()); } diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php index f77f4a816a1af..ad7d20fdc0acc 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/NoteTest.php @@ -23,9 +23,10 @@ class NoteTest extends \PHPUnit\Framework\TestCase protected function setUp() { + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $factoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\Factory::class); $collectionFactoryMock = $this->createMock(\Magento\Framework\Data\Form\Element\CollectionFactory::class); - $escaperMock = $this->createMock(\Magento\Framework\Escaper::class); + $escaperMock = $objectManager->getObject(\Magento\Framework\Escaper::class); $this->_model = new \Magento\Framework\Data\Form\Element\Note( $factoryMock, $collectionFactoryMock, diff --git a/lib/internal/Magento/Framework/Filter/Template.php b/lib/internal/Magento/Framework/Filter/Template.php index a56a4a3edf1fe..d3a8d5334ab9d 100644 --- a/lib/internal/Magento/Framework/Filter/Template.php +++ b/lib/internal/Magento/Framework/Filter/Template.php @@ -9,6 +9,9 @@ */ namespace Magento\Framework\Filter; +use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\Framework\Model\AbstractModel; + /** * Template filter * @@ -63,6 +66,18 @@ class Template implements \Zend_Filter_Interface */ protected $string; + /** + * @var string[] + */ + private $restrictedMethods = [ + 'addafterfiltercallback', + 'getresourcecollection', + 'load', + 'save', + 'getcollection', + 'getresource' + ]; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param array $variables @@ -367,6 +382,46 @@ protected function getParameters($value) return $params; } + /** + * Validate method call initiated in a template. + * + * Deny calls for methods that may disrupt template processing. + * + * @param object $object + * @param string $method + * @return void + * @throws \InvalidArgumentException + */ + private function validateVariableMethodCall($object, string $method): void + { + if ($object === $this) { + if (in_array(mb_strtolower($method), $this->restrictedMethods)) { + throw new \InvalidArgumentException("Method $method cannot be called from template."); + } + } + } + + /** + * Check allowed methods for data objects. + * + * Deny calls for methods that may disrupt template processing. + * + * @param object $object + * @param string $method + * @return bool + * @throws \InvalidArgumentException + */ + private function isAllowedDataObjectMethod($object, string $method): bool + { + if ($object instanceof AbstractExtensibleModel || $object instanceof AbstractModel) { + if (in_array(mb_strtolower($method), $this->restrictedMethods)) { + throw new \InvalidArgumentException("Method $method cannot be called from template."); + } + } + + return true; + } + /** * Return variable value for var construction * @@ -405,21 +460,27 @@ protected function getVariable($value, $default = '{no_value_defined}') || substr($stackVars[$i]['name'], 0, 3) == 'get' ) { $stackVars[$i]['args'] = $this->getStackArgs($stackVars[$i]['args']); - $stackVars[$i]['variable'] = call_user_func_array( - [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], - $stackVars[$i]['args'] - ); + + if ($this->isAllowedDataObjectMethod($stackVars[$i - 1]['variable'], $stackVars[$i]['name'])) { + $stackVars[$i]['variable'] = call_user_func_array( + [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], + $stackVars[$i]['args'] + ); + } } } $last = $i; - } elseif (isset($stackVars[$i - 1]['variable']) && $stackVars[$i]['type'] == 'method') { + } elseif (isset($stackVars[$i - 1]['variable']) + && is_object($stackVars[$i - 1]['variable']) + && $stackVars[$i]['type'] == 'method' + ) { // Calling object methods - if (method_exists($stackVars[$i - 1]['variable'], $stackVars[$i]['name'])) { - $stackVars[$i]['args'] = $this->getStackArgs($stackVars[$i]['args']); - $stackVars[$i]['variable'] = call_user_func_array( - [$stackVars[$i - 1]['variable'], $stackVars[$i]['name']], - $stackVars[$i]['args'] - ); + $object = $stackVars[$i - 1]['variable']; + $method = $stackVars[$i]['name']; + if (method_exists($object, $method)) { + $args = $this->getStackArgs($stackVars[$i]['args']); + $this->validateVariableMethodCall($object, $method); + $stackVars[$i]['variable'] = call_user_func_array([$object, $method], $args); } $last = $i; } diff --git a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php index 4883dc5fbe33b..e4a2dc48d11dd 100644 --- a/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php +++ b/lib/internal/Magento/Framework/Filter/Test/Unit/TemplateTest.php @@ -6,6 +6,8 @@ namespace Magento\Framework\Filter\Test\Unit; +use Magento\Store\Model\Store; + class TemplateTest extends \PHPUnit\Framework\TestCase { /** @@ -13,10 +15,16 @@ class TemplateTest extends \PHPUnit\Framework\TestCase */ private $templateFilter; + /** + * @var Store + */ + private $store; + protected function setUp() { $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->templateFilter = $objectManager->getObject(\Magento\Framework\Filter\Template::class); + $this->store = $objectManager->getObject(Store::class); } public function testFilter() @@ -380,4 +388,55 @@ private function getObjectData() $dataObject->setAllVisibleItems($visibleItems); return $dataObject; } + + /** + * Check that if calling a method of an object fails expected result is returned. + */ + public function testInvalidMethodCall() + { + $this->templateFilter->setVariables(['dateTime' => '\DateTime']); + $this->assertEquals( + '\DateTime', + $this->templateFilter->filter('{{var dateTime.createFromFormat(\'d\',\'1548201468\')}}') + ); + } + + /** + * Test adding callbacks when already filtering. + * + * @expectedException \InvalidArgumentException + */ + public function testInappropriateCallbacks() + { + $this->templateFilter->setVariables(['filter' => $this->templateFilter]); + $this->templateFilter->filter('Test {{var filter.addAfterFilterCallback(\'mb_strtolower\')}}'); + } + + /** + * Test adding callbacks when already filtering. + * + * @expectedException \InvalidArgumentException + * @dataProvider disallowedMethods + */ + public function testDisallowedMethods($method) + { + $this->templateFilter->setVariables(['store' => $this->store]); + $this->templateFilter->filter('{{var store.'.$method.'()}}'); + } + + /** + * Data for testDisallowedMethods method + * + * @return array + */ + public function disallowedMethods() + { + return [ + ['getResourceCollection'], + ['load'], + ['save'], + ['getCollection'], + ['getResource'], + ]; + } } diff --git a/lib/internal/Magento/Framework/Message/Manager.php b/lib/internal/Magento/Framework/Message/Manager.php index 4ef1754b7e586..d71e196deea88 100644 --- a/lib/internal/Magento/Framework/Message/Manager.php +++ b/lib/internal/Magento/Framework/Message/Manager.php @@ -8,6 +8,7 @@ use Magento\Framework\Event; use Psr\Log\LoggerInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Debug; /** * Message manager model @@ -250,7 +251,12 @@ public function addException(\Exception $exception, $alternativeText = null, $gr 'Exception message: %s%sTrace: %s', $exception->getMessage(), "\n", - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); $this->logger->critical($message); @@ -288,7 +294,12 @@ public function addExceptionMessage(\Exception $exception, $alternativeText = nu 'Exception message: %s%sTrace: %s', $exception->getMessage(), "\n", - $exception->getTraceAsString() + Debug::trace( + $exception->getTrace(), + true, + true, + (bool)getenv('MAGE_DEBUG_SHOW_ARGS') + ) ); $this->logger->critical($message); diff --git a/nginx.conf.sample b/nginx.conf.sample index aef22cc55fbbe..979ac0be1f537 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -9,6 +9,7 @@ # listen 80; # server_name mage.dev; # set $MAGE_ROOT /var/www/magento2; +# set $MAGE_DEBUG_SHOW_ARGS 1; # include /vagrant/magento2/nginx.conf.sample; # } # @@ -33,6 +34,12 @@ charset UTF-8; error_page 404 403 = /errors/404.php; #add_header "X-UA-Compatible" "IE=Edge"; + +# Deny access to sensitive files +location /.user.ini { + deny all; +} + # PHP entry point for setup application location ~* ^/setup($|/) { root $MAGE_ROOT; diff --git a/pub/.htaccess b/pub/.htaccess index 8ba04ff4415f3..85a204c85e8a8 100644 --- a/pub/.htaccess +++ b/pub/.htaccess @@ -220,6 +220,16 @@ ErrorDocument 403 /errors/404.php Require all denied +## Deny access to .user.ini + + + order allow,deny + deny from all + + = 2.4> + Require all denied + + ############################################