diff --git a/composer.json b/composer.json index ed1c1e4229ecfd457434fe07955a982ef1d1534d..0e4b4c346cf6460590637df9e61068307157f135 100644 --- a/composer.json +++ b/composer.json @@ -115,7 +115,7 @@ "drupal/embed": "1.6", "drupal/entity": "1.2", "drupal/entity_browser": "2.8", - "drupal/entity_clone": "1.0.0-beta5", + "drupal/entity_clone": "1.0.0-beta7", "drupal/entity_embed": "1.3", "drupal/entity_reference_revisions": "1.9", "drupal/exif_orientation": "^1.1", @@ -287,7 +287,7 @@ "UTC Time Adjustment": "patches/utc-time-adjustment.patch" }, "drupal/entity_clone": { - "3060223": "https://www.drupal.org/files/issues/2019-10-17/%20entity_clone-corrupted-paragraph-cloning-3060223-5.patch" + "3050027": "https://www.drupal.org/files/issues/2022-10-10/entity_clone-clone_layout_builder-3050027-34.patch" }, "drupal/entity_embed": { "3077225": "https://www.drupal.org/files/issues/2019-12-11/3077225-10.reduce-invalid-config-logs.patch" diff --git a/composer.lock b/composer.lock index 026b86ab08b6c91fe657ad40a0d6231e6e35b3c6..93eb2ed22508e52c7512c4bf91bdc7ff1e90e857 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "57c898b347489d215f8a0e5ace949ee7", + "content-hash": "cdcdcd30a5e546a59b25821bef7e01d6", "packages": [ { "name": "alchemy/zippy", @@ -4024,26 +4024,32 @@ }, { "name": "drupal/entity_clone", - "version": "1.0.0-beta5", + "version": "1.0.0-beta7", "source": { "type": "git", "url": "https://git.drupalcode.org/project/entity_clone.git", - "reference": "8.x-1.0-beta5" + "reference": "8.x-1.0-beta7" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity_clone-8.x-1.0-beta5.zip", - "reference": "8.x-1.0-beta5", - "shasum": "5220d23ac01cd13ff7ef48589e9f6438659a8736" + "url": "https://ftp.drupal.org/files/projects/entity_clone-8.x-1.0-beta7.zip", + "reference": "8.x-1.0-beta7", + "shasum": "964b72b21a7e219cf337743ed5c8850235197594" }, "require": { "drupal/core": "^8 || ^9" }, + "require-dev": { + "drupal/entity_browser": "2.x-dev", + "drupal/entity_usage": "2.x-dev", + "drupal/paragraphs": "^1.0", + "drupal/search_api": "~1.0" + }, "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.0-beta5", - "datestamp": "1615822112", + "version": "8.x-1.0-beta7", + "datestamp": "1663573486", "security-coverage": { "status": "not-covered", "message": "Beta releases are not covered by Drupal security advisories." @@ -4076,8 +4082,8 @@ "homepage": "https://www.drupal.org/user/1361586" } ], - "description": "Add a clone action for all entities", - "homepage": "https://www.drupal.org/project/entity_clone", + "description": "Add a clone action for all entities.", + "homepage": "https://drupal.org/project/entity_clone", "support": { "source": "https://git.drupalcode.org/project/entity_clone" } diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index c91c38182ed39d17a3622cfa18b736aeb6415999..0dd53b17745f3004cdfb3c99051c82e31ef1fbe7 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -4141,34 +4141,40 @@ }, { "name": "drupal/entity_clone", - "version": "1.0.0-beta5", - "version_normalized": "1.0.0.0-beta5", + "version": "1.0.0-beta7", + "version_normalized": "1.0.0.0-beta7", "source": { "type": "git", "url": "https://git.drupalcode.org/project/entity_clone.git", - "reference": "8.x-1.0-beta5" + "reference": "8.x-1.0-beta7" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/entity_clone-8.x-1.0-beta5.zip", - "reference": "8.x-1.0-beta5", - "shasum": "5220d23ac01cd13ff7ef48589e9f6438659a8736" + "url": "https://ftp.drupal.org/files/projects/entity_clone-8.x-1.0-beta7.zip", + "reference": "8.x-1.0-beta7", + "shasum": "964b72b21a7e219cf337743ed5c8850235197594" }, "require": { "drupal/core": "^8 || ^9" }, + "require-dev": { + "drupal/entity_browser": "2.x-dev", + "drupal/entity_usage": "2.x-dev", + "drupal/paragraphs": "^1.0", + "drupal/search_api": "~1.0" + }, "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.0-beta5", - "datestamp": "1615822112", + "version": "8.x-1.0-beta7", + "datestamp": "1663573486", "security-coverage": { "status": "not-covered", "message": "Beta releases are not covered by Drupal security advisories." } }, "patches_applied": { - "3060223": "https://www.drupal.org/files/issues/2019-10-17/%20entity_clone-corrupted-paragraph-cloning-3060223-5.patch" + "3050027": "https://www.drupal.org/files/issues/2022-10-10/entity_clone-clone_layout_builder-3050027-34.patch" } }, "installation-source": "dist", @@ -4177,21 +4183,29 @@ "GPL-2.0-or-later" ], "authors": [ + { + "name": "colan", + "homepage": "https://www.drupal.org/user/58704" + }, { "name": "NickDickinsonWilde", "homepage": "https://www.drupal.org/user/3094661" }, { - "name": "colan", - "homepage": "https://www.drupal.org/user/58704" + "name": "Rajeshreeputra", + "homepage": "https://www.drupal.org/user/3418561" + }, + { + "name": "Upchuk", + "homepage": "https://www.drupal.org/user/1885838" }, { "name": "vpeltot", "homepage": "https://www.drupal.org/user/1361586" } ], - "description": "Add a clone action for all entities", - "homepage": "https://www.drupal.org/project/entity_clone", + "description": "Add a clone action for all entities.", + "homepage": "https://drupal.org/project/entity_clone", "support": { "source": "https://git.drupalcode.org/project/entity_clone" }, diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index dff8f16fe4c7a4658f0eb2a8722c87333ebd10c4..7c016652b4c47f123b47afbb67e1ddaff3f5e6e7 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'osu-asc-webservices/d8-upstream', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '2475984787276b11778650ebbcd6963d3ed140b6', + 'reference' => 'f708cde51271cc92aa33da1cf90b333b9ab06e6d', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -749,9 +749,9 @@ 'dev_requirement' => false, ), 'drupal/entity_clone' => array( - 'pretty_version' => '1.0.0-beta5', - 'version' => '1.0.0.0-beta5', - 'reference' => '8.x-1.0-beta5', + 'pretty_version' => '1.0.0-beta7', + 'version' => '1.0.0.0-beta7', + 'reference' => '8.x-1.0-beta7', 'type' => 'drupal-module', 'install_path' => __DIR__ . '/../../web/modules/entity_clone', 'aliases' => array(), @@ -1594,7 +1594,7 @@ 'osu-asc-webservices/d8-upstream' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '2475984787276b11778650ebbcd6963d3ed140b6', + 'reference' => 'f708cde51271cc92aa33da1cf90b333b9ab06e6d', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/web/modules/entity_clone/PATCHES.txt b/web/modules/entity_clone/PATCHES.txt index c6ac62dbfd6451506c3c00425e0bcdfbea88b370..c6d4fbdf38b279e4173173738bb9b29c3f939f03 100644 --- a/web/modules/entity_clone/PATCHES.txt +++ b/web/modules/entity_clone/PATCHES.txt @@ -1,7 +1,7 @@ This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches) Patches applied to this directory: -3060223 -Source: https://www.drupal.org/files/issues/2019-10-17/%20entity_clone-corrupted-paragraph-cloning-3060223-5.patch +3050027 +Source: https://www.drupal.org/files/issues/2022-10-10/entity_clone-clone_layout_builder-3050027-34.patch diff --git a/web/modules/entity_clone/composer.json b/web/modules/entity_clone/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..103bd42d2cb65dbe4ad3f7df0beffd7fe244bae4 --- /dev/null +++ b/web/modules/entity_clone/composer.json @@ -0,0 +1,13 @@ +{ + "name": "drupal/entity_clone", + "description": "Add a clone action for all entities.", + "homepage": "https://drupal.org/project/entity_clone", + "type": "drupal-module", + "license": "GPL-2.0-or-later", + "require-dev": { + "drupal/entity_browser": "2.x-dev", + "drupal/entity_usage": "2.x-dev", + "drupal/paragraphs": "^1.0", + "drupal/search_api": "~1.0" + } +} diff --git a/web/modules/entity_clone/entity_clone.info.yml b/web/modules/entity_clone/entity_clone.info.yml index 5f4bb1966c6a9cdfc4fe0bf1b8f439ee259271a4..948baa170aadc233212b57411f6d742e59a71eed 100644 --- a/web/modules/entity_clone/entity_clone.info.yml +++ b/web/modules/entity_clone/entity_clone.info.yml @@ -4,8 +4,13 @@ core: "8.x" core_version_requirement: ^8 || ^9 type: module configure: entity_clone.settings +test_dependencies: + - entity_browser:entity_browser + - entity_usage:entity_usage + - paragraphs:paragraphs + - search_api:search_api -# Information added by Drupal.org packaging script on 2021-03-15 -version: '8.x-1.0-beta5' +# Information added by Drupal.org packaging script on 2022-09-19 +version: '8.x-1.0-beta7' project: 'entity_clone' -datestamp: 1615822114 +datestamp: 1663573487 diff --git a/web/modules/entity_clone/entity_clone.module b/web/modules/entity_clone/entity_clone.module index f2bdfba809472c767307830cd4af69fa117aff9a..078b942c0d8020c4efd05c95ad0bdf8ba8124544 100644 --- a/web/modules/entity_clone/entity_clone.module +++ b/web/modules/entity_clone/entity_clone.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Access\AccessResult; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Routing\RouteMatchInterface; @@ -121,7 +122,7 @@ function entity_clone_entity_type_build(array &$entity_types) { * @see \Drupal\Core\Entity\EntityListBuilderInterface::getOperations() */ function entity_clone_entity_operation(EntityInterface $entity) { - if ($entity->hasLinkTemplate('clone-form') && $entity->access('clone')) { + if ($entity->hasLinkTemplate('clone-form') && $entity->access('clone') && !$entity->isNew()) { return [ 'clone' => [ 'title' => t('Clone'), @@ -139,8 +140,39 @@ function entity_clone_entity_operation(EntityInterface $entity) { * Implements hook_entity_access(). */ function entity_clone_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { - if ($operation === 'clone') { - return AccessResult::allowedIfHasPermission($account, 'clone ' . $entity->getEntityTypeId() . ' entity'); + if ($operation !== 'clone') { + return AccessResult::neutral(); } - return AccessResult::neutral(); + + $cache = new CacheableMetadata(); + $cache->addCacheContexts(['user.permissions']); + + // Deny access if the user cannot clone the entity. + $access = AccessResult::forbiddenIf(!$account->hasPermission('clone ' . $entity->getEntityTypeId() . ' entity')); + if ($access->isForbidden()) { + return $access->addCacheableDependency($cache); + } + + // Deny access if the user can clone but cannot create new entities of this + // type. However, we have some exceptions in which the access control handler + // doesn't have a say in things. In these cases, we go based on the clone + // permission only. + $exceptions = [ + 'file', + 'paragraph', + ]; + + if (in_array($entity->getEntityTypeId(), $exceptions)) { + return AccessResult::allowed()->addCacheableDependency($cache); + } + + $handler = \Drupal::entityTypeManager()->getAccessControlHandler($entity->getEntityTypeId()); + $access = $handler->createAccess($entity->bundle(), $account, [], TRUE); + if (!$access->isAllowed()) { + $cache->addCacheableDependency($access); + $forbidden = AccessResult::forbidden(); + return $forbidden->addCacheableDependency($cache); + } + + return AccessResult::allowed()->addCacheableDependency($cache); } diff --git a/web/modules/entity_clone/entity_clone.services.yml b/web/modules/entity_clone/entity_clone.services.yml index 20cf5db105eb6c965ad63eb73bb49622e14b64f5..122aed125d8bca132b8031cca7f1cbec1671ffc5 100644 --- a/web/modules/entity_clone/entity_clone.services.yml +++ b/web/modules/entity_clone/entity_clone.services.yml @@ -7,3 +7,6 @@ services: arguments: ['@entity_type.manager'] tags: - { name: event_subscriber } + entity_clone.service_provider: + class: Drupal\entity_clone\Services\EntityCloneServiceProvider + arguments: [ ] diff --git a/web/modules/entity_clone/src/EntityClone/Content/ContentEntityCloneBase.php b/web/modules/entity_clone/src/EntityClone/Content/ContentEntityCloneBase.php index 449227a722f23186a121d88b0620dbef227d2d95..7515a7dd345da2d120e77388e0a2d8b381cea632 100644 --- a/web/modules/entity_clone/src/EntityClone/Content/ContentEntityCloneBase.php +++ b/web/modules/entity_clone/src/EntityClone/Content/ContentEntityCloneBase.php @@ -2,16 +2,27 @@ namespace Drupal\entity_clone\EntityClone\Content; +use Drupal\block_content\BlockContentInterface; +use Drupal\Component\Datetime\TimeInterface; +use Drupal\content_moderation\Entity\ContentModerationState; +use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityChangedInterface; use Drupal\Core\Entity\EntityHandlerInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Entity\TranslatableInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldConfigInterface; use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Session\AccountProxyInterface; use Drupal\entity_clone\EntityClone\EntityCloneInterface; -use Drupal\Component\Datetime\TimeInterface; +use Drupal\layout_builder\InlineBlockUsageInterface; +use Drupal\layout_builder\Section; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -27,33 +38,53 @@ class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInter protected $entityTypeManager; /** - * The time service. + * The entity type ID. + * + * @var string + */ + protected $entityTypeId; + + /** + * A service for obtaining the system's time. * * @var \Drupal\Component\Datetime\TimeInterface */ - protected $time; + protected $timeService; /** - * The entity type ID. + * The module handler. * - * @var string + * @var \Drupal\Core\Extension\ModuleHandlerInterface */ - protected $entityTypeId; + protected $moduleHandler; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountProxyInterface + */ + protected $currentUser; /** * Constructs a new ContentEntityCloneBase. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. - * @param \Drupal\Component\Datetime\TimeInterface $time - * The time service. * @param string $entity_type_id * The entity type ID. + * @param \Drupal\Component\Datetime\TimeInterface $time_service + * A service for obtaining the system's time. + * @param \Drupal\Core\Session\AccountProxyInterface $current_user + * The current user. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, TimeInterface $time, $entity_type_id) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, $entity_type_id, TimeInterface $time_service, AccountProxyInterface$current_user, ModuleHandlerInterface $module_handler) { $this->entityTypeManager = $entity_type_manager; $this->entityTypeId = $entity_type_id; - $this->time = $time; + $this->timeService = $time_service; + $this->currentUser = $current_user; + $this->moduleHandler = $module_handler; } /** @@ -62,8 +93,10 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Tim public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( $container->get('entity_type.manager'), + $entity_type->id(), $container->get('datetime.time'), - $entity_type->id() + $container->get('current_user'), + $container->get('module_handler') ); } @@ -71,28 +104,114 @@ public static function createInstance(ContainerInterface $container, EntityTypeI * {@inheritdoc} */ public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = [], array &$already_cloned = []) { + if (isset($properties['take_ownership']) && $properties['take_ownership'] === 1) { + $cloned_entity->setOwnerId($this->currentUser->id()); + } // Clone referenced entities. $already_cloned[$entity->getEntityTypeId()][$entity->id()] = $cloned_entity; if ($cloned_entity instanceof FieldableEntityInterface && $entity instanceof FieldableEntityInterface) { foreach ($cloned_entity->getFieldDefinitions() as $field_id => $field_definition) { - if ($this->fieldIsClonable($field_definition)) { + if ($cloning_method = $this->getCloneCallback($field_definition)) { $field = $entity->get($field_id); /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $value */ if ($field->count() > 0) { - $cloned_entity->set($field_id, $this->cloneReferencedEntities($field, $field_definition, $properties, $already_cloned)); + $cloned_entity->set($field_id, $this->$cloning_method($field, $field_definition, $properties, $already_cloned)); } } } } $this->setClonedEntityLabel($entity, $cloned_entity); - $this->setClonedEntityChangedTime($cloned_entity); - $this->setClonedEntityCreatedTime($cloned_entity); + $this->setCreatedAndChangedDates($cloned_entity); + + if ($this->hasTranslatableModerationState($cloned_entity)) { + // If we are using moderation state, ensure that each translation gets + // the same moderation state BEFORE we save so that upon save, each + // translation gets its publishing status updated according to the + // moderation state. After the entity is saved, we kick in the creation + // of translations of created moderation state entity. + foreach ($cloned_entity->getTranslationLanguages(TRUE) as $language) { + $translation = $cloned_entity->getTranslation($language->getId()); + $translation->set('moderation_state', $cloned_entity->get('moderation_state')->value); + } + } + $cloned_entity->save(); - $entity->save(); + + // If we are using content moderation, make sure the moderation state + // entity gets translated to reflect the available translations on the + // source entity. Thus, we call this after the save because we need the + // original moderation state entity to have been created. + if ($this->hasTranslatableModerationState($cloned_entity)) { + $this->setTranslationModerationState($entity, $cloned_entity); + } + + // Set inline block usage after setting the field otherwise there will be + // access issue. + $this->checkAndUpdateInlineBlockUsage($cloned_entity); + return $cloned_entity; } + /** + * Update inline block usage. + * + * @param \Drupal\Core\Entity\EntityInterface $cloned_entity + * The entity. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function checkAndUpdateInlineBlockUsage(EntityInterface $cloned_entity) { + if ($this->moduleHandler->moduleExists('layout_builder') + && $cloned_entity->hasField('layout_builder__layout')) { + + // We cannot inject this one. + /** @var \Drupal\layout_builder\InlineBlockUsageInterface $inline_block_usage */ + $inline_block_usage = \Drupal::service('inline_block.usage'); + + /** @var \Drupal\Core\Entity\EntityStorageInterface $block_content_storage */ + $block_content_storage = $this->entityTypeManager->getStorage('block_content'); + $this->updateInlineBlockUsage($cloned_entity, $inline_block_usage, $block_content_storage); + } + } + + /** + * Update inline block usage. + * + * @param \Drupal\Core\Entity\EntityInterface $cloned_entity + * The entity. + * @param \Drupal\layout_builder\InlineBlockUsageInterface $inline_block_usage + * The inline block usage service. + * @param \Drupal\Core\Entity\EntityStorageInterface $block_content_storage + * The block content's storage. + * + * @return void + * Returns nothings. + */ + protected function updateInlineBlockUsage(EntityInterface $cloned_entity, InlineBlockUsageInterface $inline_block_usage, EntityStorageInterface $block_content_storage) { + $sections = $cloned_entity->get('layout_builder__layout'); + foreach ($sections as $value) { + $section = $value->section; + $components = $section->getComponents(); + + /** @var \Drupal\layout_builder\SectionComponent $component */ + foreach ($components as $component) { + if (!isset($component->toArray()['configuration']['block_revision_id'])) { + continue; + } + + $componentAsArray = $component->toArray(); + $referenced_block = $block_content_storage->loadRevision($componentAsArray['configuration']['block_revision_id']); + + if ($referenced_block) { + $inline_block_usage->addUsage($referenced_block->id(), $cloned_entity); + } + } + } + } + + /** * Determines if a field is clonable. * @@ -100,18 +219,20 @@ public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_ent * The field definition. * * @return bool - * TRUE if th field is clonable; FALSE otherwise. + * TRUE if the field is clonable; FALSE otherwise. */ - protected function fieldIsClonable(FieldDefinitionInterface $field_definition) { + protected function getCloneCallback(FieldDefinitionInterface $field_definition) { $clonable_field_types = [ - 'entity_reference', - 'entity_reference_revisions', + 'entity_reference' => 'cloneReferencedEntities', + 'entity_reference_revisions' => 'cloneReferencedEntities', + 'layout_section' => 'cloneInlineBlockEntities', ]; - $type_is_clonable = in_array($field_definition->getType(), $clonable_field_types, TRUE); + $type_is_clonable = isset($clonable_field_types[$field_definition->getType()]); if (($field_definition instanceof FieldConfigInterface) && $type_is_clonable) { - return TRUE; + return $clonable_field_types[$field_definition->getType()]; } + return FALSE; } @@ -122,6 +243,8 @@ protected function fieldIsClonable(FieldDefinitionInterface $field_definition) { * The original entity. * @param \Drupal\Core\Entity\EntityInterface $cloned_entity * The entity cloned from the original. + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ protected function setClonedEntityLabel(EntityInterface $original_entity, EntityInterface $cloned_entity) { $label_key = $this->entityTypeManager->getDefinition($this->entityTypeId)->getKey('label'); @@ -131,31 +254,86 @@ protected function setClonedEntityLabel(EntityInterface $original_entity, Entity } /** - * Sets the cloned entity's Changed Time. + * Clone inline block entities. * - * @param \Drupal\Core\Entity\EntityInterface $cloned_entity - * The entity cloned from the original. + * @param \Drupal\Core\Field\FieldItemListInterface $field + * The field item. + * @param \Drupal\Core\Field\FieldConfigInterface $field_definition + * The field definition. + * @param array $properties + * All new properties to replace old. + * @param array $already_cloned + * List of all already cloned entities, used for circular references. + * + * @return array + * Referenced entities. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ - protected function setClonedEntityChangedTime(EntityInterface $cloned_entity) { - if ($cloned_entity->hasField('changed')) { - $cloned_entity->set('changed', $this->time->getRequestTime()); + protected function cloneInlineBlockEntities(FieldItemListInterface $field, FieldConfigInterface $field_definition, array $properties, array &$already_cloned) { + $referenced_entities = []; + foreach ($field as $sid => $value) { + if ($value->isEmpty()) { + continue; + } + $section = $value->section; + if (!$section) { + continue; + } + $components = $section->getComponents(); + $section_array = $section->toArray(); + foreach ($components as $component) { + // If it is not a revisionable block, skip. + if (!isset($component->toArray()['configuration']) || !isset($component->toArray()['configuration']['block_revision_id'])) { + continue; + } + $component_array = $component->toArray(); + $configuration = $component_array['configuration']; + $referenced_entity = $this->entityTypeManager + ->getStorage('block_content') + ->loadRevision($configuration['block_revision_id']); + + if ($referenced_entity && $referenced_entity instanceof BlockContentInterface) { + $child_properties = $this->getChildProperties($properties, $field_definition, $referenced_entity); + $component_array['configuration'] = $this->cloneInlineBlock($referenced_entity, $configuration, $child_properties['children'], $already_cloned); + $section_array['components'][$component_array['uuid']] = $component_array; + } + } + $cloned_section = Section::fromArray($section_array); + $referenced_entities[$sid] = $cloned_section; } + return $referenced_entities; } /** - * Sets the cloned entity's Created Time. + * Clone inline block. * - * @param \Drupal\Core\Entity\EntityInterface $cloned_entity - * The entity cloned from the original. + * @param \Drupal\block_content\BlockContentInterface $referenced_entity + * The referenced block. + * @param array $block_configuration + * The configuration of the block. + * @param array $properties + * The entity clone properties. + * @param array $already_cloned + * Already cloned entities. + * + * @return array + * The modified block configuration. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException */ - protected function setClonedEntityCreatedTime(EntityInterface $cloned_entity) { - if ($cloned_entity->hasField('created')) { - $cloned_entity->set('created', $this->time->getRequestTime()); - } + protected function cloneInlineBlock(BlockContentInterface $referenced_entity, array $block_configuration, array $properties, array &$already_cloned) { + $cloned_reference = $referenced_entity->createDuplicate(); + /** @var \Drupal\entity_clone\EntityClone\EntityCloneInterface $entity_clone_handler */ + $entity_clone_handler = $this->entityTypeManager->getHandler($referenced_entity->getEntityTypeId(), 'entity_clone'); + $entity_clone_handler->cloneEntity($referenced_entity, $cloned_reference, $properties, $already_cloned); + $block_configuration['block_revision_id'] = $cloned_reference->getRevisionId(); + return $block_configuration; } /** - * Clone referenced entities. + * Clones referenced entities. * * @param \Drupal\Core\Field\FieldItemListInterface $field * The field item. @@ -168,6 +346,8 @@ protected function setClonedEntityCreatedTime(EntityInterface $cloned_entity) { * * @return array * Referenced entities. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException */ protected function cloneReferencedEntities(FieldItemListInterface $field, FieldConfigInterface $field_definition, array $properties, array &$already_cloned) { $referenced_entities = []; @@ -201,6 +381,7 @@ protected function cloneReferencedEntities(FieldItemListInterface $field, FieldC $referenced_entities[] = $referenced_entity; } } + return $referenced_entities; } @@ -225,7 +406,121 @@ protected function getChildProperties(array $properties, FieldConfigInterface $f if (!isset($child_properties['children'])) { $child_properties['children'] = []; } + return $child_properties; } + /** + * Create moderation_state translations for the cloned entities. + * + * When a new translation is saved, content moderation creates a corresponding + * translation to the moderation_state entity as well. However, for this to + * happen, the translation itself needs to be saved. When we clone, this + * doesn't happen as the original entity gets cloned together with the + * translations and a save is called on the original language being cloned. So + * we have to do this manually. + * + * This is doing essentially what + * Drupal\content_moderation\EntityOperations::updateOrCreateFromEntity but + * we had to replicate it because if a user clones a node translation + * directly, updateOrCreateFromEntity() would not create a translation for + * the original language but would override the language when passing the + * original entity translation. + */ + protected function setTranslationModerationState(ContentEntityInterface $entity, ContentEntityInterface $cloned_entity) { + $languages = $cloned_entity->getTranslationLanguages(); + + // Load the existing moderation state entity for the cloned entity. This + // should exist and have only 1 translation. + $needs_save = FALSE; + $moderation_state = ContentModerationState::loadFromModeratedEntity($cloned_entity); + $original_translation = $cloned_entity->getUntranslated(); + if ($moderation_state->language()->getId() !== $original_translation->language()->getId()) { + // If we are cloning a node while not being in the original translation + // language, Drupal core will set the default language of the moderation + // state to that language whereas the node is simply duplicated and will + // keep the original default language. So we need to change it to that + // also in the moderation state to keep things consistent. + $moderation_state->set($moderation_state->getEntityType()->getKey('langcode'), $original_translation->language()->getId()); + $needs_save = TRUE; + } + + foreach ($languages as $language) { + $translation = $cloned_entity->getTranslation($language->getId()); + if (!$moderation_state->hasTranslation($translation->language()->getId())) { + // We make a 1 to 1 copy of the moderation state entity from the + // original created already by the content_moderation module. This is ok + // because even if translations can be in different moderation states, + // when cloning, the moderation state is reset to whatever the workflow + // default is configured to be. So we anyway should end up with the + // same state across all languages. + $moderation_state->addTranslation($translation->language()->getId(), $moderation_state->toArray()); + $needs_save = TRUE; + } + } + + if ($needs_save) { + ContentModerationState::updateOrCreateFromEntity($moderation_state); + } + } + + /** + * Checks if the entity has the moderation state field and can be moderated. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity. + * + * @return bool + * Whether it can be moderated. + */ + protected function hasTranslatableModerationState(ContentEntityInterface $entity): bool { + if (!$entity->hasField('moderation_state') || !$entity->get('moderation_state') instanceof ModerationStateFieldItemList) { + return FALSE; + } + + return !empty($entity->getTranslationLanguages(FALSE)); + } + + /** + * Resets the created and changed dates on the cloned entity. + * + * Since we don't want the cloned entity to have the old dates (as a new + * entity is being created), we need to reset the created and changed dates + * for those entities that support it. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The cloned entity. + * @param bool $is_translation + * Whether we are recursing over a translation. + */ + protected function setCreatedAndChangedDates(EntityInterface $entity, bool $is_translation = FALSE) { + $created_time = $this->timeService->getRequestTime(); + + // For now, check that the cloned entity has a 'setCreatedTime' method, and + // if so, try to call it. This condition can be replaced with a more-robust + // check whether $cloned_entity is an instance of + // Drupal\Core\Entity\EntityCreatedInterface once + // https://www.drupal.org/project/drupal/issues/2833378 lands. + if (method_exists($entity, 'setCreatedTime')) { + $entity->setCreatedTime($created_time); + } + + // If the entity has a changed time field, we should update it to the + // created time we set above as it cannot possibly be before. + if ($entity instanceof EntityChangedInterface) { + $entity->setChangedTime($created_time); + } + + if ($is_translation) { + return; + } + + if ($entity instanceof TranslatableInterface) { + foreach ($entity->getTranslationLanguages(FALSE) as $language) { + $translation = $entity->getTranslation($language->getId()); + $this->setCreatedAndChangedDates($translation, TRUE); + } + } + } + } diff --git a/web/modules/entity_clone/src/EntityClone/Content/ContentEntityCloneFormBase.php b/web/modules/entity_clone/src/EntityClone/Content/ContentEntityCloneFormBase.php index 0aef96b8bfac8084e29b7608cc78108cdc48eab4..67ff6234ccff4196411d7ee01ea4692a25aebf95 100644 --- a/web/modules/entity_clone/src/EntityClone/Content/ContentEntityCloneFormBase.php +++ b/web/modules/entity_clone/src/EntityClone/Content/ContentEntityCloneFormBase.php @@ -2,6 +2,7 @@ namespace Drupal\entity_clone\EntityClone\Content; +use Drupal\block_content\BlockContentInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\ContentEntityStorageInterface; use Drupal\Core\Entity\EntityHandlerInterface; @@ -15,6 +16,7 @@ use Drupal\Core\StringTranslation\TranslationManager; use Drupal\entity_clone\EntityClone\EntityCloneFormInterface; use Drupal\entity_clone\EntityCloneSettingsManager; +use Drupal\layout_builder\SectionListInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -100,6 +102,12 @@ public function formElement(EntityInterface $entity, $parent = TRUE, &$discovere $form['recursive'] = array_merge($form['recursive'], $this->getRecursiveFormElement($field_definition, $field_id, $field, $discovered_entities)); } } + if ($field_definition instanceof FieldConfigInterface && $field_definition->getType() === 'layout_section') { + $field = $entity->get($field_id); + if ($field->count() > 0) { + $form['recursive'] = array_merge($form['recursive'], $this->getRecursiveLayoutFormElement($field_definition, $field_id, $field, $discovered_entities)); + } + } } if ($parent) { @@ -115,6 +123,87 @@ public function formElement(EntityInterface $entity, $parent = TRUE, &$discovere return $form; } + /** + * Get the recursive Inline Blocks form element. + * + * @param \Drupal\Core\Field\FieldConfigInterface $field_definition + * The field definition. + * @param string $field_id + * The field ID. + * @param \Drupal\layout_builder\SectionListInterface $field + * The field item. + * @param array $discovered_entities + * List of all entities already discovered. + * + * @return array + * The form element for a recursive clone. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getRecursiveLayoutFormElement(FieldConfigInterface $field_definition, $field_id, SectionListInterface $field, array &$discovered_entities) { + $form_element = [ + '#tree' => TRUE, + ]; + + $target = $field_definition->getFieldStorageDefinition()->getSetting('target_type'); + if ($target !== NULL) { + $fieldset_access = !$this->entityCloneSettingsManager->getHiddenValue($target); + } + else { + $fieldset_access = !$this->entityCloneSettingsManager->getLayoutReferencesSetting(); + } + + $form_element[$field_definition->id()] = [ + '#type' => 'fieldset', + '#title' => $this->translationManager->translate('Entities referenced by field <em>@label (@field_id)</em>.', [ + '@label' => $field_definition->label(), + '@field_id' => $field_id, + ]), + '#description' => $this->translationManager->translate('Layout builder blocks always have to be cloned in order to preserve the integrity of the system.'), + '#access' => $fieldset_access, + '#description_should_be_shown' => $fieldset_access, + ]; + + foreach ($field as $value) { + if ($value->isEmpty()) { + continue; + } + $section = $value->section; + if (!$section) { + continue; + } + $components = $section->getComponents(); + foreach ($components as $component) { + // If it is not a revisionable block, skip. + if (!isset($component->toArray()['configuration']) || !isset($component->toArray()['configuration']['block_revision_id'])) { + continue; + } + $configuration = $component->toArray()['configuration']; + $referenced_entity = \Drupal::entityTypeManager()->getStorage('block_content')->loadRevision($configuration['block_revision_id']); + if ($referenced_entity instanceof BlockContentInterface && $referenced_entity->id()) { + $form_element[$field_definition->id()]['references'][$referenced_entity->id()]['clone'] = [ + '#type' => 'checkbox', + '#title' => $this->translationManager->translate('Clone entity <strong>ID:</strong> <em>@entity_id</em>, <strong>Type:</strong> <em>@entity_type - @bundle</em>, <strong>Label:</strong> <em>@entity_label</em>', [ + '@entity_id' => $referenced_entity->id(), + '@entity_type' => $referenced_entity->getEntityTypeId(), + '@bundle' => $referenced_entity->bundle(), + '@entity_label' => $referenced_entity->label(), + ]), + '#default_value' => TRUE, + '#disabled' => TRUE, + '#access' => $referenced_entity->access('view label'), + ]; + } + if ($referenced_entity instanceof ContentEntityInterface) { + $form_element[$field_definition->id()]['references'][$referenced_entity->id()]['children'] = $this->getChildren($referenced_entity, $discovered_entities); + } + } + } + + return $form_element; + } + /** * Get the recursive form element. * diff --git a/web/modules/entity_clone/src/EntityClone/Content/FileEntityClone.php b/web/modules/entity_clone/src/EntityClone/Content/FileEntityClone.php index 9b7b592a80b392e52302c47402b5945c7a998932..4821b0d01e2c408ea38e9bbf51e4271f6199e9e6 100644 --- a/web/modules/entity_clone/src/EntityClone/Content/FileEntityClone.php +++ b/web/modules/entity_clone/src/EntityClone/Content/FileEntityClone.php @@ -16,6 +16,9 @@ class FileEntityClone extends ContentEntityCloneBase { public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = [], array &$already_cloned = []) { /** @var \Drupal\file\FileInterface $cloned_entity */ $cloned_file = file_copy($cloned_entity, $cloned_entity->getFileUri(), FileSystemInterface::EXISTS_RENAME); + if (isset($properties['take_ownership']) && $properties['take_ownership'] === 1) { + $cloned_file->setOwnerId(\Drupal::currentUser()->id()); + } return parent::cloneEntity($entity, $cloned_file, $properties); } diff --git a/web/modules/entity_clone/src/EntityClonePermissions.php b/web/modules/entity_clone/src/EntityClonePermissions.php index 28f9f3717db7eb7ced62883d3b8ed1390d8c0fd1..2de222784cd5d734fc1185fe8f7556c529a690a3 100644 --- a/web/modules/entity_clone/src/EntityClonePermissions.php +++ b/web/modules/entity_clone/src/EntityClonePermissions.php @@ -6,6 +6,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\StringTranslation\TranslationManager; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\entity_clone\Services\EntityCloneServiceProvider; /** * Provides dynamic permissions of the entity_clone module. @@ -26,6 +27,13 @@ class EntityClonePermissions implements ContainerInjectionInterface { */ protected $translationManager; + /** + * The Service Provider that verifies if entity has ownership. + * + * @var \Drupal\entity_clone\Services\EntityCloneServiceProvider + */ + protected $serviceProvider; + /** * Constructs a new EntityClonePermissions instance. * @@ -33,10 +41,13 @@ class EntityClonePermissions implements ContainerInjectionInterface { * The entity type manager. * @param \Drupal\Core\StringTranslation\TranslationManager $string_translation * The string translation manager. + * @param \\Drupal\entity_clone\Services\EntityCloneServiceProvider $service_provider + * The Service Provider that verifies if entity has ownership. */ - public function __construct(EntityTypeManagerInterface $entity_manager, TranslationManager $string_translation) { + public function __construct(EntityTypeManagerInterface $entity_manager, TranslationManager $string_translation, EntityCloneServiceProvider $service_provider) { $this->entityTypeManager = $entity_manager; $this->translationManager = $string_translation; + $this->serviceProvider = $service_provider; } /** @@ -45,7 +56,8 @@ public function __construct(EntityTypeManagerInterface $entity_manager, Translat public static function create(ContainerInterface $container) { return new static( $container->get('entity_type.manager'), - $container->get('string_translation') + $container->get('string_translation'), + $container->get('entity_clone.service_provider') ); } @@ -62,6 +74,12 @@ public function permissions() { $permissions['clone ' . $entity_type_id . ' entity'] = $this->translationManager->translate('Clone all <em>@label</em> entities.', [ '@label' => $entity_type->getLabel(), ]); + + if ($this->serviceProvider->entityTypeHasOwnerTrait($entity_type)) { + $permissions['take_ownership_on_clone ' . $entity_type_id . ' entity'] = $this->translationManager->translate('Allow user to take ownership of <em>@label</em> cloned entities', [ + '@label' => $entity_type->getLabel(), + ]); + } } return $permissions; diff --git a/web/modules/entity_clone/src/EntityCloneSettingsManager.php b/web/modules/entity_clone/src/EntityCloneSettingsManager.php index 59899a3fb0199e8d9603a8d20bb1c11b3e5a5673..29ac9c4bed6c54104ce5ed9d59129556622f221f 100644 --- a/web/modules/entity_clone/src/EntityCloneSettingsManager.php +++ b/web/modules/entity_clone/src/EntityCloneSettingsManager.php @@ -146,4 +146,38 @@ public function getHiddenValue($entity_type_id) { return FALSE; } + /** + * Set the take ownership setting. + * + * @param int $setting + * The settings from the form. + */ + public function setTakeOwnershipSettings(int $setting) { + $this->editableConfig->set('take_ownership', $setting)->save(); + } + + /** + * Get the take ownership settings. + */ + public function getTakeOwnershipSetting() { + return $this->config->get('take_ownership') ?? FALSE; + } + + /** + * Set the layout builder references setting. + * + * @param int $setting + * The settings from the form. + */ + public function setLayoutReferencesSettings(int $setting) { + $this->editableConfig->set('hide_layout_builder_referenced_entities', $setting)->save(); + } + + /** + * Get the layout builder references settings. + */ + public function getLayoutReferencesSetting() { + return $this->config->get('hide_layout_builder_referenced_entities') ?? FALSE; + } + } diff --git a/web/modules/entity_clone/src/Form/EntityCloneForm.php b/web/modules/entity_clone/src/Form/EntityCloneForm.php index b9dfb3879009c2e956af46f28e57458931da6ec9..f99e011bd54471a66e4ad3eb62a9cd034a751a03 100644 --- a/web/modules/entity_clone/src/Form/EntityCloneForm.php +++ b/web/modules/entity_clone/src/Form/EntityCloneForm.php @@ -8,7 +8,10 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Messenger\Messenger; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\StringTranslation\TranslationManager; +use Drupal\entity_clone\Services\EntityCloneServiceProvider; +use Drupal\entity_clone\EntityCloneSettingsManager; use Drupal\entity_clone\Event\EntityCloneEvent; use Drupal\entity_clone\Event\EntityCloneEvents; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -61,6 +64,27 @@ class EntityCloneForm extends FormBase { */ protected $messenger; + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * The entity clone settings manager service. + * + * @var \Drupal\entity_clone\EntityCloneSettingsManager + */ + protected $entityCloneSettingsManager; + + /** + * The Service Provider that verifies if entity has ownership. + * + * @var \Drupal\entity_clone\Services\EntityCloneServiceProvider + */ + protected $serviceProvider; + /** * Constructs a new Entity Clone form. * @@ -74,10 +98,16 @@ class EntityCloneForm extends FormBase { * The event dispatcher service. * @param \Drupal\Core\Messenger\Messenger $messenger * The messenger service. + * @param \Drupal\Core\Session\AccountProxyInterface $currentUser + * The current user. + * @param \Drupal\entity_clone\EntityCloneSettingsManager $entity_clone_settings_manager + * The entity clone settings manager. + * @param \Drupal\entity_clone\Services\EntityCloneServiceProvider $service_provider + * The Service Provider that verifies if entity has ownership. * * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match, TranslationManager $string_translation, EventDispatcherInterface $eventDispatcher, Messenger $messenger) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match, TranslationManager $string_translation, EventDispatcherInterface $eventDispatcher, Messenger $messenger, AccountProxyInterface $currentUser, EntityCloneSettingsManager $entity_clone_settings_manager, EntityCloneServiceProvider $service_provider) { $this->entityTypeManager = $entity_type_manager; $this->stringTranslationManager = $string_translation; $this->eventDispatcher = $eventDispatcher; @@ -87,6 +117,9 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Rou $this->entity = $route_match->getParameter($parameter_name); $this->entityTypeDefinition = $entity_type_manager->getDefinition($this->entity->getEntityTypeId()); + $this->currentUser = $currentUser; + $this->entityCloneSettingsManager = $entity_clone_settings_manager; + $this->serviceProvider = $service_provider; } /** @@ -98,7 +131,10 @@ public static function create(ContainerInterface $container) { $container->get('current_route_match'), $container->get('string_translation'), $container->get('event_dispatcher'), - $container->get('messenger') + $container->get('messenger'), + $container->get('current_user'), + $container->get('entity_clone.settings.manager'), + $container->get('entity_clone.service_provider') ); } @@ -120,6 +156,15 @@ public function buildForm(array $form, FormStateInterface $form_state) { $entity_clone_form_handler = $this->entityTypeManager->getHandler($this->entityTypeDefinition->id(), 'entity_clone_form'); $form = array_merge($form, $entity_clone_form_handler->formElement($this->entity)); } + $entityType = $this->getEntity()->getEntityTypeId(); + if ($this->serviceProvider->entityTypeHasOwnerTrait($this->getEntity()->getEntityType()) && $this->currentUser->hasPermission('take_ownership_on_clone ' . $entityType . ' entity')) { + $form['take_ownership'] = [ + '#type' => 'checkbox', + '#title' => $this->stringTranslationManager->translate('Take ownership'), + '#default_value' => $this->entityCloneSettingsManager->getTakeOwnershipSetting(), + '#description' => $this->stringTranslationManager->translate('Take ownership of the newly created cloned entity.'), + ]; + } $form['actions'] = ['#type' => 'actions']; $form['actions']['clone'] = [ @@ -186,7 +231,7 @@ public function cancelForm(array &$form, FormStateInterface $form_state) { } /** - * Set a redirect on form state. + * Sets a redirect on form state. * * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. @@ -195,7 +240,7 @@ public function cancelForm(array &$form, FormStateInterface $form_state) { */ protected function formSetRedirect(FormStateInterface $form_state, EntityInterface $entity) { if ($entity && $entity->hasLinkTemplate('canonical')) { - $form_state->setRedirect($entity->toUrl()->getRouteName(), $entity->toUrl()->getRouteParameters()); + $form_state->setRedirect($entity->toUrl('canonical')->getRouteName(), $entity->toUrl('canonical')->getRouteParameters()); } else { $form_state->setRedirect('<front>'); diff --git a/web/modules/entity_clone/src/Form/EntityCloneSettingsForm.php b/web/modules/entity_clone/src/Form/EntityCloneSettingsForm.php index 9dde82bf5b60c2eec92c0e1cdf6cbc6f3396ef7a..e1c57fbea9c8f5f96fa250993331f303e933d494 100644 --- a/web/modules/entity_clone/src/Form/EntityCloneSettingsForm.php +++ b/web/modules/entity_clone/src/Form/EntityCloneSettingsForm.php @@ -111,6 +111,20 @@ public function buildForm(array $form, FormStateInterface $form_state) { ]; } + $form['take_ownership'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Take ownership'), + '#description' => $this->t('Whether the "Take ownership" option should be checked by default on the entity clone form.'), + '#default_value' => $this->entityCloneSettingsManager->getTakeOwnershipSetting(), + ]; + + $form['hide_layout_builder_referenced_entities'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Hide Layout builder referenced entities checkboxes'), + '#description' => $this->t('Whether the "Entities referenced by field Layout" information should be shown on the entity clone form.'), + '#default_value' => $this->entityCloneSettingsManager->getLayoutReferencesSetting(), + ]; + return parent::buildForm($form, $form_state); } @@ -119,6 +133,8 @@ public function buildForm(array $form, FormStateInterface $form_state) { */ public function submitForm(array &$form, FormStateInterface $form_state) { $this->entityCloneSettingsManager->setFormSettings($form_state->getValue('form_settings')); + $this->entityCloneSettingsManager->setTakeOwnershipSettings($form_state->getValue('take_ownership')); + $this->entityCloneSettingsManager->setLayoutReferencesSettings($form_state->getValue('hide_layout_builder_referenced_entities')); parent::submitForm($form, $form_state); } diff --git a/web/modules/entity_clone/src/Services/EntityCloneServiceProvider.php b/web/modules/entity_clone/src/Services/EntityCloneServiceProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..8f9f04d1115e474679cd19311d67645500407ada --- /dev/null +++ b/web/modules/entity_clone/src/Services/EntityCloneServiceProvider.php @@ -0,0 +1,39 @@ +<?php + +namespace Drupal\entity_clone\Services; + +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\user\EntityOwnerTrait; + +/** + * Service Provider Class. + */ +class EntityCloneServiceProvider { + + /** + * Constructs a new ServiceProvider object. + */ + public function __construct() {} + + /** + * Checks if the given entity implements has owner trait. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity + * Entity to be tested. + * + * @return bool + * Returns boolean for the owner trait test. + */ + public function entityTypeHasOwnerTrait(EntityTypeInterface $entityType) { + try { + $reflectionClass = new \ReflectionClass($entityType->getOriginalClass()); + } catch (\ReflectionException $e) { + return FALSE; + } + return in_array( + EntityOwnerTrait::class, + array_keys($reflectionClass->getTraits()) + ); + } + +} diff --git a/web/modules/entity_clone/tests/src/Functional/EntityCloneContentModerationTest.php b/web/modules/entity_clone/tests/src/Functional/EntityCloneContentModerationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..391b5878f41699f0b6bce8e054732a1d1797f480 --- /dev/null +++ b/web/modules/entity_clone/tests/src/Functional/EntityCloneContentModerationTest.php @@ -0,0 +1,274 @@ +<?php + +namespace Drupal\Tests\entity_clone\Functional; + +use Drupal\content_moderation\Entity\ContentModerationState as ContentModerationStateEntity; +use Drupal\Core\Url; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\node\Entity\Node; +use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait; +use Drupal\Tests\node\Functional\NodeTestBase; + +/** + * Create a moderated content and test the clone of its moderation state. + * + * @group entity_clone + */ +class EntityCloneContentModerationTest extends NodeTestBase { + + use ContentModerationTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'entity_clone', + 'content_moderation', + 'language', + 'content_translation', + 'block', + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'classy'; + + /** + * Permissions to grant admin user. + * + * @var array + */ + protected $permissions = [ + 'bypass node access', + 'administer nodes', + 'clone node entity', + 'use editorial transition create_new_draft', + 'use editorial transition publish', + 'use editorial transition archive', + 'use editorial transition archived_draft', + 'use editorial transition archived_published', + ]; + + /** + * A user with permission to bypass content access checks. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * Sets the test up. + */ + protected function setUp(): void { + parent::setUp(); + + ConfigurableLanguage::createFromLangcode('fr')->save(); + \Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE); + $workflow = $this->createEditorialWorkflow(); + $this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'page'); + + $this->adminUser = $this->drupalCreateUser($this->permissions); + $this->drupalLogin($this->adminUser); + } + + /** + * Test content entity clone. + */ + public function testContentModerationEntityClone() { + $node = Node::create([ + 'type' => 'page', + 'title' => 'My node', + ]); + + $node->save(); + $translation = $node->addTranslation('fr', $node->toArray()); + // Unfortunately content moderation only creates translations to the + // moderation state entities when the actual translation of the source + // entity gets saved (as opposed to an original node with multiple + // translations). + $translation->save(); + + // Assert that we have a moderation state translation for each language. + $node = Node::load($node->id()); + $this->assertCount(2, $node->getTranslationLanguages()); + $moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($node); + $this->assertFalse($moderation_state->isNew()); + $this->assertCount(2, $moderation_state->getTranslationLanguages()); + foreach ($moderation_state->getTranslationLanguages() as $language) { + $this->assertEquals('draft', $moderation_state->getTranslation($language->getId())->get('moderation_state')->value); + } + $moderation_state_id = $moderation_state->id(); + + // Clone the node and assert that the moderation state is cloned and has + // a translation for each language. + $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); + $this->submitForm([], t('Clone')); + + $nodes = \Drupal::entityTypeManager() + ->getStorage('node') + ->loadByProperties([ + 'title' => 'My node - Cloned', + ]); + $clone = reset($nodes); + $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); + + $this->assertCount(2, $clone->getTranslationLanguages()); + $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); + $this->assertNotEquals($moderation_state_id, $clone_moderation_state->id()); + $this->assertFalse($clone_moderation_state->isNew()); + $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); + foreach ($clone_moderation_state->getTranslationLanguages() as $language) { + $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); + } + + // Create another node, but this time, move the state to published. + $node = Node::create([ + 'type' => 'page', + 'title' => 'My second node', + ]); + $node->save(); + $node->set('moderation_state', 'published'); + $node->setNewRevision(); + $node->save(); + $translation = $node->addTranslation('fr', $node->toArray()); + $translation->save(); + $moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($node); + $this->assertFalse($moderation_state->isNew()); + $this->assertCount(2, $moderation_state->getTranslationLanguages()); + foreach ($moderation_state->getTranslationLanguages() as $language) { + $this->assertEquals('published', $moderation_state->getTranslation($language->getId())->get('moderation_state')->value); + } + + // Clone the node and assert that the moderation state is cloned and has + // a translation for each language. + $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); + $this->submitForm([], t('Clone')); + + $nodes = \Drupal::entityTypeManager() + ->getStorage('node') + ->loadByProperties([ + 'title' => 'My second node - Cloned', + ]); + $clone = reset($nodes); + $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); + + $this->assertCount(2, $clone->getTranslationLanguages()); + $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); + $this->assertFalse($clone_moderation_state->isNew()); + $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); + foreach ($clone_moderation_state->getTranslationLanguages() as $language) { + // When we clone, the default moderation state is set on the clone for + // both languages (draft), even if the cloned content was published. + $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); + } + + // Create another node, but this time the original should be published but + // the translation should be draft. + $node = Node::create([ + 'type' => 'page', + 'title' => 'My third node', + ]); + $node->save(); + $translation = $node->addTranslation('fr', $node->toArray()); + $translation->save(); + $node->set('moderation_state', 'published'); + $node->setNewRevision(); + $node->save(); + + $moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($node); + $this->assertFalse($moderation_state->isNew()); + $this->assertCount(2, $moderation_state->getTranslationLanguages()); + $expected_map = [ + 'en' => 'published', + 'fr' => 'draft', + ]; + foreach ($moderation_state->getTranslationLanguages() as $language) { + $this->assertEquals($expected_map[$language->getId()], $moderation_state->getTranslation($language->getId())->get('moderation_state')->value); + } + $this->assertTrue($node->getTranslation('en')->isPublished()); + $this->assertFalse($node->getTranslation('fr')->isPublished()); + + // Clone the node and assert that the moderation state is reset to draft + // for both languages. + $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); + $this->submitForm([], t('Clone')); + + $nodes = \Drupal::entityTypeManager() + ->getStorage('node') + ->loadByProperties([ + 'title' => 'My third node - Cloned', + ]); + $clone = reset($nodes); + $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); + + $this->assertCount(2, $clone->getTranslationLanguages()); + $this->assertFalse($clone->getTranslation('en')->isPublished()); + $this->assertFalse($clone->getTranslation('fr')->isPublished()); + $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); + $this->assertFalse($clone_moderation_state->isNew()); + $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); + foreach ($clone_moderation_state->getTranslationLanguages() as $language) { + $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); + } + + // Create another node but this time clone while on the French and assert + // that the moderation state default language is the same as of the node. + $node = Node::create([ + 'type' => 'page', + 'title' => 'My fourth node', + ]); + + $node->save(); + $translation = $node->addTranslation('fr', ['title' => 'My fourth node FR'] + $node->toArray()); + $translation->save(); + $node = Node::load($node->id()); + $this->assertCount(2, $node->getTranslationLanguages()); + $this->drupalGet(Url::fromUserInput('/fr/entity_clone/node/' . $node->id())); + $this->submitForm([], t('Clone')); + + $clone = Node::load($node->id() + 1); + $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); + + $this->assertCount(2, $clone->getTranslationLanguages()); + $this->assertEquals('My fourth node FR - Cloned', $clone->getTranslation('fr')->label()); + $clone_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($clone); + $this->assertFalse($clone_moderation_state->isNew()); + $this->assertCount(2, $clone_moderation_state->getTranslationLanguages()); + foreach ($clone_moderation_state->getTranslationLanguages() as $language) { + $this->assertEquals('draft', $clone_moderation_state->getTranslation($language->getId())->get('moderation_state')->value); + } + $this->assertTrue($clone_moderation_state->isDefaultTranslation()); + $this->assertEquals('en', $clone_moderation_state->language()->getId()); + + // Create another node, published, translated and assert that upon cloning + // the node status is reset to 0 to match the fact that it's a draft. + $node = Node::create([ + 'type' => 'page', + 'title' => 'My fifth node', + 'moderation_state' => 'published', + ]); + $node->save(); + $translation = $node->addTranslation('fr', $node->toArray()); + $translation->save(); + $node = Node::load($node->id()); + $this->assertCount(2, $node->getTranslationLanguages()); + $this->assertTrue($node->getTranslation('en')->isPublished()); + $this->assertTrue($node->getTranslation('fr')->isPublished()); + $this->drupalGet(Url::fromUserInput('/entity_clone/node/' . $node->id())); + $this->submitForm([], t('Clone')); + + $nodes = \Drupal::entityTypeManager() + ->getStorage('node') + ->loadByProperties([ + 'title' => 'My fifth node - Cloned', + ]); + $clone = reset($nodes); + $this->assertInstanceOf(Node::class, $clone, 'Test node cloned found in database.'); + $this->assertCount(2, $clone->getTranslationLanguages()); + $this->assertFalse($clone->getTranslation('en')->isPublished()); + $this->assertFalse($clone->getTranslation('fr')->isPublished()); + + } + +} diff --git a/web/modules/entity_clone/tests/src/Functional/EntityCloneContentTest.php b/web/modules/entity_clone/tests/src/Functional/EntityCloneContentTest.php index 384e951458e163cc60ed248eb913ce4ae71fe430..85770f6fe9fe3f8d959e136a12ba9df93ec2cea7 100644 --- a/web/modules/entity_clone/tests/src/Functional/EntityCloneContentTest.php +++ b/web/modules/entity_clone/tests/src/Functional/EntityCloneContentTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\entity_clone\Functional; +use Drupal\Core\Url; +use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\Tests\field\Traits\EntityReferenceTestTrait; use Drupal\Tests\node\Functional\NodeTestBase; @@ -20,7 +22,7 @@ class EntityCloneContentTest extends NodeTestBase { * * @var array */ - public static $modules = ['entity_clone', 'block', 'node', 'datetime', 'taxonomy']; + public static $modules = ['entity_clone', 'block', 'node', 'datetime', 'taxonomy', 'content_translation', 'language']; /** * Theme to enable by default @@ -54,6 +56,9 @@ protected function setUp(): void { $this->adminUser = $this->drupalCreateUser($this->permissions); $this->drupalLogin($this->adminUser); + + ConfigurableLanguage::createFromLangcode('fr')->save(); + \Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE); } /** @@ -78,6 +83,9 @@ public function testContentEntityClone() { $this->assertInstanceOf(Node::class, $node, 'Test node cloned found in database.'); } + /** + * Test content reference config entity. + */ public function testContentReferenceConfigEntity() { $this->createEntityReferenceField('node', 'page', 'config_field_reference', 'Config field reference', 'taxonomy_vocabulary'); @@ -93,4 +101,64 @@ public function testContentReferenceConfigEntity() { $this->assertSession()->elementNotExists('css', '#edit-recursive-nodepageconfig-field-reference'); } + /** + * Test the cloned entity's created and changed dates. + * + * For entities that support these kinds of dates, both are reset to the + * current time. + */ + public function testCreatedAndChangedDate() { + // Create the original node. + $original_node_creation_date = new \DateTimeImmutable('1 year 1 month 1 day ago'); + $translation_creation_date = new \DateTimeImmutable('1 month 1 day ago'); + $original_node = Node::create([ + 'type' => 'page', + 'title' => 'Test', + 'created' => $original_node_creation_date->getTimestamp(), + 'changed' => $original_node_creation_date->getTimestamp(), + ]); + $original_node->addTranslation('fr', $original_node->toArray()); + // The translation was created and updated later. + $translation = $original_node->getTranslation('fr'); + $translation->setCreatedTime($translation_creation_date->getTimestamp()); + $translation->setChangedTime($translation_creation_date->getTimestamp()); + $original_node->save(); + + $original_node = Node::load($original_node->id()); + $this->assertEquals($original_node_creation_date->getTimestamp(), $original_node->getCreatedTime()); + $this->assertEquals($original_node_creation_date->getTimestamp(), $original_node->getChangedTime()); + $this->assertEquals($translation_creation_date->getTimestamp(), $original_node->getTranslation('fr')->getCreatedTime()); + $this->assertEquals($translation_creation_date->getTimestamp(), $original_node->getTranslation('fr')->getChangedTime()); + + // Clone the node. + $this->drupalPostForm('entity_clone/node/' . $original_node->id(), [], t('Clone')); + + // Find the cloned node. + $nodes = \Drupal::entityTypeManager() + ->getStorage('node') + ->loadByProperties([ + 'title' => sprintf('%s - Cloned', $original_node->label()), + ]); + $this->assertGreaterThanOrEqual(1, count($nodes)); + /** @var \Drupal\node\NodeInterface $cloned_node */ + $cloned_node = reset($nodes); + + // Validate the cloned node's created time is more recent than the original + // node. + $this->assertNotEquals($original_node->getCreatedTime(), $cloned_node->getCreatedTime()); + $this->assertGreaterThanOrEqual($original_node->getCreatedTime(), $cloned_node->getCreatedTime()); + + // Assert the changed time is equal to the newly created time since we + // cannot have a changed date before it. + $this->assertEquals($cloned_node->getCreatedTime(), $cloned_node->getChangedTime()); + + // Validate the translation created and updated dates. + $this->assertTrue($cloned_node->hasTranslation('fr')); + $translation = $cloned_node->getTranslation('fr'); + // The created and updated times should be the same between the original + // and the translation as both should be reset. + $this->assertEquals($cloned_node->getCreatedTime(), $translation->getCreatedTime()); + $this->assertEquals($cloned_node->getChangedTime(), $translation->getChangedTime()); + } + } diff --git a/web/modules/entity_clone/tests/src/Functional/EntityCloneEntityFormModeTest.php b/web/modules/entity_clone/tests/src/Functional/EntityCloneEntityFormModeTest.php index a310ab0cc47ffbcde8067883d866f81ba2ec5075..2e3acaa46b7821c4f18b2585437919d48fc5fc82 100644 --- a/web/modules/entity_clone/tests/src/Functional/EntityCloneEntityFormModeTest.php +++ b/web/modules/entity_clone/tests/src/Functional/EntityCloneEntityFormModeTest.php @@ -17,7 +17,7 @@ class EntityCloneEntityFormModeTest extends BrowserTestBase { * * @var array */ - public static $modules = ['entity_clone']; + public static $modules = ['entity_clone', 'field_ui']; /** * Theme to enable by default @@ -32,6 +32,7 @@ class EntityCloneEntityFormModeTest extends BrowserTestBase { */ protected $permissions = [ 'clone entity_form_mode entity', + 'administer display modes', ]; /** diff --git a/web/modules/entity_clone/tests/src/Functional/EntityCloneEntityViewModeTest.php b/web/modules/entity_clone/tests/src/Functional/EntityCloneEntityViewModeTest.php index a06b4e70268cdfd9799e0a69b166c7999b5de5f6..1a7faf198d5e88960ea753f11a423c1b75d88cc0 100644 --- a/web/modules/entity_clone/tests/src/Functional/EntityCloneEntityViewModeTest.php +++ b/web/modules/entity_clone/tests/src/Functional/EntityCloneEntityViewModeTest.php @@ -17,7 +17,7 @@ class EntityCloneEntityViewModeTest extends BrowserTestBase { * * @var array */ - public static $modules = ['entity_clone']; + public static $modules = ['entity_clone', 'field_ui']; /** * Theme to enable by default @@ -32,6 +32,7 @@ class EntityCloneEntityViewModeTest extends BrowserTestBase { */ protected $permissions = [ 'clone entity_view_mode entity', + 'administer display modes', ]; /** diff --git a/web/modules/entity_clone/tests/src/Functional/EntityCloneParagraphTest.php b/web/modules/entity_clone/tests/src/Functional/EntityCloneParagraphTest.php new file mode 100644 index 0000000000000000000000000000000000000000..aa870cefe39f73b426ac48b86a9f7c24464ad267 --- /dev/null +++ b/web/modules/entity_clone/tests/src/Functional/EntityCloneParagraphTest.php @@ -0,0 +1,107 @@ +<?php + +namespace Drupal\Tests\entity_clone\Functional; + +use Drupal\Tests\node\Functional\NodeTestBase; + +/** + * Create a content with a paragraph and test a clone. + * + * @group entity_clone + */ +class EntityCloneParagraphTest extends NodeTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['entity_clone', 'paragraphs_demo']; + + /** + * Theme to enable by default + * @var string + */ + protected $defaultTheme = 'classy'; + + /** + * Profile to install. + * + * @var string + */ + protected $profile = 'standard'; + + /** + * Permissions to grant admin user. + * + * @var array + */ + protected $permissions = [ + 'clone node entity', + 'bypass node access', + ]; + + /** + * A user with permission to bypass content access checks. + * + * @var \Drupal\user\UserInterface + */ + protected $adminUser; + + /** + * Sets the test up. + */ + protected function setUp(): void { + parent::setUp(); + + $this->adminUser = $this->drupalCreateUser($this->permissions); + $this->drupalLogin($this->adminUser); + } + + /** + * Tests cloning of paragraph entities. + */ + public function testParagraphClone() { + // Use node title from paragraphs_demo_install(). + $node_title = 'Welcome to the Paragraphs Demo module!'; + $nodes = \Drupal::entityTypeManager() + ->getStorage('node') + ->loadByProperties([ + 'title' => $node_title, + ]); + $node = reset($nodes); + + // Clone all paragraphs except the shared library paragraph. + $clone_options = [ + 'recursive[node.paragraphed_content_demo.field_paragraphs_demo][references][1][clone]' => 1, + 'recursive[node.paragraphed_content_demo.field_paragraphs_demo][references][2][clone]' => 1, + 'recursive[node.paragraphed_content_demo.field_paragraphs_demo][references][3][clone]' => 1, + 'recursive[node.paragraphed_content_demo.field_paragraphs_demo][references][5][clone]' => 1, + 'recursive[node.paragraphed_content_demo.field_paragraphs_demo][references][5][children][recursive][paragraph.nested_paragraph.field_paragraphs_demo][references][4][clone]' => 1, + ]; + + $this->drupalPostForm('entity_clone/node/' . $node->id(), $clone_options, t('Clone')); + + $clones = \Drupal::entityTypeManager() + ->getStorage('node') + ->loadByProperties([ + 'title' => $node_title . ' - Cloned', + ]); + $clone = reset($clones); + + $original_paragraph = $node->get('field_paragraphs_demo') + ->first() + ->get('entity') + ->getTarget() + ->getValue(); + + $cloned_paragraph = $clone->get('field_paragraphs_demo') + ->first() + ->get('entity') + ->getTarget() + ->getValue(); + + $this->assertNotEqual($original_paragraph->getParentEntity()->id(), $cloned_paragraph->getParentEntity()->id()); + } + +} diff --git a/web/modules/entity_clone/tests/src/Functional/EntityCloneShortcutSetTest.php b/web/modules/entity_clone/tests/src/Functional/EntityCloneShortcutSetTest.php index 017d2673c33182672bb29d067f78c9fc35e7f6fa..be26497cfc8181764b83cfc80df43176e6d33070 100644 --- a/web/modules/entity_clone/tests/src/Functional/EntityCloneShortcutSetTest.php +++ b/web/modules/entity_clone/tests/src/Functional/EntityCloneShortcutSetTest.php @@ -32,6 +32,7 @@ class EntityCloneShortcutSetTest extends BrowserTestBase { */ protected $permissions = [ 'clone shortcut_set entity', + 'administer shortcuts', ]; /** diff --git a/web/modules/entity_clone/tests/src/Functional/EntityCloneUserTest.php b/web/modules/entity_clone/tests/src/Functional/EntityCloneUserTest.php index 330ff04a0f9b36c9013aae23354af97bf941173d..08fa2df0a1a1adf2c8fd410e459d4dba87a76dee 100644 --- a/web/modules/entity_clone/tests/src/Functional/EntityCloneUserTest.php +++ b/web/modules/entity_clone/tests/src/Functional/EntityCloneUserTest.php @@ -32,6 +32,7 @@ class EntityCloneUserTest extends BrowserTestBase { */ protected $permissions = [ 'clone user entity', + 'administer users', ]; /** diff --git a/web/modules/entity_clone/tests/src/Functional/EntityCloneViewTest.php b/web/modules/entity_clone/tests/src/Functional/EntityCloneViewTest.php index be26c1bae360eca167bc7d180d47a7de884bc880..a2efac0fc949d0ae5ecfcdfa69c17d115d60c150 100644 --- a/web/modules/entity_clone/tests/src/Functional/EntityCloneViewTest.php +++ b/web/modules/entity_clone/tests/src/Functional/EntityCloneViewTest.php @@ -17,7 +17,7 @@ class EntityCloneViewTest extends BrowserTestBase { * * @var array */ - public static $modules = ['entity_clone', 'views']; + public static $modules = ['entity_clone', 'views', 'views_ui']; /** * Theme to enable by default @@ -32,6 +32,7 @@ class EntityCloneViewTest extends BrowserTestBase { */ protected $permissions = [ 'clone view entity', + 'administer views', ]; /** diff --git a/web/modules/entity_clone/tests/src/Kernel/EntityCloneAccessTest.php b/web/modules/entity_clone/tests/src/Kernel/EntityCloneAccessTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7e626c471839cbf41213e07fbbe66520293adb45 --- /dev/null +++ b/web/modules/entity_clone/tests/src/Kernel/EntityCloneAccessTest.php @@ -0,0 +1,102 @@ +<?php + +namespace Drupal\Tests\entity_clone\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; +use Drupal\Tests\user\Traits\UserCreationTrait; + +/** + * Tests entity clone access. + * + * @group entity_clone + */ +class EntityCloneAccessTest extends KernelTestBase { + + use UserCreationTrait; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'node', + 'field', + 'text', + 'user', + 'entity_clone', + 'system', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installSchema('system', ['sequences']); + $this->installSchema('node', ['node_access']); + + $this->installConfig([ + 'node', + 'user', + 'system', + 'entity_clone', + ]); + + // Call the user module install process that creates the anonymous user + // and user 1. + $this->container->get('module_handler')->loadInclude('user', 'install'); + user_install(); + + $node_type = NodeType::create([ + 'type' => 'page', + 'id' => 'page', + ]); + $node_type->save(); + } + + /** + * Tests that users need to have the correct permissions to clone an entity. + */ + public function testCloneAccess() { + $node = Node::create([ + 'type' => 'page', + 'title' => 'My node to clone', + 'status' => TRUE, + ]); + + $node->save(); + + $user_no_permissions = $this->createUser(['access content']); + $user_that_can_create = $this->createUser(['access content', 'create page content']); + $user_that_can_clone = $this->createUser(['access content', 'clone node entity']); + $user_that_can_do_both = $this->createUser(['access content', 'clone node entity', 'create page content']); + + $url = $node->toUrl('clone-form'); + + $access_control_handler = $this->container->get('entity_type.manager')->getAccessControlHandler('node'); + + // The user without permissions can view the content but cannot clone. + $this->assertTrue($access_control_handler->access($node, 'view', $user_no_permissions)); + $this->assertFalse($access_control_handler->access($node, 'clone', $user_no_permissions)); + $this->assertFalse($url->access($user_no_permissions)); + + // The user that can create content, cannot clone. + $this->assertTrue($access_control_handler->createAccess('page', $user_that_can_create)); + $this->assertFalse($access_control_handler->access($node, 'clone', $user_that_can_create)); + $this->assertFalse($url->access($user_that_can_create)); + + // The user that has clone permissions, but cannot create content, cannot + // clone. + $this->assertFalse($access_control_handler->createAccess('page', $user_that_can_clone)); + $this->assertFalse($access_control_handler->access($node, 'clone', $user_that_can_clone)); + $this->assertFalse($url->access($user_that_can_clone)); + + // The user that can do both, can clone. + $this->assertTrue($access_control_handler->createAccess('page', $user_that_can_do_both)); + $this->assertTrue($access_control_handler->access($node, 'clone', $user_that_can_do_both)); + $this->assertTrue($url->access($user_that_can_do_both)); + } +}