diff --git a/app/code/Magento/Webapi/Controller/Rest/InputParamsResolver.php b/app/code/Magento/Webapi/Controller/Rest/InputParamsResolver.php index 07d1b4e07fe9d..fa89b7223a92e 100644 --- a/app/code/Magento/Webapi/Controller/Rest/InputParamsResolver.php +++ b/app/code/Magento/Webapi/Controller/Rest/InputParamsResolver.php @@ -6,10 +6,10 @@ namespace Magento\Webapi\Controller\Rest; -use Magento\Framework\Webapi\ServiceInputProcessor; use Magento\Framework\Webapi\Rest\Request as RestRequest; -use Magento\Webapi\Controller\Rest\Router; +use Magento\Framework\Webapi\ServiceInputProcessor; use Magento\Webapi\Controller\Rest\Router\Route; +use Magento\Webapi\Model\UrlDecoder; /** * This class is responsible for retrieving resolved input data @@ -47,26 +47,32 @@ class InputParamsResolver private $requestValidator; /** - * Initialize dependencies - * + * @var UrlDecoder + */ + private $urlDecoder; + + /** * @param RestRequest $request * @param ParamsOverrider $paramsOverrider * @param ServiceInputProcessor $serviceInputProcessor * @param Router $router * @param RequestValidator $requestValidator + * @param UrlDecoder $urlDecoder */ public function __construct( RestRequest $request, ParamsOverrider $paramsOverrider, ServiceInputProcessor $serviceInputProcessor, Router $router, - RequestValidator $requestValidator + RequestValidator $requestValidator, + UrlDecoder $urlDecoder = null ) { $this->request = $request; $this->paramsOverrider = $paramsOverrider; $this->serviceInputProcessor = $serviceInputProcessor; $this->router = $router; $this->requestValidator = $requestValidator; + $this->urlDecoder = $urlDecoder ?: \Magento\Framework\App\ObjectManager::getInstance()->get(UrlDecoder::class); } /** @@ -97,6 +103,7 @@ public function resolve() $inputData = $this->request->getRequestData(); } + $inputData = $this->urlDecoder->decodeParams($inputData); $inputData = $this->paramsOverrider->override($inputData, $route->getParameters()); $inputParams = $this->serviceInputProcessor->process($serviceClassName, $serviceMethodName, $inputData); return $inputParams; diff --git a/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php b/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php index 0e87805daf60a..081b4c829475a 100644 --- a/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php +++ b/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php @@ -17,6 +17,7 @@ use Magento\Webapi\Model\Soap\Config as SoapConfig; use Magento\Framework\Reflection\MethodsMap; use Magento\Webapi\Model\ServiceMetadata; +use Magento\Webapi\Model\UrlDecoder; /** * Handler of requests to SOAP server. @@ -70,8 +71,11 @@ class Handler protected $methodsMapProcessor; /** - * Initialize dependencies. - * + * @var UrlDecoder + */ + private $urlDecoder; + + /** * @param SoapRequest $request * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param SoapConfig $apiConfig @@ -80,6 +84,7 @@ class Handler * @param ServiceInputProcessor $serviceInputProcessor * @param DataObjectProcessor $dataObjectProcessor * @param MethodsMap $methodsMapProcessor + * @param UrlDecoder $urlDecoder */ public function __construct( SoapRequest $request, @@ -89,7 +94,8 @@ public function __construct( SimpleDataObjectConverter $dataObjectConverter, ServiceInputProcessor $serviceInputProcessor, DataObjectProcessor $dataObjectProcessor, - MethodsMap $methodsMapProcessor + MethodsMap $methodsMapProcessor, + UrlDecoder $urlDecoder = null ) { $this->_request = $request; $this->_objectManager = $objectManager; @@ -99,6 +105,7 @@ public function __construct( $this->serviceInputProcessor = $serviceInputProcessor; $this->_dataObjectProcessor = $dataObjectProcessor; $this->methodsMapProcessor = $methodsMapProcessor; + $this->urlDecoder = $urlDecoder ?: \Magento\Framework\App\ObjectManager::getInstance()->get(UrlDecoder::class); } /** @@ -150,6 +157,7 @@ protected function _prepareRequestData($serviceClass, $serviceMethod, $arguments /** SoapServer wraps parameters into array. Thus this wrapping should be removed to get access to parameters. */ $arguments = reset($arguments); $arguments = $this->_dataObjectConverter->convertStdObjectToArray($arguments, true); + $arguments = $this->urlDecoder->decodeParams($arguments); return $this->serviceInputProcessor->process($serviceClass, $serviceMethod, $arguments); } diff --git a/app/code/Magento/Webapi/Model/UrlDecoder.php b/app/code/Magento/Webapi/Model/UrlDecoder.php new file mode 100644 index 0000000000000..f3bab1f45994f --- /dev/null +++ b/app/code/Magento/Webapi/Model/UrlDecoder.php @@ -0,0 +1,35 @@ +decodeParams($param); + } else { + if ($param !== null && is_string($param)) { + $param = rawurldecode($param); + } + } + } + + return $params; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php index cb33edce3af39..09f6362c833d4 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php @@ -47,6 +47,15 @@ class ProductRepositoryInterfaceTest extends WebapiAbstract ProductInterface::TYPE_ID => 'simple', ProductInterface::PRICE => 10 ], + [ + ProductInterface::SKU => [ + 'rest' => 'sku%252fwith%252fslashes', + 'soap' => 'sku%2fwith%2fslashes' + ], + ProductInterface::NAME => 'Simple Product with Sku with Slashes', + ProductInterface::TYPE_ID => 'simple', + ProductInterface::PRICE => 10 + ], ]; /** @@ -135,6 +144,20 @@ public function productCreationProvider() ]; } + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_sku_with_slash.php + */ + public function testGetBySkuWithSlash() + { + $productData = $this->productData[2]; + $response = $this->getProduct($productData[ProductInterface::SKU][TESTS_WEB_API_ADAPTER]); + $productData[ProductInterface::SKU] = rawurldecode($productData[ProductInterface::SKU]['soap']); + foreach ([ProductInterface::SKU, ProductInterface::NAME, ProductInterface::PRICE] as $key) { + $this->assertEquals($productData[$key], $response[$key]); + } + $this->assertEquals([1], $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]["website_ids"]); + } + /** * Test removing association between product and website 1 * @magentoApiDataFixture Magento/Catalog/_files/product_with_two_websites.php diff --git a/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/StockItemTest.php b/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/StockItemTest.php index 0d9f121ed7745..096baa269f3bd 100644 --- a/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/StockItemTest.php +++ b/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/StockItemTest.php @@ -49,11 +49,12 @@ public function setUp() /** * @param array $result + * @param string $productSku + * * @return array */ - protected function getStockItemBySku($result) + protected function getStockItemBySku($result, $productSku) { - $productSku = 'simple1'; $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_GET_PATH . "/$productSku", @@ -69,6 +70,7 @@ protected function getStockItemBySku($result) $apiResult = $this->_webApiCall($serviceInfo, $arguments); $result['item_id'] = $apiResult['item_id']; $this->assertEquals($result, array_intersect_key($apiResult, $result), 'The stock data does not match.'); + return $apiResult; } @@ -76,15 +78,33 @@ protected function getStockItemBySku($result) * @param array $newData * @param array $expectedResult * @param array $fixtureData + * * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php * @dataProvider saveStockItemBySkuWithWrongInputDataProvider */ public function testStockItemPUTWithWrongInput($newData, $expectedResult, $fixtureData) { - $stockItemOld = $this->getStockItemBySku($fixtureData); $productSku = 'simple1'; + $stockItemOld = $this->getStockItemBySku($fixtureData, $productSku); $itemId = $stockItemOld['item_id']; + $actualData = $this->updateStockItemBySku($productSku, $itemId, $newData); + + $this->assertEquals($stockItemOld['item_id'], $actualData); + + /** @var \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory */ + $stockItemFactory = $this->objectManager + ->get(\Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory::class); + $stockItem = $stockItemFactory->create(); + /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\Item $stockItemResource */ + $stockItemResource = $this->objectManager->get(\Magento\CatalogInventory\Model\ResourceModel\Stock\Item::class); + $stockItemResource->loadByProductId($stockItem, $stockItemOld['product_id'], $stockItemOld['stock_id']); + $expectedResult['item_id'] = $stockItem->getItemId(); + $this->assertEquals($expectedResult, array_intersect_key($stockItem->getData(), $expectedResult)); + } + + private function updateStockItemBySku($productSku, $itemId, $newData) + { $resourcePath = str_replace([':productSku', ':itemId'], [$productSku, $itemId], self::RESOURCE_PUT_PATH); $serviceInfo = [ @@ -113,7 +133,124 @@ public function testStockItemPUTWithWrongInput($newData, $expectedResult, $fixtu $data = $stockItemDetailsDo->getData(); $data['show_default_notification_message'] = false; $arguments = ['productSku' => $productSku, 'stockItem' => $data]; - $this->assertEquals($stockItemOld['item_id'], $this->_webApiCall($serviceInfo, $arguments)); + + return $this->_webApiCall($serviceInfo, $arguments); + } + + /** + * @return array + */ + public function saveStockItemBySkuWithWrongInputDataProvider() + { + return [ + [ + [ + 'item_id' => 222, + 'product_id' => 222, + 'stock_id' => 1, + 'qty' => '111.0000', + 'min_qty' => '0.0000', + 'use_config_min_qty' => 1, + 'is_qty_decimal' => 0, + 'backorders' => 0, + 'use_config_backorders' => 1, + 'min_sale_qty' => '1.0000', + 'use_config_min_sale_qty' => 1, + 'max_sale_qty' => '0.0000', + 'use_config_max_sale_qty' => 1, + 'is_in_stock' => 1, + 'low_stock_date' => '', + 'notify_stock_qty' => null, + 'use_config_notify_stock_qty' => 1, + 'manage_stock' => 0, + 'use_config_manage_stock' => 1, + 'stock_status_changed_auto' => 0, + 'use_config_qty_increments' => 1, + 'qty_increments' => '0.0000', + 'use_config_enable_qty_inc' => 1, + 'enable_qty_increments' => 0, + 'is_decimal_divided' => 0, + ], + [ + 'item_id' => '1', + 'product_id' => '10', + 'stock_id' => '1', + 'qty' => '111.0000', + 'min_qty' => '0.0000', + 'use_config_min_qty' => '1', + 'is_qty_decimal' => '0', + 'backorders' => '0', + 'use_config_backorders' => '1', + 'min_sale_qty' => '1.0000', + 'use_config_min_sale_qty' => '1', + 'max_sale_qty' => '0.0000', + 'use_config_max_sale_qty' => '1', + 'is_in_stock' => '1', + 'low_stock_date' => null, + 'notify_stock_qty' => null, + 'use_config_notify_stock_qty' => '1', + 'manage_stock' => '0', + 'use_config_manage_stock' => '1', + 'stock_status_changed_auto' => '0', + 'use_config_qty_increments' => '1', + 'qty_increments' => '0.0000', + 'use_config_enable_qty_inc' => '1', + 'enable_qty_increments' => '0', + 'is_decimal_divided' => '0', + 'type_id' => 'simple', + ], + [ + 'item_id' => 1, + 'product_id' => 10, + 'stock_id' => 1, + 'qty' => 100, + 'is_in_stock' => 1, + 'is_qty_decimal' => '', + 'show_default_notification_message' => '', + 'use_config_min_qty' => 1, + 'min_qty' => 0, + 'use_config_min_sale_qty' => 1, + 'min_sale_qty' => 1, + 'use_config_max_sale_qty' => 1, + 'max_sale_qty' => 10000, + 'use_config_backorders' => 1, + 'backorders' => 0, + 'use_config_notify_stock_qty' => 1, + 'notify_stock_qty' => 1, + 'use_config_qty_increments' => 1, + 'qty_increments' => 0, + 'use_config_enable_qty_inc' => 1, + 'enable_qty_increments' => '', + 'use_config_manage_stock' => 1, + 'manage_stock' => 1, + 'low_stock_date' => '', + 'is_decimal_divided' => '', + 'stock_status_changed_auto' => 0, + ], + ], + ]; + } + + /** + * @param array $newData + * @param array $expectedResult + * @param array $fixtureData + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_sku_with_slash.php + * @dataProvider testUpdateStockItemBySkuDataProvider + */ + public function testUpdateStockItemBySku($newData, $expectedResult, $fixtureData) + { + $productSku = [ + 'rest' => 'sku%252fwith%252fslashes', + 'soap' => 'sku%2fwith%2fslashes' + ]; + $stockItemOld = $this->getStockItemBySku($fixtureData, $productSku[TESTS_WEB_API_ADAPTER]); + $itemId = $stockItemOld['item_id']; + + $actualData = $this->updateStockItemBySku($productSku[TESTS_WEB_API_ADAPTER], $itemId, $newData); + + $this->assertEquals($stockItemOld['item_id'], $actualData); /** @var \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory */ $stockItemFactory = $this->objectManager @@ -126,10 +263,7 @@ public function testStockItemPUTWithWrongInput($newData, $expectedResult, $fixtu $this->assertEquals($expectedResult, array_intersect_key($stockItem->getData(), $expectedResult)); } - /** - * @return array - */ - public function saveStockItemBySkuWithWrongInputDataProvider() + public function testUpdateStockItemBySkuDataProvider() { return [ [ @@ -162,7 +296,7 @@ public function saveStockItemBySkuWithWrongInputDataProvider() ], [ 'item_id' => '1', - 'product_id' => '10', + 'product_id' => '1', 'stock_id' => '1', 'qty' => '111.0000', 'min_qty' => '0.0000', @@ -190,7 +324,7 @@ public function saveStockItemBySkuWithWrongInputDataProvider() ], [ 'item_id' => 1, - 'product_id' => 10, + 'product_id' => 1, 'stock_id' => 1, 'qty' => 100, 'is_in_stock' => 1, diff --git a/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/StockStatusTest.php b/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/StockStatusTest.php index c7e5736b3324c..c3c80621f2348 100644 --- a/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/StockStatusTest.php +++ b/dev/tests/api-functional/testsuite/Magento/CatalogInventory/Api/StockStatusTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogInventory\Api; use Magento\TestFramework\Helper\Bootstrap; @@ -27,6 +28,14 @@ public function testGetProductStockStatus() /** @var \Magento\Catalog\Model\Product $product */ $product = $objectManager->get(\Magento\Catalog\Model\Product::class)->load(1); $expectedData = $product->getQuantityAndStockStatus(); + $actualData = $this->getProductStockStatus($productSku); + $this->assertArrayHasKey('stock_item', $actualData); + $this->assertEquals($expectedData['is_in_stock'], $actualData['stock_item']['is_in_stock']); + $this->assertEquals($expectedData['qty'], $actualData['stock_item']['qty']); + } + + private function getProductStockStatus($productSku) + { $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/$productSku", @@ -41,6 +50,25 @@ public function testGetProductStockStatus() $requestData = ['productSku' => $productSku]; $actualData = $this->_webApiCall($serviceInfo, $requestData); + + return $actualData; + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_sku_with_slash.php + */ + public function testGetProductStockStatusBySkuWithSlashes() + { + $productSku = [ + 'rest' => 'sku%252fwith%252fslashes', + 'soap' => 'sku%2fwith%2fslashes' + ]; + $objectManager = Bootstrap::getObjectManager(); + + /** @var \Magento\Catalog\Model\Product $product */ + $product = $objectManager->get(\Magento\Catalog\Model\Product::class)->load(1); + $expectedData = $product->getQuantityAndStockStatus(); + $actualData = $this->getProductStockStatus($productSku[TESTS_WEB_API_ADAPTER]); $this->assertArrayHasKey('stock_item', $actualData); $this->assertEquals($expectedData['is_in_stock'], $actualData['stock_item']['is_in_stock']); $this->assertEquals($expectedData['qty'], $actualData['stock_item']['qty']); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_sku_with_slash.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_sku_with_slash.php new file mode 100644 index 0000000000000..271eda8a0ffce --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_sku_with_slash.php @@ -0,0 +1,41 @@ +create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(1) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product with Sku with Slashes') + ->setSku('sku/with/slashes') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setDescription('Description with html tag') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + )->setCanSaveCustomOptions(true) + ->setHasOptions(true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepositoryFactory */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_sku_with_slash_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_sku_with_slash_rollback.php new file mode 100644 index 0000000000000..9cd5adae15915 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_sku_with_slash_rollback.php @@ -0,0 +1,24 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +try { + $product = $productRepository->get('sku/with/slashes', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false);