From 99ad328cd72c607b0256bff073adbea9752b05e0 Mon Sep 17 00:00:00 2001 From: Alexander Bobylev Date: Mon, 18 Oct 2021 11:53:44 +0300 Subject: [PATCH] create a patch --- .gitignore | 1 + LICENSE | 21 ++ README.md | 26 ++ composer.json | 16 + .../module-cms/Model/PageRepository.patch | 16 + .../module-cms/Model/PageRepository.php | 312 ++++++++++++++++++ 6 files changed, 392 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 vendorPatch/magento/module-cms/Model/PageRepository.patch create mode 100644 vendorPatch/magento/module-cms/Model/PageRepository.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bb3336d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Mariusz Łopuch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ed3b28 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Instalation + +Fix for https://github.com/magento/magento2/issues/31007 + +1. Install package by composer: + +`composer require alex-79/magento2-page-save-fixes` + +2. Extend composer.json: + +``` + "extra": { + "enable-patching": true, + "magento-force": "override", + "composer-exit-on-patch-failure": true, + "patches": { + "magento/module-cms": { + "Fix for https://github.com/magento/magento2/issues/31007": "vendor/alex-79/magento2-cms-save-fixes/vendorPatch/magento/module-cms/Model/PageRepository.patch", + } + } + } +``` + +3. composer install + +`composer install` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..49976f8 --- /dev/null +++ b/composer.json @@ -0,0 +1,16 @@ +{ + "name": "alex-79/magento2-page-save-fixes", + "description": "Magento 2 fixes: CMS Page saving fails with 'The value specified in the URL Key field would generate a URL that already exists.'", + "version": "1.0.0", + "minimum-stability": "dev", + "require": { + "cweagans/composer-patches": "^1.7" + }, + "license": "MIT", + "authors": [ + { + "name": "Alexander Bobylev", + "email": "alexander.bobylev@gmail.com" + } + ] +} diff --git a/vendorPatch/magento/module-cms/Model/PageRepository.patch b/vendorPatch/magento/module-cms/Model/PageRepository.patch new file mode 100644 index 0000000..3a1bca6 --- /dev/null +++ b/vendorPatch/magento/module-cms/Model/PageRepository.patch @@ -0,0 +1,16 @@ +diff --git a/magento/module-cms/Model/PageRepository.php b/magento/module-cms/Model/PageRepository.php +index 2816b4c..6508aa2 100644 +--- a/magento/module-cms/Model/PageRepository.php ++++ b/magento/module-cms/Model/PageRepository.php +@@ -188,7 +188,7 @@ class PageRepository implements PageRepositoryInterface + $page->setStoreId($storeId); + } + $this->validateLayoutUpdate($page); +- $this->validateRoutesDuplication($page); ++ // $this->validateRoutesDuplication($page); + $this->resource->save($page); + $this->identityMap->add($page); + } catch (LocalizedException $exception) { +-- +2.20.1 + diff --git a/vendorPatch/magento/module-cms/Model/PageRepository.php b/vendorPatch/magento/module-cms/Model/PageRepository.php new file mode 100644 index 0000000..6508aa2 --- /dev/null +++ b/vendorPatch/magento/module-cms/Model/PageRepository.php @@ -0,0 +1,312 @@ +resource = $resource; + $this->pageFactory = $pageFactory; + $this->pageCollectionFactory = $pageCollectionFactory; + $this->searchResultsFactory = $searchResultsFactory; + $this->dataObjectHelper = $dataObjectHelper; + $this->dataPageFactory = $dataPageFactory; + $this->dataObjectProcessor = $dataObjectProcessor; + $this->storeManager = $storeManager; + $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); + $this->identityMap = $identityMap ?? ObjectManager::getInstance() + ->get(IdentityMap::class); + $this->routeConfig = $routeConfig ?? ObjectManager::getInstance() + ->get(Config::class); + $this->hydrator = $hydrator ?: ObjectManager::getInstance()->get(HydratorInterface::class); + } + + /** + * Validate new layout update values. + * + * @param PageInterface $page + * @return void + * @throws \InvalidArgumentException + */ + private function validateLayoutUpdate(PageInterface $page): void + { + //Persisted data + $oldData = null; + if ($page->getId() && $page instanceof Page) { + $oldData = $page->getOrigData(); + } + //Custom layout update can be removed or kept as is. + if ($page->getCustomLayoutUpdateXml() + && ( + !$oldData + || $page->getCustomLayoutUpdateXml() !== $oldData[Data\PageInterface::CUSTOM_LAYOUT_UPDATE_XML] + ) + ) { + throw new \InvalidArgumentException('Custom layout updates must be selected from a file'); + } + if ($page->getLayoutUpdateXml() + && (!$oldData || $page->getLayoutUpdateXml() !== $oldData[Data\PageInterface::LAYOUT_UPDATE_XML]) + ) { + throw new \InvalidArgumentException('Custom layout updates must be selected from a file'); + } + } + + /** + * Save Page data + * + * @param PageInterface|Page $page + * @return Page + * @throws CouldNotSaveException + */ + public function save(PageInterface $page) + { + try { + $pageId = $page->getId(); + if ($pageId && !($page instanceof Page && $page->getOrigData())) { + $page = $this->hydrator->hydrate($this->getById($pageId), $this->hydrator->extract($page)); + } + if ($page->getStoreId() === null) { + $storeId = $this->storeManager->getStore()->getId(); + $page->setStoreId($storeId); + } + $this->validateLayoutUpdate($page); + // $this->validateRoutesDuplication($page); + $this->resource->save($page); + $this->identityMap->add($page); + } catch (LocalizedException $exception) { + throw new CouldNotSaveException( + __('Could not save the page: %1', $exception->getMessage()), + $exception + ); + } catch (\Throwable $exception) { + throw new CouldNotSaveException( + __('Could not save the page: %1', __('Something went wrong while saving the page.')), + $exception + ); + } + return $page; + } + + /** + * Load Page data by given Page Identity + * + * @param string $pageId + * @return Page + * @throws NoSuchEntityException + */ + public function getById($pageId) + { + $page = $this->pageFactory->create(); + $page->load($pageId); + if (!$page->getId()) { + throw new NoSuchEntityException(__('The CMS page with the "%1" ID doesn\'t exist.', $pageId)); + } + $this->identityMap->add($page); + + return $page; + } + + /** + * Load Page data collection by given search criteria + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @param SearchCriteriaInterface $criteria + * @return PageSearchResultsInterface + */ + public function getList(SearchCriteriaInterface $criteria) + { + $collection = $this->pageCollectionFactory->create(); + + $this->collectionProcessor->process($criteria, $collection); + + $searchResults = $this->searchResultsFactory->create(); + $searchResults->setSearchCriteria($criteria); + $searchResults->setItems($collection->getItems()); + $searchResults->setTotalCount($collection->getSize()); + return $searchResults; + } + + /** + * Delete Page + * + * @param PageInterface $page + * @return bool + * @throws CouldNotDeleteException + */ + public function delete(PageInterface $page) + { + try { + $this->resource->delete($page); + $this->identityMap->remove($page->getId()); + } catch (\Exception $exception) { + throw new CouldNotDeleteException( + __('Could not delete the page: %1', $exception->getMessage()) + ); + } + return true; + } + + /** + * Delete Page by given Page Identity + * + * @param string $pageId + * @return bool + * @throws CouldNotDeleteException + * @throws NoSuchEntityException + */ + public function deleteById($pageId) + { + return $this->delete($this->getById($pageId)); + } + + /** + * Retrieve collection processor + * + * @deprecated 102.0.0 + * @return CollectionProcessorInterface + */ + private function getCollectionProcessor() + { + if (!$this->collectionProcessor) { + // phpstan:ignore "Class Magento\Cms\Model\Api\SearchCriteria\PageCollectionProcessor not found." + $this->collectionProcessor = ObjectManager::getInstance() + ->get(PageCollectionProcessor::class); + } + return $this->collectionProcessor; + } + + /** + * Checks that page identifier doesn't duplicate existed routes + * + * @param PageInterface $page + * @return void + * @throws CouldNotSaveException + */ + private function validateRoutesDuplication($page): void + { + if ($this->routeConfig->getRouteByFrontName($page->getIdentifier(), 'frontend')) { + throw new CouldNotSaveException( + __('The value specified in the URL Key field would generate a URL that already exists.') + ); + } + } +}