Add support for the Trash module

Created on 12 February 2024, 10 months ago
Updated 2 April 2024, 9 months ago

Problem/Motivation

Both this module and the Trash module overwrite the node access control handler, and the Trash handler seems to get priority, causing part of our access checks to not be executed.

Steps to reproduce

Install and configure both the Node Singles and Trash modules.

Proposed resolution

Add an extra access control handler extending Drupal\trash\EntityHandler\TrashNodeAccessControlHandler and use that one if the Trash module is installed.

🐛 Bug report
Status

Fixed

Version

3.4

Component

Code

Created by

🇧🇪Belgium dieterholvoet Brussels

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @dieterholvoet
  • Merge request !15Add TrashSingleNodeAccessControlHandler → (Closed) created by dieterholvoet
  • Status changed to Needs review 10 months ago
  • 🇧🇪Belgium Robin.Houtevelts

    This might be a good moment to ask this:
    Do we really need to define our own AccessHandler?

    Should we use access hooks instead so we don't have to do this for each module that also wants to define an AccessHandler?

  • 🇧🇪Belgium dieterholvoet Brussels

    Is this a BC change?

    I would say no, that sounds like an edge case we could document in the release notes, but it shouldn't stop us from doing this. We could move it right after the Trash module instead of to the back of the list, that could limit any possible issues, but it isn't a guarantee. I don't really see another way around this, but let me know if you do.

    Is that still going to work without modifications?

    No.

    Should we use access hooks instead?

    We can't. The problem is that Drupal\node\NodeAccessControlHandler has hardcoded checks for the bypass node access and the access content permissions: if the first one is present or the second one is missing, the access hooks aren't even invoked. That would mean that the access checks aren't executed for those users, or user 1.

  • 🇧🇪Belgium Robin.Houtevelts

    I see. Thanks for clarifying!

    let me know if you do.

    No, not without being fonky.
    Ideally the AccessHandler would be an actual service we could decorate.
    But something like this might make sure we can decorate the previously defined handler too.

    But it might be a bit too fonky..

    function node_singles_entity_type_alter(array &$entityTypes): void {
      \Drupal::getContainer()->get('state')
        ->set(
            SingleNodeAccessControlHandler::STATE_DECORATED_CLASS_KEY,
            $entityTypes['node']->getHandlerClass('access')
         )
    
      $entityTypes['node']->setHandlerClass('access', SingleNodeAccessControlHandler::class);
      $entityTypes['node_type']->setFormClass('delete', SingleNodeTypeDeleteConfirm::class);
    }
    
    
    use Drupal\Core\Entity\EntityAccessControlHandlerInterface;
    use Drupal\Core\Entity\EntityHandlerInterface;
    use Drupal\Core\Entity\EntityInterface;
    use Drupal\Core\Entity\EntityTypeInterface;
    use Drupal\Core\Entity\EntityTypeManagerInterface;
    use Drupal\Core\Extension\ModuleHandlerInterface;
    use Drupal\Core\Field\FieldDefinitionInterface;
    use Drupal\Core\Field\FieldItemListInterface;
    use Drupal\Core\Session\AccountInterface;
    use Drupal\Core\State\StateInterface;
    use Drupal\node\NodeAccessControlHandler;
    use Drupal\node\NodeAccessControlHandlerInterface;
    use Symfony\Component\DependencyInjection\ContainerInterface;
    
    class SingleNodeAccessControlHandler implements NodeAccessControlHandlerInterface, EntityAccessControlHandlerInterface, EntityHandlerInterface
    {
    
        protected EntityTypeInterface $entityType;
        protected EntityTypeManagerInterface $entityTypeManager;
        protected StateInterface $state;
        protected NodeAccessControlHandlerInterface&EntityAccessControlHandlerInterface $decorated;
    
        public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type)
        {
            $self = new self();
            $self->entityType = $entity_type;
            $self->entityTypeManager = $container->get('entity_type.manager');
            $self->state = $container->get('state');
    
            return $self;
        }
    
        /**
         * {@inheritdoc}
         */
        public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE)
        {
            return $this->decorated()->access($entity, $operation, $account, $return_as_object);
        }
    
        /**
         * {@inheritdoc}
         */
        public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE)
        {
            return $this->decorated()->createAccess($entity_bundle, $account, $context, $return_as_object);
        }
    
        /**
         * {@inheritdoc}
         */
        public function resetCache()
        {
            $this->decorated()->resetCache();
        }
    
        /**
         * {@inheritdoc}
         */
        public function setModuleHandler(ModuleHandlerInterface $module_handler)
        {
            $this->decorated()->setModuleHandler($module_handler);
            return $this;
        }
    
        /**
         * {@inheritdoc}
         */
        public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL, $return_as_object = FALSE)
        {
            return $this->decorated()->fieldAccess($operation, $field_definition, $account, $items, $return_as_object);
        }
    
        /**
         * {@inheritdoc}
         */
        public function acquireGrants(NodeInterface $node)
        {
            return $this->decorated()->acquireGrants($node);
        }
    
        /**
         * {@inheritdoc}
         */
        public function writeDefaultGrant()
        {
            $this->decorated()->writeDefaultGrant();
        }
    
        /**
         * {@inheritdoc}
         */
        public function deleteGrants()
        {
            $this->decorated()->deleteGrants();
        }
    
        /**
         * {@inheritdoc}
         */
        public function countGrants()
        {
            return $this->decorated()->countGrants();
        }
    
        /**
         * {@inheritdoc}
         */
        public function checkAllGrants(AccountInterface $account)
        {
            return $this->decorated()->checkAllGrants($account);
        }
    
        private function decorated(): NodeAccessControlHandlerInterface&EntityAccessControlHandlerInterface
        {
            if (isset($this->decorated)) {
                return $this->decorated;
            }
            $originalAccessHandlerClass = $this->state->get(self::STATE_DECORATED_CLASS_KEY, NodeAccessControlHandler::class);
            $handler = $this->entityTypeManager->createHandlerInstance($originalAccessHandlerClass, $this->entityType);
    
            if (!$handler instanceof NodeAccessControlHandlerInterface) {
                throw new \InvalidArgumentException('The decorated access control handler must implement NodeAccessControlHandlerInterface.');
            }
            if (!$handler instanceof EntityAccessControlHandlerInterface) {
                throw new \InvalidArgumentException('The decorated access control handler must implement EntityAccessControlHandlerInterface.');
            }
    
            return $handler;
        }
    }
    
  • 🇧🇪Belgium dieterholvoet Brussels

    I just tried it out and this would work, but only if all contrib modules overriding the node access handler implement it this way. trash_entity_type_alter() is executed after node_singles_entity_type_alter(), so without a change in the Trash module this won't work. I will start a new branch on the issue fork with this implementation anyway.

  • Merge request !16Decorate the node access control handler → (Merged) created by dieterholvoet
  • 🇧🇪Belgium Robin.Houtevelts

    Isn't that fixed by the node_singles_module_implements_alter added in this MR?
    Then we decorate whatever was final.

  • 🇧🇪Belgium dieterholvoet Brussels

    Fixed!

  • 🇧🇪Belgium Robin.Houtevelts

    Cool!
    I'm actually planned to update one of our projects to 10.2 this week.
    That project currently has a module whose HOOK_entity_type_alter appears to run after node_singles too -just like Trash- but *doesn't* extend/wrap this AccessHandler.

    So I think I can test this exact use case.
    I'll test this once I get to 10.2 somewhere this or next week, and let you know here.

  • Status changed to RTBC 9 months ago
  • 🇧🇪Belgium Robin.Houtevelts

    Works for me. Changing to RTBC.

  • Pipeline finished with Skipped
    9 months ago
    #123416
  • Status changed to Fixed 9 months ago
  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.71.5 2024