diff --git a/data/mysql/schema.sql b/data/mysql/schema.sql index 4240633d43c..634b0cf8e3c 100644 --- a/data/mysql/schema.sql +++ b/data/mysql/schema.sql @@ -620,6 +620,7 @@ CREATE TABLE `ezcontentobject` ( `remote_id` varchar(100) DEFAULT NULL, `section_id` int(11) NOT NULL DEFAULT '0', `status` int(11) DEFAULT '0', + `is_hidden` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `ezcontentobject_remote_id` (`remote_id`), KEY `ezcontentobject_classid` (`contentclass_id`), diff --git a/data/update/mysql/dbupdate-7.4.0-to-7.5.0.sql b/data/update/mysql/dbupdate-7.4.0-to-7.5.0.sql index 4c0f86b96d8..83ca09ab4f5 100644 --- a/data/update/mysql/dbupdate-7.4.0-to-7.5.0.sql +++ b/data/update/mysql/dbupdate-7.4.0-to-7.5.0.sql @@ -30,3 +30,9 @@ ADD CONSTRAINT `ezcontentclass_attribute_ml_lang_fk` -- ALTER TABLE `eznotification` MODIFY COLUMN `data` TEXT; + +-- +-- EZP-30139: As an editor I want to hide and reveal a content item +-- + +ALTER TABLE `ezcontentobject` ADD COLUMN `is_hidden` tinyint(1) NOT NULL DEFAULT '0'; diff --git a/data/update/postgres/dbupdate-7.4.0-to-7.5.0.sql b/data/update/postgres/dbupdate-7.4.0-to-7.5.0.sql index 6ace0005b3e..32ed2c6c55f 100644 --- a/data/update/postgres/dbupdate-7.4.0-to-7.5.0.sql +++ b/data/update/postgres/dbupdate-7.4.0-to-7.5.0.sql @@ -31,3 +31,9 @@ ADD CONSTRAINT ezcontentclass_attribute_ml_lang_fk ALTER TABLE eznotification ALTER COLUMN is_pending TYPE BOOLEAN; ALTER TABLE eznotification ALTER COLUMN is_pending SET DEFAULT true; + +-- +-- EZP-30139: As an editor I want to hide and reveal a content item +-- + +ALTER TABLE ezcontentobject ADD is_hidden boolean DEFAULT false NOT NULL; diff --git a/eZ/Publish/API/Repository/ContentService.php b/eZ/Publish/API/Repository/ContentService.php index 217bbfe37e0..9799828f0da 100644 --- a/eZ/Publish/API/Repository/ContentService.php +++ b/eZ/Publish/API/Repository/ContentService.php @@ -439,6 +439,28 @@ public function deleteTranslation(ContentInfo $contentInfo, $languageCode); */ public function deleteTranslationFromDraft(VersionInfo $versionInfo, $languageCode); + /** + * Hides Content by making all the Locations appear hidden. + * It does not persist hidden state on Location object itself. + * + * Content hidden by this API can be revealed by revealContent API. + * + * @see revealContent + * + * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo + */ + public function hideContent(ContentInfo $contentInfo): void; + + /** + * Reveals Content hidden by hideContent API. + * Locations which were hidden before hiding Content will remain hidden. + * + * @see hideContent + * + * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo + */ + public function revealContent(ContentInfo $contentInfo): void; + /** * Instantiates a new content create struct object. * diff --git a/eZ/Publish/API/Repository/Tests/ContentServiceTest.php b/eZ/Publish/API/Repository/Tests/ContentServiceTest.php index 4657bca9e01..6bfe34b05c2 100644 --- a/eZ/Publish/API/Repository/Tests/ContentServiceTest.php +++ b/eZ/Publish/API/Repository/Tests/ContentServiceTest.php @@ -6186,4 +6186,196 @@ private function getExpectedMediaContentInfoProperties() 'status' => ContentInfo::STATUS_PUBLISHED, ]; } + + /** + * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationsToHide + * + * @dataProvider provideLocationsToHideAndReveal + */ + public function testHideContent(array $locationsToHide) + { + $repository = $this->getRepository(); + + $contentTypeService = $repository->getContentTypeService(); + + $contentType = $contentTypeService->loadContentTypeByIdentifier('folder'); + + $contentService = $repository->getContentService(); + $locationService = $repository->getLocationService(); + + $contentCreate = $contentService->newContentCreateStruct($contentType, 'eng-US'); + $contentCreate->setField('name', 'Folder to hide'); + + $content = $contentService->createContent( + $contentCreate, + $locationsToHide + ); + + $publishedContent = $contentService->publishVersion($content->versionInfo); + $locations = $locationService->loadLocations($publishedContent->contentInfo); + + // Sanity check + $this->assertCount(3, $locations); + $this->assertEquals( + [ + false, + false, + false, + ], + array_column($locations, 'hidden') + ); + + /* BEGIN: Use Case */ + $contentService->hideContent($publishedContent->contentInfo); + /* END: Use Case */ + + $hiddenLocations = $locationService->loadLocations($publishedContent->contentInfo); + $this->assertEquals( + [ + true, + true, + true, + ], + array_column($hiddenLocations, 'hidden') + ); + } + + /** + * @param \eZ\Publish\API\Repository\Values\Content\LocationCreateStruct[] $locationsToHide + * + * @dataProvider provideLocationsToHideAndReveal + */ + public function testRevealContent(array $locationsToReveal) + { + $repository = $this->getRepository(); + + $contentTypeService = $repository->getContentTypeService(); + + $contentType = $contentTypeService->loadContentTypeByIdentifier('folder'); + + $contentService = $repository->getContentService(); + $locationService = $repository->getLocationService(); + + $contentCreate = $contentService->newContentCreateStruct($contentType, 'eng-US'); + $contentCreate->setField('name', 'Folder to hide'); + + $locationsToReveal[0]->hidden = true; + + $content = $contentService->createContent( + $contentCreate, + $locationsToReveal + ); + + $publishedContent = $contentService->publishVersion($content->versionInfo); + $locations = $locationService->loadLocations($publishedContent->contentInfo); + + // Sanity check + $this->assertCount(3, $locations); + $this->assertEquals( + [ + true, + false, + false, + ], + array_column($locations, 'hidden') + ); + + /* BEGIN: Use Case */ + $contentService->hideContent($publishedContent->contentInfo); + + $this->assertEquals( + [ + true, + true, + true, + ], + array_column($locationService->loadLocations($publishedContent->contentInfo), 'hidden') + ); + + $contentService->revealContent($publishedContent->contentInfo); + /* END: Use Case */ + + $this->assertEquals( + [ + true, + false, + false, + ], + array_column($locationService->loadLocations($publishedContent->contentInfo), 'hidden') + ); + } + + public function provideLocationsToHideAndReveal() + { + $locationService = $this->getRepository()->getLocationService(); + + return [ + [ + [ + $mainLocationCreateStruct = $locationService->newLocationCreateStruct( + $this->generateId('location', 2) + ), + $otherCreateStruct = $locationService->newLocationCreateStruct( + $this->generateId('location', 5) + ), + $thirdCreateStruct = $locationService->newLocationCreateStruct( + $this->generateId('location', 43) + ), + ], + ], + ]; + } + + public function testHideContentWithParentLocation() + { + $repository = $this->getRepository(); + $contentTypeService = $repository->getContentTypeService(); + + $contentType = $contentTypeService->loadContentTypeByIdentifier('folder'); + + $contentService = $repository->getContentService(); + $locationService = $repository->getLocationService(); + + $contentCreate = $contentService->newContentCreateStruct($contentType, 'eng-US'); + $contentCreate->setField('name', 'Parent'); + + $content = $contentService->createContent( + $contentCreate, + [ + $locationService->newLocationCreateStruct( + $this->generateId('location', 2) + ), + ] + ); + + $publishedContent = $contentService->publishVersion($content->versionInfo); + + /* BEGIN: Use Case */ + $contentService->hideContent($publishedContent->contentInfo); + /* END: Use Case */ + + $locations = $locationService->loadLocations($publishedContent->contentInfo); + + $childContentCreate = $contentService->newContentCreateStruct($contentType, 'eng-US'); + $childContentCreate->setField('name', 'Child'); + + $childContent = $contentService->createContent( + $childContentCreate, + [ + $locationService->newLocationCreateStruct( + $locations[0]->id + ), + ] + ); + + $publishedChildContent = $contentService->publishVersion($childContent->versionInfo); + + $childLocations = $locationService->loadLocations($publishedChildContent->contentInfo); + + $this->assertTrue($locations[0]->hidden); + $this->assertTrue($locations[0]->invisible); + + $this->assertFalse($childLocations[0]->hidden); + $this->assertTrue($childLocations[0]->invisible); + } } diff --git a/eZ/Publish/API/Repository/Tests/SearchServiceLocationTest.php b/eZ/Publish/API/Repository/Tests/SearchServiceLocationTest.php index 131dc42de68..0c63b06ef31 100644 --- a/eZ/Publish/API/Repository/Tests/SearchServiceLocationTest.php +++ b/eZ/Publish/API/Repository/Tests/SearchServiceLocationTest.php @@ -1037,6 +1037,102 @@ public function testMapLocationDistanceWithCustomFieldSort() ); } + /** + * Test for the findLocations() method. + * + * @see \eZ\Publish\API\Repository\SearchService::findLocations() + */ + public function testVisibilityCriterionWithHiddenContent() + { + $repository = $this->getRepository(); + $contentTypeService = $repository->getContentTypeService(); + $contentType = $contentTypeService->loadContentTypeByIdentifier('folder'); + + $contentService = $repository->getContentService(); + $locationService = $repository->getLocationService(); + $searchService = $repository->getSearchService(); + + $testRootContentCreate = $contentService->newContentCreateStruct($contentType, 'eng-US'); + $testRootContentCreate->setField('name', 'Root for test'); + + $rootContent = $contentService->createContent( + $testRootContentCreate, + [ + $locationService->newLocationCreateStruct( + $this->generateId('location', 2) + ), + ] + ); + + $publishedRootContent = $contentService->publishVersion($rootContent->versionInfo); + + $contentCreate = $contentService->newContentCreateStruct($contentType, 'eng-US'); + $contentCreate->setField('name', 'To Hide'); + + $content = $contentService->createContent( + $contentCreate, + [ + $locationService->newLocationCreateStruct( + $publishedRootContent->contentInfo->mainLocationId + ), + ] + ); + $publishedContent = $contentService->publishVersion($content->versionInfo); + + $childContentCreate = $contentService->newContentCreateStruct($contentType, 'eng-US'); + $childContentCreate->setField('name', 'Invisible Child'); + + $childContent = $contentService->createContent( + $childContentCreate, + [ + $locationService->newLocationCreateStruct( + $publishedContent->contentInfo->mainLocationId + ), + ] + ); + $rootLocation = $locationService->loadLocation($publishedRootContent->contentInfo->mainLocationId); + + $contentService->publishVersion($childContent->versionInfo); + $this->refreshSearch($repository); + + $query = new LocationQuery([ + 'query' => new Criterion\LogicalAnd([ + new Criterion\Visibility( + Criterion\Visibility::VISIBLE + ), + new Criterion\Subtree( + $rootLocation->pathString + ), + ]), + ]); + + //Sanity check for visible locations + $result = $searchService->findLocations($query); + $this->assertEquals(3, $result->totalCount); + + //Hide main content + $contentService->hideContent($publishedContent->contentInfo); + $this->refreshSearch($repository); + + $result = $searchService->findLocations($query); + $this->assertEquals(1, $result->totalCount); + + //Query for invisible content + $hiddenQuery = new LocationQuery([ + 'query' => new Criterion\LogicalAnd([ + new Criterion\Visibility( + Criterion\Visibility::HIDDEN + ), + new Criterion\Subtree( + $rootLocation->pathString + ), + ]), + ]); + + $result = $searchService->findLocations($hiddenQuery); + $this->assertEquals(2, $result->totalCount); + } + /** * Assert that query result matches the given fixture. * diff --git a/eZ/Publish/API/Repository/Values/Content/ContentInfo.php b/eZ/Publish/API/Repository/Values/Content/ContentInfo.php index c9750897dd5..77e19cf8cd6 100644 --- a/eZ/Publish/API/Repository/Values/Content/ContentInfo.php +++ b/eZ/Publish/API/Repository/Values/Content/ContentInfo.php @@ -27,6 +27,7 @@ * @property-read string $mainLanguageCode The main language code of the Content object. If the available flag is set to true the Content is shown in this language if the requested language does not exist. * @property-read mixed $mainLocationId Identifier of the main location. * @property-read int $status status of the Content object + * @property-read bool $isHidden status of the Content object */ class ContentInfo extends ValueObject { @@ -140,6 +141,11 @@ class ContentInfo extends ValueObject */ protected $status; + /** + * @var bool + */ + protected $isHidden; + /** * @return bool */ diff --git a/eZ/Publish/Core/Persistence/Cache/LocationHandler.php b/eZ/Publish/Core/Persistence/Cache/LocationHandler.php index 8204aaffd0d..27b46b6e004 100644 --- a/eZ/Publish/Core/Persistence/Cache/LocationHandler.php +++ b/eZ/Publish/Core/Persistence/Cache/LocationHandler.php @@ -227,6 +227,32 @@ public function unHide($locationId) return $return; } + /** + * Sets a location + all children to invisible. + * + * @param int $id Location ID + */ + public function setInvisible(int $id): void + { + $this->logger->logCall(__METHOD__, ['location' => $id]); + $this->persistenceHandler->locationHandler()->setInvisible($id); + + $this->cache->invalidateTags(['location-path-data-' . $id]); + } + + /** + * Sets a location + all children to visible. + * + * @param int $id Location ID + */ + public function setVisible(int $id): void + { + $this->logger->logCall(__METHOD__, ['location' => $id]); + $this->persistenceHandler->locationHandler()->setVisible($id); + + $this->cache->invalidateTags(['location-path-data-' . $id]); + } + /** * {@inheritdoc} */ diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php index ea32363c08f..1a0e6da0561 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase.php @@ -339,6 +339,12 @@ public function updateContent($contentId, MetadataUpdateStruct $struct, VersionI $q->bindValue($mask, null, \PDO::PARAM_INT) ); } + if (isset($struct->isHidden)) { + $q->set( + $this->dbHandler->quoteColumn('is_hidden'), + $q->bindValue($struct->isHidden, null, \PDO::PARAM_BOOL) + ); + } $q->where( $q->expr->eq( $this->dbHandler->quoteColumn('id'), @@ -855,6 +861,7 @@ private function internalLoadContent(array $contentIds, int $version = null, arr 'c.status AS ezcontentobject_status', 'c.name AS ezcontentobject_name', 'c.language_mask AS ezcontentobject_language_mask', + 'c.is_hidden AS ezcontentobject_is_hidden', 'v.id AS ezcontentobject_version_id', 'v.version AS ezcontentobject_version_version', 'v.modified AS ezcontentobject_version_modified', diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase/QueryBuilder.php b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase/QueryBuilder.php index c8eeac81a86..fb6e65ec61f 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase/QueryBuilder.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Gateway/DoctrineDatabase/QueryBuilder.php @@ -215,7 +215,8 @@ public function createVersionInfoFindQuery() $this->dbHandler->aliasedColumn($query, 'published', 'ezcontentobject'), $this->dbHandler->aliasedColumn($query, 'status', 'ezcontentobject'), $this->dbHandler->aliasedColumn($query, 'name', 'ezcontentobject'), - $this->dbHandler->aliasedColumn($query, 'language_mask', 'ezcontentobject') + $this->dbHandler->aliasedColumn($query, 'language_mask', 'ezcontentobject'), + $this->dbHandler->aliasedColumn($query, 'is_hidden', 'ezcontentobject') )->from( $this->dbHandler->quoteTable('ezcontentobject_version') )->innerJoin( diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway.php b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway.php index 7ef1078f74b..36c5172244d 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway.php @@ -170,6 +170,26 @@ abstract public function hideSubtree($pathString); */ abstract public function unHideSubtree($pathString); + /** + * @param string $pathString + **/ + abstract public function setNodeWithChildrenInvisible(string $pathString): void; + + /** + * @param string $pathString + **/ + abstract public function setNodeWithChildrenVisible(string $pathString): void; + + /** + * @param string $pathString + **/ + abstract public function setNodeHidden(string $pathString): void; + + /** + * @param string $pathString + **/ + abstract public function setNodeUnhidden(string $pathString): void; + /** * Swaps the content object being pointed to by a location object. * diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php index 4b97e913d58..7a27bb9693f 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php @@ -420,6 +420,15 @@ public function updateSubtreeModificationTime($pathString, $timestamp = null) * @param string $pathString */ public function hideSubtree($pathString) + { + $this->setNodeWithChildrenInvisible($pathString); + $this->setNodeHidden($pathString); + } + + /** + * @param string $pathString + **/ + public function setNodeWithChildrenInvisible(string $pathString): void { $query = $this->handler->createUpdateQuery(); $query @@ -438,14 +447,30 @@ public function hideSubtree($pathString) $query->bindValue($pathString . '%') ) ); + $query->prepare()->execute(); + } + + /** + * @param string $pathString + **/ + public function setNodeHidden(string $pathString): void + { + $this->setNodeHiddenStatus($pathString, true); + } + /** + * @param string $pathString + * @param bool $isHidden + */ + private function setNodeHiddenStatus(string $pathString, bool $isHidden): void + { $query = $this->handler->createUpdateQuery(); $query ->update($this->handler->quoteTable('ezcontentobject_tree')) ->set( $this->handler->quoteColumn('is_hidden'), - $query->bindValue(1) + $query->bindValue((int) $isHidden) ) ->where( $query->expr->eq( @@ -453,6 +478,7 @@ public function hideSubtree($pathString) $query->bindValue($pathString) ) ); + $query->prepare()->execute(); } @@ -464,22 +490,17 @@ public function hideSubtree($pathString) */ public function unHideSubtree($pathString) { - // Unhide the requested node - $query = $this->handler->createUpdateQuery(); - $query - ->update($this->handler->quoteTable('ezcontentobject_tree')) - ->set( - $this->handler->quoteColumn('is_hidden'), - $query->bindValue(0) - ) - ->where( - $query->expr->eq( - $this->handler->quoteColumn('path_string'), - $query->bindValue($pathString) - ) - ); - $query->prepare()->execute(); + $this->setNodeUnhidden($pathString); + $this->setNodeWithChildrenVisible($pathString); + } + /** + * Sets a location + children to visible unless a parent is hiding the tree. + * + * @param string $pathString + **/ + public function setNodeWithChildrenVisible(string $pathString): void + { // Check if any parent nodes are explicitly hidden $query = $this->handler->createSelectQuery(); $query @@ -497,6 +518,7 @@ public function unHideSubtree($pathString) ) ) ); + $statement = $query->prepare(); $statement->execute(); if (count($statement->fetchAll(\PDO::FETCH_COLUMN))) { @@ -565,7 +587,17 @@ function ($pathString) use ($query, $handler) { ); } $query->where($where); - $statement = $query->prepare()->execute(); + $query->prepare()->execute(); + } + + /** + * Sets location to be unhidden. + * + * @param string $pathString + **/ + public function setNodeUnhidden(string $pathString): void + { + $this->setNodeHiddenStatus($pathString, false); } /** diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php index fe7fbf1b5e8..3f6a308c61c 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php @@ -298,6 +298,62 @@ public function unHideSubtree($pathString) } } + /** + * @param string $pathString + **/ + public function setNodeWithChildrenInvisible(string $pathString): void + { + try { + $this->innerGateway->setNodeWithChildrenInvisible($pathString); + } catch (DBALException $e) { + throw new RuntimeException('Database error', 0, $e); + } catch (PDOException $e) { + throw new RuntimeException('Database error', 0, $e); + } + } + + /** + * @param string $pathString + **/ + public function setNodeHidden(string $pathString): void + { + try { + $this->innerGateway->setNodeHidden($pathString); + } catch (DBALException $e) { + throw new RuntimeException('Database error', 0, $e); + } catch (PDOException $e) { + throw new RuntimeException('Database error', 0, $e); + } + } + + /** + * @param string $pathString + **/ + public function setNodeWithChildrenVisible(string $pathString): void + { + try { + $this->innerGateway->setNodeWithChildrenVisible($pathString); + } catch (DBALException $e) { + throw new RuntimeException('Database error', 0, $e); + } catch (PDOException $e) { + throw new RuntimeException('Database error', 0, $e); + } + } + + /** + * @param string $pathString + **/ + public function setNodeUnhidden(string $pathString): void + { + try { + $this->innerGateway->setNodeUnhidden($pathString); + } catch (DBALException $e) { + throw new RuntimeException('Database error', 0, $e); + } catch (PDOException $e) { + throw new RuntimeException('Database error', 0, $e); + } + } + /** * Swaps the content object being pointed to by a location object. * diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Handler.php b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Handler.php index 53db2739d9f..4a68c5fc8c7 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Location/Handler.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Location/Handler.php @@ -444,6 +444,30 @@ public function unHide($id) $this->locationGateway->unhideSubtree($sourceNodeData['path_string']); } + /** + * Sets a location + all children to invisible. + * + * @param int $id Location ID + */ + public function setInvisible(int $id): void + { + $sourceNodeData = $this->locationGateway->getBasicNodeData($id); + + $this->locationGateway->setNodeWithChildrenInvisible($sourceNodeData['path_string']); + } + + /** + * Sets a location + all children to visible. + * + * @param int $id Location ID + */ + public function setVisible(int $id): void + { + $sourceNodeData = $this->locationGateway->getBasicNodeData($id); + + $this->locationGateway->setNodeWithChildrenVisible($sourceNodeData['path_string']); + } + /** * Swaps the content object being pointed to by a location object. * diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Mapper.php b/eZ/Publish/Core/Persistence/Legacy/Content/Mapper.php index 456353f7d71..2218a60bda2 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Mapper.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Mapper.php @@ -261,6 +261,7 @@ public function extractContentInfoFromRow(array $row, $prefix = '', $treePrefix $contentInfo->mainLocationId = ($row["{$treePrefix}main_node_id"] !== null ? (int)$row["{$treePrefix}main_node_id"] : null); $contentInfo->status = (int)$row["{$prefix}status"]; $contentInfo->isPublished = ($contentInfo->status == ContentInfo::STATUS_PUBLISHED); + $contentInfo->isHidden = (bool)$row["{$prefix}is_hidden"]; return $contentInfo; } diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Gateway/DoctrineDatabaseTest.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Gateway/DoctrineDatabaseTest.php index cd27ca7852c..5d2ff761c4a 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Gateway/DoctrineDatabaseTest.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Gateway/DoctrineDatabaseTest.php @@ -661,7 +661,7 @@ public function testListVersions() foreach ($res as $row) { $this->assertEquals( - 22, + 23, count($row) ); } @@ -719,7 +719,7 @@ public function testListVersionsForUser() foreach ($res as $row) { $this->assertEquals( - 22, + 23, count($row) ); } diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows.php index 0ceae5cd7c9..e86422a5024 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows.php @@ -34,6 +34,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 1 => array( @@ -68,6 +69,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => 'new test article (2)', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 2 => array( @@ -102,6 +104,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => 'something', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 3 => array( @@ -138,6 +141,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 4 => array( @@ -174,6 +178,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 5 => array( @@ -210,6 +215,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 6 => array( @@ -244,6 +250,7 @@ 'ezcontentobject_attribute_sort_key_int' => '1', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 7 => array( @@ -280,6 +287,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 8 => array( @@ -316,6 +324,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 9 => array( @@ -350,6 +359,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 10 => array( @@ -384,6 +394,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 11 => array( @@ -418,6 +429,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), 12 => array( @@ -452,5 +464,6 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '228', + 'ezcontentobject_is_hidden' => '0', ), ); diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows_multiple_versions.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows_multiple_versions.php index 12cd2218796..49997d6cb3a 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows_multiple_versions.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_content_from_rows_multiple_versions.php @@ -34,6 +34,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '12', + 'ezcontentobject_is_hidden' => '0', ), 1 => array( @@ -68,6 +69,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '12', + 'ezcontentobject_is_hidden' => '0', ), 2 => array( @@ -102,6 +104,7 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => 'members', 'ezcontentobject_tree_main_node_id' => '12', + 'ezcontentobject_is_hidden' => '0', ), 3 => array( @@ -136,5 +139,6 @@ 'ezcontentobject_attribute_sort_key_int' => '0', 'ezcontentobject_attribute_sort_key_string' => '', 'ezcontentobject_tree_main_node_id' => '12', + 'ezcontentobject_is_hidden' => '0', ), ); diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_version_info_from_rows_multiple_versions.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_version_info_from_rows_multiple_versions.php index a7e7b0f7f29..110b5b5e8b3 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_version_info_from_rows_multiple_versions.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/_fixtures/extract_version_info_from_rows_multiple_versions.php @@ -24,7 +24,8 @@ 'ezcontentobject_published' => '1033920746', 'ezcontentobject_status' => '1', 'ezcontentobject_name' => 'Members', - 'ezcontentobject_language_mask' => '3' + 'ezcontentobject_language_mask' => '3', + 'ezcontentobject_is_hidden' => '0', ), 1 => array( @@ -49,6 +50,7 @@ 'ezcontentobject_published' => '1033920746', 'ezcontentobject_status' => '1', 'ezcontentobject_name' => 'Members', - 'ezcontentobject_language_mask' => '3' + 'ezcontentobject_language_mask' => '3', + 'ezcontentobject_is_hidden' => '0', ), ); diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.mysql.sql b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.mysql.sql index 3ce74d1fb92..67593956b82 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.mysql.sql +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.mysql.sql @@ -212,6 +212,7 @@ CREATE TABLE ezcontentobject ( remote_id varchar(100) DEFAULT NULL, section_id int(11) NOT NULL DEFAULT 0, status int(11) DEFAULT 0, + is_hidden tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY ezcontentobject_classid (contentclass_id), KEY ezcontentobject_currentversion (current_version), diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.pgsql.sql b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.pgsql.sql index de0a341c42d..cd27267175a 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.pgsql.sql +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.pgsql.sql @@ -192,7 +192,8 @@ CREATE TABLE ezcontentobject ( published integer DEFAULT 0 NOT NULL, remote_id character varying(100), section_id integer DEFAULT 0 NOT NULL, - status integer DEFAULT 0 + status integer DEFAULT 0, + is_hidden boolean DEFAULT FALSE NOT NULL ); DROP TABLE IF EXISTS ezcontentobject_attribute; diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.sqlite.sql b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.sqlite.sql index 0b205bb1173..3e13509e0da 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.sqlite.sql +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.sqlite.sql @@ -183,7 +183,8 @@ CREATE TABLE ezcontentobject ( published integer NOT NULL DEFAULT 0, remote_id text(100), section_id integer NOT NULL DEFAULT 0, - status integer DEFAULT 0 + status integer DEFAULT 0, + is_hidden integer DEFAULT 0 ); CREATE UNIQUE INDEX ezcontentobject_remote_id ON ezcontentobject (remote_id); CREATE INDEX ezcontentobject_classid ON ezcontentobject (contentclass_id); diff --git a/eZ/Publish/Core/REST/Client/ContentService.php b/eZ/Publish/Core/REST/Client/ContentService.php index c2866871f3f..89085d79530 100644 --- a/eZ/Publish/Core/REST/Client/ContentService.php +++ b/eZ/Publish/Core/REST/Client/ContentService.php @@ -753,4 +753,32 @@ public function newTranslationValues() { throw new \Exception('@todo: Implement.'); } + + /** + * Hides Content by making all the Locations appear hidden. + * It does not persist hidden state on Location object itself. + * + * Content hidden by this API can be revealed by revealContent API. + * + * @see revealContent + * + * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo + */ + public function hideContent(ContentInfo $contentInfo): void + { + throw new \Exception('@todo: Implement.'); + } + + /** + * Reveals Content hidden by hideContent API. + * Locations which were hidden before hiding Content will remain hidden. + * + * @see hideContent + * + * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo + */ + public function revealContent(ContentInfo $contentInfo): void + { + throw new \Exception('@todo: Implement.'); + } } diff --git a/eZ/Publish/Core/Repository/ContentService.php b/eZ/Publish/Core/Repository/ContentService.php index 8754a205152..8881edd0806 100644 --- a/eZ/Publish/Core/Repository/ContentService.php +++ b/eZ/Publish/Core/Repository/ContentService.php @@ -2126,6 +2126,76 @@ public function deleteTranslationFromDraft(APIVersionInfo $versionInfo, $languag } } + /** + * Hides Content by making all the Locations appear hidden. + * It does not persist hidden state on Location object itself. + * + * Content hidden by this API can be revealed by revealContent API. + * + * @see revealContent + * + * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo + */ + public function hideContent(ContentInfo $contentInfo): void + { + if (!$this->repository->canUser('content', 'hide', $contentInfo)) { + throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]); + } + + $this->repository->beginTransaction(); + try { + $this->persistenceHandler->contentHandler()->updateMetadata( + $contentInfo->id, + new SPIMetadataUpdateStruct([ + 'isHidden' => true, + ]) + ); + $locationHandler = $this->persistenceHandler->locationHandler(); + $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id); + foreach ($childLocations as $childLocation) { + $locationHandler->setInvisible($childLocation->id); + } + $this->repository->commit(); + } catch (Exception $e) { + $this->repository->rollback(); + throw $e; + } + } + + /** + * Reveals Content hidden by hideContent API. + * Locations which were hidden before hiding Content will remain hidden. + * + * @see hideContent + * + * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo + */ + public function revealContent(ContentInfo $contentInfo): void + { + if (!$this->repository->canUser('content', 'hide', $contentInfo)) { + throw new UnauthorizedException('content', 'hide', ['contentId' => $contentInfo->id]); + } + + $this->repository->beginTransaction(); + try { + $this->persistenceHandler->contentHandler()->updateMetadata( + $contentInfo->id, + new SPIMetadataUpdateStruct([ + 'isHidden' => false, + ]) + ); + $locationHandler = $this->persistenceHandler->locationHandler(); + $childLocations = $locationHandler->loadLocationsByContent($contentInfo->id); + foreach ($childLocations as $childLocation) { + $locationHandler->setVisible($childLocation->id); + } + $this->repository->commit(); + } catch (Exception $e) { + $this->repository->rollback(); + throw $e; + } + } + /** * Instantiates a new content create struct object. * diff --git a/eZ/Publish/Core/Repository/Helper/DomainMapper.php b/eZ/Publish/Core/Repository/Helper/DomainMapper.php index 37b056835ff..09f8ae96d0d 100644 --- a/eZ/Publish/Core/Repository/Helper/DomainMapper.php +++ b/eZ/Publish/Core/Repository/Helper/DomainMapper.php @@ -400,6 +400,7 @@ public function buildContentInfoDomainObject(SPIContentInfo $spiContentInfo) 'mainLanguageCode' => $spiContentInfo->mainLanguageCode, 'mainLocationId' => $spiContentInfo->mainLocationId, 'status' => $status, + 'isHidden' => $spiContentInfo->isHidden, ) ); } @@ -520,7 +521,7 @@ private function mapLocation(SPILocation $spiLocation, ContentInfo $contentInfo, 'contentInfo' => $contentInfo, 'id' => $spiLocation->id, 'priority' => $spiLocation->priority, - 'hidden' => $spiLocation->hidden, + 'hidden' => $spiLocation->hidden || $contentInfo->isHidden, 'invisible' => $spiLocation->invisible, 'remoteId' => $spiLocation->remoteId, 'parentLocationId' => $spiLocation->parentId, diff --git a/eZ/Publish/Core/Repository/SiteAccessAware/ContentService.php b/eZ/Publish/Core/Repository/SiteAccessAware/ContentService.php index 60d947e2d08..39d53ec8db2 100644 --- a/eZ/Publish/Core/Repository/SiteAccessAware/ContentService.php +++ b/eZ/Publish/Core/Repository/SiteAccessAware/ContentService.php @@ -201,6 +201,16 @@ public function loadContentListByContentInfo(array $contentInfoList, array $lang return $this->service->loadContentListByContentInfo($contentInfoList, $languages, $useAlwaysAvailable); } + public function hideContent(ContentInfo $contentInfo): void + { + $this->service->hideContent($contentInfo); + } + + public function revealContent(ContentInfo $contentInfo): void + { + $this->service->revealContent($contentInfo); + } + public function newContentCreateStruct(ContentType $contentType, $mainLanguageCode) { return $this->service->newContentCreateStruct($contentType, $mainLanguageCode); diff --git a/eZ/Publish/Core/Search/Common/Slot/HideContent.php b/eZ/Publish/Core/Search/Common/Slot/HideContent.php new file mode 100644 index 00000000000..f985cf57116 --- /dev/null +++ b/eZ/Publish/Core/Search/Common/Slot/HideContent.php @@ -0,0 +1,31 @@ +persistenceHandler->locationHandler()->loadLocationsByContent($signal->contentId); + foreach ($locations as $location) { + $this->indexSubtree($location->id); + } + } +} diff --git a/eZ/Publish/Core/Search/Common/Slot/RevealContent.php b/eZ/Publish/Core/Search/Common/Slot/RevealContent.php new file mode 100644 index 00000000000..f780f0b7fc5 --- /dev/null +++ b/eZ/Publish/Core/Search/Common/Slot/RevealContent.php @@ -0,0 +1,31 @@ +persistenceHandler->locationHandler()->loadLocationsByContent($signal->contentId); + foreach ($locations as $location) { + $this->indexSubtree($location->id); + } + } +} diff --git a/eZ/Publish/Core/SignalSlot/ContentService.php b/eZ/Publish/Core/SignalSlot/ContentService.php index 5c8b01e4ae0..8ca912f1df5 100644 --- a/eZ/Publish/Core/SignalSlot/ContentService.php +++ b/eZ/Publish/Core/SignalSlot/ContentService.php @@ -21,7 +21,9 @@ use eZ\Publish\API\Repository\Values\User\User; use eZ\Publish\Core\SignalSlot\Signal\ContentService\CreateContentSignal; use eZ\Publish\Core\SignalSlot\Signal\ContentService\DeleteTranslationSignal; +use eZ\Publish\Core\SignalSlot\Signal\ContentService\HideContentSignal; use eZ\Publish\Core\SignalSlot\Signal\ContentService\RemoveTranslationSignal; +use eZ\Publish\Core\SignalSlot\Signal\ContentService\RevealContentSignal; use eZ\Publish\Core\SignalSlot\Signal\ContentService\UpdateContentMetadataSignal; use eZ\Publish\Core\SignalSlot\Signal\ContentService\DeleteContentSignal; use eZ\Publish\Core\SignalSlot\Signal\ContentService\CreateContentDraftSignal; @@ -732,6 +734,44 @@ public function loadContentListByContentInfo( ); } + /** + * Hides Content by making all the Locations appear hidden. + * It does not persist hidden state on Location object itself. + * + * Content hidden by this API can be revealed by revealContent API. + * + * @see revealContent + * + * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo + */ + public function hideContent(ContentInfo $contentInfo): void + { + $this->service->hideContent($contentInfo); + $this->signalDispatcher->emit( + new HideContentSignal([ + 'contentId' => $contentInfo->id, + ]) + ); + } + + /** + * Reveals Content hidden by hideContent API. + * Locations which were hidden before hiding Content will remain hidden. + * + * @see hideContent + * + * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo + */ + public function revealContent(ContentInfo $contentInfo): void + { + $this->service->revealContent($contentInfo); + $this->signalDispatcher->emit( + new RevealContentSignal([ + 'contentId' => $contentInfo->id, + ]) + ); + } + /** * Instantiates a new content create struct object. * diff --git a/eZ/Publish/Core/SignalSlot/Signal/ContentService/HideContentSignal.php b/eZ/Publish/Core/SignalSlot/Signal/ContentService/HideContentSignal.php new file mode 100644 index 00000000000..732c9314064 --- /dev/null +++ b/eZ/Publish/Core/SignalSlot/Signal/ContentService/HideContentSignal.php @@ -0,0 +1,19 @@ +