diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php index da7c5326587e3..10487e50c91cf 100644 --- a/app/code/Magento/Catalog/Controller/Category/View.php +++ b/app/code/Magento/Catalog/Controller/Category/View.php @@ -3,6 +3,9 @@ * Copyright 2014 Adobe * All Rights Reserved. */ + +declare(strict_types=1); + namespace Magento\Catalog\Controller\Category; use Magento\Catalog\Api\CategoryRepositoryInterface; @@ -11,8 +14,8 @@ use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; use Magento\Catalog\Model\Design; use Magento\Catalog\Model\Layer\Resolver; -use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; use Magento\Catalog\Model\Product\ProductList\Toolbar; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; use Magento\Catalog\Model\Session; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\Framework\App\Action\Action; @@ -21,8 +24,10 @@ use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\ActionInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Controller\Result\Forward; use Magento\Framework\Controller\Result\ForwardFactory; -use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\ResultInterface; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; @@ -121,8 +126,8 @@ class View extends Action implements HttpGetActionInterface, HttpPostActionInter * @param CategoryRepositoryInterface $categoryRepository * @param ToolbarMemorizer|null $toolbarMemorizer * @param LayoutUpdateManager|null $layoutUpdateManager - * @param CategoryHelper $categoryHelper - * @param LoggerInterface $logger + * @param CategoryHelper|null $categoryHelper + * @param LoggerInterface|null $logger * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -189,7 +194,7 @@ protected function _initCategory() ['category' => $category, 'controller_action' => $this] ); } catch (LocalizedException $e) { - $this->logger->critical($e); + $this->logger->critical((string)$e); return false; } @@ -199,63 +204,99 @@ protected function _initCategory() /** * Category view action * + * @return ResultInterface * @throws NoSuchEntityException */ public function execute() { - $result = null; - if ($this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED)) { //phpcs:ignore Magento2.Legacy.ObsoleteResponse return $this->resultRedirectFactory->create()->setUrl($this->_redirect->getRedirectUrl()); } $category = $this->_initCategory(); - if ($category) { - $this->layerResolver->create(Resolver::CATALOG_LAYER_CATEGORY); - $settings = $this->_catalogDesign->getDesignSettings($category); + if (!$category) { + return $this->resultForwardFactory->create()->forward('noroute'); + } - // apply custom design - if ($settings->getCustomDesign()) { - $this->_catalogDesign->applyCustomDesign($settings->getCustomDesign()); - } + $pageRedirect = $this->handlePageRedirect($category); + if ($pageRedirect) { + return $pageRedirect; + } - $this->_catalogSession->setLastViewedCategoryId($category->getId()); + $page = $this->preparePage($category); - $page = $this->resultPageFactory->create(); - // apply custom layout (page) template once the blocks are generated - if ($settings->getPageLayout()) { - $page->getConfig()->setPageLayout($settings->getPageLayout()); - } + if ($this->shouldRedirectOnToolbarAction()) { + $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); + } - $pageType = $this->getPageType($category); + return $page; + } - if (!$category->hasChildren()) { - // Two levels removed from parent. Need to add default page type. - $parentPageType = strtok($pageType, '_'); - $page->addPageLayoutHandles(['type' => $parentPageType], null, false); - } - $page->addPageLayoutHandles(['type' => $pageType], null, false); - $categoryDisplayMode = is_string($category->getDisplayMode()) ? - strtolower($category->getDisplayMode()) : ''; - $page->addPageLayoutHandles(['displaymode' => $categoryDisplayMode], null, false); - $page->addPageLayoutHandles(['id' => $category->getId()]); + /** + * @param Category $category + * @return Redirect|null + */ + private function handlePageRedirect(Category $category): ?Redirect + { + if ($this->_request->getParam(Toolbar::PAGE_PARM_NAME) < 0) { + return $this->resultRedirectFactory->create() + ->setHttpResponseCode(301) + ->setUrl($category->getUrl()); + } - // apply custom layout update once layout is loaded - $this->applyLayoutUpdates($page, $settings); + return null; + } - $page->getConfig()->addBodyClass('page-products') - ->addBodyClass('categorypath-' . $this->categoryUrlPathGenerator->getUrlPath($category)) - ->addBodyClass('category-' . $category->getUrlKey()); + /** + * @param Category $category + * @return Page + */ + private function preparePage(Category $category): Page + { + $this->layerResolver->create(Resolver::CATALOG_LAYER_CATEGORY); + $settings = $this->_catalogDesign->getDesignSettings($category); - if ($this->shouldRedirectOnToolbarAction()) { - $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); - } - return $page; - } elseif (!$this->getResponse()->isRedirect()) { - $result = $this->resultForwardFactory->create()->forward('noroute'); + if ($settings->getCustomDesign()) { + $this->_catalogDesign->applyCustomDesign($settings->getCustomDesign()); + } + + $this->_catalogSession->setLastViewedCategoryId($category->getId()); + + $page = $this->resultPageFactory->create(); + if ($settings->getPageLayout()) { + $page->getConfig()->setPageLayout($settings->getPageLayout()); + } + + $this->addPageLayoutHandles($page, $category); + $this->applyLayoutUpdates($page, $settings); + + $page->getConfig()->addBodyClass('page-products') + ->addBodyClass('categorypath-' . $this->categoryUrlPathGenerator->getUrlPath($category)) + ->addBodyClass('category-' . $category->getUrlKey()); + + return $page; + } + + /** + * @param Page $page + * @param Category $category + * @return void + */ + private function addPageLayoutHandles(Page $page, Category $category): void + { + $pageType = $this->getPageType($category); + + if (!$category->hasChildren()) { + $parentPageType = strtok($pageType, '_'); + $page->addPageLayoutHandles(['type' => $parentPageType], null, false); } - return $result; + $page->addPageLayoutHandles(['type' => $pageType], null, false); + + $categoryDisplayMode = is_string($category->getDisplayMode()) ? + strtolower($category->getDisplayMode()) : ''; + $page->addPageLayoutHandles(['displaymode' => $categoryDisplayMode], null, false); + $page->addPageLayoutHandles(['id' => $category->getId()]); } /** @@ -264,11 +305,11 @@ public function execute() * @param Category $category * @return string */ - private function getPageType(Category $category) : string + private function getPageType(Category $category): string { $hasChildren = $category->hasChildren(); if ($category->getIsAnchor()) { - return $hasChildren ? 'layered' : 'layered_without_children'; + return $hasChildren ? 'layered' : 'layered_without_children'; } return $hasChildren ? 'default' : 'default_without_children'; @@ -284,7 +325,7 @@ private function getPageType(Category $category) : string private function applyLayoutUpdates( Page $page, DataObject $settings - ) { + ): void { $layoutUpdates = $settings->getLayoutUpdates(); if ($layoutUpdates && is_array($layoutUpdates)) { foreach ($layoutUpdates as $layoutUpdate) { @@ -293,7 +334,6 @@ private function applyLayoutUpdates( } } - //Selected files if ($settings->getPageLayoutHandles()) { $page->addPageLayoutHandles($settings->getPageLayoutHandles()); } @@ -308,11 +348,11 @@ private function shouldRedirectOnToolbarAction(): bool { $params = $this->getRequest()->getParams(); - return $this->toolbarMemorizer->isMemorizingAllowed() && empty(array_intersect([ + return $this->toolbarMemorizer->isMemorizingAllowed() && !empty(array_intersect([ Toolbar::ORDER_PARAM_NAME, Toolbar::DIRECTION_PARAM_NAME, Toolbar::MODE_PARAM_NAME, Toolbar::LIMIT_PARAM_NAME - ], array_keys($params))) === false; + ], array_keys($params))); } } diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPaginationResetOnNegativePageNumberTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPaginationResetOnNegativePageNumberTest.xml new file mode 100644 index 0000000000000..ff1e3d22a2479 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPaginationResetOnNegativePageNumberTest.xml @@ -0,0 +1,47 @@ + + + + + + + + + <description value="Pagination should reset on storefront when negative page number requested"/> + <severity value="AVERAGE"/> + <testCaseId value=""/> + <useCaseId value=""/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProductOne"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwo"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProductOne" stepKey="deleteProductOne"/> + <deleteData createDataKey="createSimpleProductTwo" stepKey="deleteProductTwo"/> + </after> + <!-- Go to category page with `-1` as a Page Number --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}?p=-1" stepKey="goToStorefrontCreatedCategoryPage"/> + <!-- Validate that "no products found" error message is not present --> + <actionGroup ref="StorefrontDontSeeNoProductsFoundActionGroup" stepKey="dontSeeNoProdsFoundMessage"/> + <!-- Verify the products are visible on the Category Page --> + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="seeProductOneOnGrid"> + <argument name="productName" value="$$createSimpleProductOne.name$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="seeProductTwoOnGrid"> + <argument name="productName" value="$$createSimpleProductTwo.name$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Controller/Result/Index.php b/app/code/Magento/CatalogSearch/Controller/Result/Index.php index 61d55d10dd45b..ff32f01494769 100644 --- a/app/code/Magento/CatalogSearch/Controller/Result/Index.php +++ b/app/code/Magento/CatalogSearch/Controller/Result/Index.php @@ -3,8 +3,10 @@ * Copyright 2014 Adobe * All Rights Reserved. */ + namespace Magento\CatalogSearch\Controller\Result; +use Magento\Catalog\Model\Product\ProductList\Toolbar; use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Catalog\Model\Layer\Resolver; use Magento\Catalog\Model\Session; @@ -22,11 +24,9 @@ class Index extends \Magento\Framework\App\Action\Action implements HttpGetActio /** * No results default handle. */ - const DEFAULT_NO_RESULT_HANDLE = 'catalogsearch_result_index_noresults'; + public const DEFAULT_NO_RESULT_HANDLE = 'catalogsearch_result_index_noresults'; /** - * Catalog session - * * @var Session */ protected $_catalogSession; @@ -42,8 +42,6 @@ class Index extends \Magento\Framework\App\Action\Action implements HttpGetActio private $_queryFactory; /** - * Catalog Layer Resolver - * * @var Resolver */ private $layerResolver; @@ -88,28 +86,37 @@ public function execute() $queryText = $query->getQueryText(); - if ($queryText != '') { - $catalogSearchHelper = $this->_objectManager->get(\Magento\CatalogSearch\Helper\Data::class); + if (empty($queryText)) { + $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); + return; + } + + // Negative ?p= value is not supported, redirect to a base version of category page. + if ($this->_request->getParam(Toolbar::PAGE_PARM_NAME) < 0) { + $this->getResponse()->setRedirect( + $this->_url->getUrl('*/*', ['_current' => true, '_query' => [Toolbar::PAGE_PARM_NAME => null]]) + ); + return; + } - $getAdditionalRequestParameters = $this->getRequest()->getParams(); - unset($getAdditionalRequestParameters[QueryFactory::QUERY_VAR_NAME]); + $catalogSearchHelper = $this->_objectManager->get(\Magento\CatalogSearch\Helper\Data::class); - $handles = null; - if ($query->getNumResults() == 0) { - $this->_view->getPage()->initLayout(); - $handles = $this->_view->getLayout()->getUpdate()->getHandles(); - $handles[] = static::DEFAULT_NO_RESULT_HANDLE; - } + $getAdditionalRequestParameters = $this->getRequest()->getParams(); + unset($getAdditionalRequestParameters[QueryFactory::QUERY_VAR_NAME]); - if (empty($getAdditionalRequestParameters) && - $this->_objectManager->get(PopularSearchTerms::class)->isCacheable($queryText, $storeId) - ) { - $this->getCacheableResult($catalogSearchHelper, $query, $handles); - } else { - $this->getNotCacheableResult($catalogSearchHelper, $query, $handles); - } + $handles = null; + if ($query->getNumResults() == 0) { + $this->_view->getPage()->initLayout(); + $handles = $this->_view->getLayout()->getUpdate()->getHandles(); + $handles[] = static::DEFAULT_NO_RESULT_HANDLE; + } + + if (empty($getAdditionalRequestParameters) && + $this->_objectManager->get(PopularSearchTerms::class)->isCacheable($queryText, $storeId) + ) { + $this->getCacheableResult($catalogSearchHelper, $query, $handles); } else { - $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); + $this->getNotCacheableResult($catalogSearchHelper, $query, $handles); } } diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/CatalogSearchWithNegativePageNumberTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/CatalogSearchWithNegativePageNumberTest.xml new file mode 100644 index 0000000000000..a4b9a90ab9bd7 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/CatalogSearchWithNegativePageNumberTest.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright 2019 Adobe + * All Rights Reserved. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CatalogSearchWithNegativePageNumberTest"> + <annotations> + <features value="CatalogSearch"/> + <stories value="Catalog Search Results"/> + <title value="Pagination should reset on Storefront when negative page number requested"/> + <description value="Pagination should reset on Storefront when negative page number requested"/> + <severity value="AVERAGE"/> + <testCaseId value=""/> + <useCaseId value=""/> + <group value="CatalogSearch"/> + <group value="SearchEngine"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProductOne"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProductTwo"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProductOne" stepKey="deleteProductOne"/> + <deleteData createDataKey="createSimpleProductTwo" stepKey="deleteProductTwo"/> + </after> + + <actionGroup ref="StorefrontQuickSearchWithPaginationActionGroup" stepKey="visitSearchResults"> + <argument name="phrase" value="simple"/> + <argument name="pageNumber" value="-1"/> + </actionGroup> + + <!-- Validate that "no products found" error message is not present --> + <actionGroup ref="StorefrontDontSeeNoProductsFoundActionGroup" stepKey="dontSeeNoProdsFoundMessage"/> + <!-- Verify the products are visible on the Category Page --> + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="seeProductOneOnGrid"> + <argument name="productName" value="$$createSimpleProductOne.name$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="seeProductTwoOnGrid"> + <argument name="productName" value="$$createSimpleProductTwo.name$$"/> + </actionGroup> + </test> +</tests>