Commit 3b3a89cc authored by Patrick Fey's avatar Patrick Fey Committed by root@s112-106.werk21system.de
Browse files

Issue #3110558 by FeyP, technoveltyco, el1_1el, shortspoken, dunx: Add a select terms permission

parent d9a941c7
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -59,6 +59,8 @@ To access the vocabulary overview page for a vocabulary, users must have permiss
* create, edit, delete or reorder terms in that vocabulary in addition to the Taxonomy module's permission to "Access the taxonomy vocabulary overview page".
* to "Administer vocabularies and terms".

The module will replace the default entity reference selection plugin for Taxonomy Terms with an extended version that checks for a separate per-vocabulary permission to "select" terms. Note: Users with permission to select terms will be able to see the term labels of those terms in the results of the autocomplete widget, even if they don't have permission to "view" those terms. Only Entity Reference fields referencing Taxonomy terms using the "Default" reference method will honor this permission.


MAINTAINERS
-----------
+20 −0
Original line number Diff line number Diff line
@@ -50,6 +50,14 @@ class TaxonomyAccessFixPermissions implements ContainerInjectionInterface {
      'view any unpublished term' => $this->t('View any unpublished term'),
      'view any term name' => $this->t('View any published term name'),
      'view any unpublished term name' => $this->t('View any unpublished term name'),
      'select any term' => [
        'title' => $this->t('Select any published term'),
        'description' => $this->t('Select published terms for Entity Reference fields referencing Taxonomy terms in any vocabulary using the "Default" reference method.'),
      ],
      'select any unpublished term' => [
        'title' => $this->t('Select any unpublished term'),
        'description' => $this->t('Select unpublished terms for Entity Reference fields referencing Taxonomy terms in any vocabulary using the "Default" reference method.'),
      ],
    ];

    $vocabularies = $this->entityTypeManager->getStorage('taxonomy_vocabulary')->loadMultiple();
@@ -80,6 +88,18 @@ class TaxonomyAccessFixPermissions implements ContainerInjectionInterface {
          '%vocabulary' => $vocabulary->label(),
        ]),
      ];
      $permissions['select terms in ' . $vocabulary->id()] = [
        'title' => $this->t('Select published terms in %vocabulary', [
          '%vocabulary' => $vocabulary->label(),
        ]),
        'description' => $this->t('Select published terms for Entity Reference fields referencing Taxonomy terms in the specified vocabulary using the "Default" reference method.'),
      ];
      $permissions['select unpublished terms in ' . $vocabulary->id()] = [
        'title' => $this->t('Select unpublished terms in %vocabulary', [
          '%vocabulary' => $vocabulary->label(),
        ]),
        'description' => $this->t('Select unpublished terms for Entity Reference fields referencing Taxonomy terms in the specified vocabulary using the "Default" reference method.'),
      ];
    }

    return $permissions;
+6 −6
Original line number Diff line number Diff line
@@ -23,7 +23,7 @@ class TermAccessControlHandler extends OriginalTermAccessControlHandler {
   * {@inheritdoc}
   */
  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
    if (!in_array($operation, ['view', 'view label'], TRUE)) {
    if (!in_array($operation, ['view', 'view label', 'select'], TRUE)) {
      return parent::checkAccess($entity, $operation, $account);
    }
    if ($account->hasPermission('administer taxonomy')) {
@@ -68,13 +68,13 @@ class TermAccessControlHandler extends OriginalTermAccessControlHandler {
   */
  protected function getPermissions(EntityInterface $entity, string $operation): array {
    /** @var \Drupal\taxonomy\TermInterface $entity */
    if ($operation === 'view') {
    if (in_array($operation, ['view', 'select'], TRUE)) {
      return $entity->isPublished() ? [
        "view terms in {$entity->bundle()}",
        'view any term',
        "{$operation} terms in {$entity->bundle()}",
        "{$operation} any term",
      ] : [
        "view unpublished terms in {$entity->bundle()}",
        'view any unpublished term',
        "{$operation} unpublished terms in {$entity->bundle()}",
        "{$operation} any unpublished term",
      ];
    }
    elseif ($operation === 'view label') {

src/TermSelection.php

0 → 100644
+225 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\taxonomy_access_fix;

use Drupal\Component\Utility\Html;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\Plugin\EntityReferenceSelection\TermSelection as OriginalTermSelection;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Extends the original term selection plugin to check our permissions.
 */
class TermSelection extends OriginalTermSelection {

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $connection;

  /**
   * Constructs a new TermSelection object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler service.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Database\Connection $connection
   *   The database connection.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   The entity field manager.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
   *   The entity type bundle info service.
   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
   *   The entity repository.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user, Connection $connection, EntityFieldManagerInterface $entity_field_manager = NULL, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, EntityRepositoryInterface $entity_repository = NULL) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $module_handler, $current_user, $entity_field_manager, $entity_type_bundle_info, $entity_repository);
    $this->connection = $connection;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_type.manager'),
      $container->get('module_handler'),
      $container->get('current_user'),
      $container->get('database'),
      $container->get('entity_field.manager'),
      $container->get('entity_type.bundle.info'),
      $container->get('entity.repository')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
    if ($match || $limit) {
      return parent::getReferenceableEntities($match, $match_operator, $limit);
    }

    $options = [];

    $bundles = $this->getConfiguredBundles();

    $unpublished_terms = [];
    foreach ($bundles as $bundle) {
      if ($vocabulary = Vocabulary::load($bundle)) {
        /** @var \Drupal\taxonomy\TermInterface[] $terms */
        if ($terms = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vocabulary->id(), 0, NULL, TRUE)) {
          foreach ($terms as $term) {
            if (!$term->access('select') || in_array($term->parent->target_id, $unpublished_terms)) {
              $unpublished_terms[] = $term->id();
              continue;
            }
            $options[$vocabulary->id()][$term->id()] = str_repeat('-', $term->depth) . Html::escape($this->entityRepository->getTranslationFromContext($term)->label());
          }
        }
      }
    }

    return $options;
  }

  /**
   * Gets configured bundle names for this term selection.
   *
   * @return string[]
   *   Configured bundle names or all bundle names, if none configured.
   */
  protected function getConfiguredBundles(): array {
    $bundles = $this->entityTypeBundleInfo->getBundleInfo('taxonomy_term');
    return $this->getConfiguration()['target_bundles'] ?: array_keys($bundles);
  }

  /**
   * {@inheritdoc}
   */
  public function entityQueryAlter(SelectInterface $query) {
    $conditions = $query->conditions();

    // If the query has a condition for the "status" field, this user doesn't
    // have permission to "administer taxonomy". We need to remove that
    // condition and add our own conditions based on our own permissions.
    $remove_index = -1;
    $status_table = $this
      ->entityTypeManager
      ->getStorage('taxonomy_term')
      ->getTableMapping()
      ->getFieldTableName('status');
    $status_field = $status_table . '.status';
    foreach ($conditions as $index => $condition) {
      if (!is_array($condition)) {
        continue;
      }
      if ($condition['field'] === $status_field && $condition['value'] === 1 && $condition['operator'] === '=') {
        $remove_index = $index;
        break;
      }
    }
    if ($remove_index < 0) {
      // This user has permission to "administer taxonomy".
      return;
    }
    unset($query->conditions()[$remove_index]);

    $hasAnyPublished = $this->currentUser->hasPermission('select any term');
    $hasAnyUnpublished = $this->currentUser->hasPermission('select any unpublished term');
    if ($hasAnyPublished && $hasAnyUnpublished) {
      // This user has access to select any term.
      return;
    }

    // Prepare new per-vocabulary conditions.
    $or = $this->connection
      ->condition('OR');

    $bundle_table = $this
      ->entityTypeManager
      ->getStorage('taxonomy_term')
      ->getTableMapping()
      ->getFieldTableName('vid');
    $bundle_field = $status_table . '.vid';
    $bundles = $this->getConfiguredBundles();
    foreach ($bundles as $bundle) {
      $hasPublished = $hasAnyPublished || $this->currentUser->hasPermission("select terms in {$bundle}");
      $hasUnpublished = $hasAnyUnpublished || $this->currentUser->hasPermission("select unpublished terms in {$bundle}");
      if (!$hasPublished && !$hasUnpublished) {
        continue;
      }
      if ($hasPublished && $hasUnpublished) {
        $and = $this->connection->condition('AND');
        $and->condition($bundle_field, $bundle, '=');
        $or->condition($and);
        continue;
      }
      if ($hasPublished) {
        $and = $this->connection->condition('AND');
        $and->condition($bundle_field, $bundle, '=');
        $and->condition($status_field, 1, '=');
        $or->condition($and);
        continue;
      }
      if ($hasUnpublished) {
        $and = $this->connection->condition('AND');
        $and->condition($bundle_field, $bundle, '=');
        $and->condition($status_field, 0, '=');
        $or->condition($and);
        continue;
      }
    }

    if (count($or) === 0) {
      // No per-vocabulary conditions have been added. This user has no access
      // at all. Add a condition that won't return any results.
      $and = $this->connection->condition('AND');
      $and->condition($bundle_field, '', '=');
      $or->condition($and);
    }

    // Add per-vocabulary conditions to query.
    $query->condition($or);
  }

  /**
   * {@inheritdoc}
   */
  public function validateReferenceableNewEntities(array $entities) {
    $grandparent = new \ReflectionMethod(get_parent_class(get_parent_class($this)), 'validateReferenceableNewEntities');
    $entities = $grandparent->invoke($this, $entities);

    if (!$this->currentUser->hasPermission('administer taxonomy')) {
      $entities = array_filter($entities, function ($term) {
        /** @var \Drupal\taxonomy\TermInterface $term */
        return $term->access('select');
      });
    }

    return $entities;
  }

}
+1 −1
Original line number Diff line number Diff line
name: Taxonomy Access Fix
type: module
description: Fixes the crooked access checks for Taxonomy pages.
core_version_requirement: ^8.8 || ^9
core_version_requirement: ^9.3
package: Taxonomy

dependencies:
Loading