You are here

protected function EntityQueryAlter::doAlter in Group 8

Same name and namespace in other branches
  1. 2.0.x src/QueryAccess/EntityQueryAlter.php \Drupal\group\QueryAccess\EntityQueryAlter::doAlter()

Actually alters the select query for the given entity type.

Parameters

\Drupal\Core\Database\Query\SelectInterface $query: The select query.

\Drupal\Core\Entity\EntityTypeInterface $entity_type: The entity type.

string $operation: The query operation.

1 call to EntityQueryAlter::doAlter()
EntityQueryAlter::alter in src/QueryAccess/EntityQueryAlter.php
Alters the select query for the given entity type.

File

src/QueryAccess/EntityQueryAlter.php, line 164

Class

EntityQueryAlter
Defines a class for altering entity queries.

Namespace

Drupal\group\QueryAccess

Code

protected function doAlter(SelectInterface $query, EntityTypeInterface $entity_type, $operation) {
  $entity_type_id = $entity_type
    ->id();
  $storage = $this->entityTypeManager
    ->getStorage($entity_type_id);
  if (!$storage instanceof SqlContentEntityStorage) {
    return;
  }

  // Find all of the group content plugins that define access.
  $plugin_ids = $this->pluginManager
    ->getPluginIdsByEntityTypeAccess($entity_type_id);
  if (empty($plugin_ids)) {
    return;
  }

  // Load all of the group content types that define access.

  /** @var \Drupal\group\Entity\Storage\GroupContentTypeStorageInterface $gct_storage */
  $gct_storage = $this->entityTypeManager
    ->getStorage('group_content_type');
  $group_content_types = $gct_storage
    ->loadByContentPluginId($plugin_ids);

  // If any new group content entity is added using any of the retrieved
  // plugins, it might change access.
  $cache_tags = [];
  foreach ($plugin_ids as $plugin_id) {
    $cache_tags[] = "group_content_list:plugin:{$plugin_id}";
  }
  $this->cacheableMetadata
    ->addCacheTags($cache_tags);
  if (empty($group_content_types)) {

    // Because we add cache tags checking for new group content above, we can
    // simply bail out here without adding any group content type related
    // cache tags because a new group content type does not change the
    // permissions until a group content is created using said group content
    // type, at which point the cache tags above kick in.
    return;
  }

  // Find all group content types that have content for them.
  $group_content_type_ids_in_use = $this->database
    ->select('group_content_field_data', 'gc')
    ->fields('gc', [
    'type',
  ])
    ->condition('type', array_keys($group_content_types), 'IN')
    ->distinct()
    ->execute()
    ->fetchCol();
  if (empty($group_content_type_ids_in_use)) {
    return;
  }

  // @todo Remove these lines once we kill the bypass permission.
  // If the account can bypass group access, we do not alter the query at all.
  $this->cacheableMetadata
    ->addCacheContexts([
    'user.permissions',
  ]);
  if ($this->currentUser
    ->hasPermission('bypass group access')) {
    return;
  }

  // From this point onward, we know that there are grouped entities and that
  // we need to check access, so we can LEFT JOIN the necessary table.
  $base_table = $entity_type
    ->getBaseTable();
  $id_key = $entity_type
    ->getKey('id');

  // The base table is usually aliased, so let's try and find it.
  foreach ($query
    ->getTables() as $alias => $table) {
    if ($table['join type'] === NULL) {
      $base_table = $alias;
      break;
    }
  }
  $query
    ->leftJoin('group_content_field_data', 'gcfd', "{$base_table}.{$id_key}=gcfd.entity_id AND gcfd.type IN (:group_content_type_ids_in_use[])", [
    ':group_content_type_ids_in_use[]' => $group_content_type_ids_in_use,
  ]);
  $this->cacheableMetadata
    ->addCacheContexts([
    'user.group_permissions',
  ]);
  $calculated_permissions = $this->permissionCalculator
    ->calculatePermissions($this->currentUser);

  // We only check unpublished vs published for "view" right now. If we ever
  // start supporting other operations, we need to remove the "view" check.
  $check_published = $operation === 'view' && $entity_type
    ->entityClassImplements(EntityPublishedInterface::class);
  $owner_key = $entity_type
    ->getKey('owner');
  $published_key = $entity_type
    ->getKey('published');

  // Get some maps to use in the loops below so we save some milliseconds.
  $plugin_id_map = $this->pluginManager
    ->getPluginGroupContentTypeMap();
  $gct_to_gt_map = [];
  foreach ($group_content_types as $group_content_type_id => $group_content_type) {
    $gct_to_gt_map[$group_content_type_id] = $group_content_type
      ->getGroupTypeId();
  }
  $allowed_any_ids = $allowed_own_ids = $allowed_any_by_status_ids = $allowed_own_by_status_ids = $member_group_ids = [];
  foreach ($plugin_ids as $plugin_id) {

    // If the plugin is not installed, skip it.
    if (!isset($plugin_id_map[$plugin_id])) {
      continue;
    }

    // For backwards compatibility reasons, if the group content enabler
    // plugin used by the group content type does not specify a permission
    // provider, we do not alter the query for that group content type. In
    // 8.2.x all group content types will get a permission handler by
    // default, so this check can be safely removed then.
    if (!$this->pluginManager
      ->hasHandler($plugin_id, 'permission_provider')) {
      continue;
    }
    foreach ($plugin_id_map[$plugin_id] as $group_content_type_id) {

      // If the group content type has no content, skip it.
      if (!in_array($group_content_type_id, $group_content_type_ids_in_use)) {
        continue;
      }
      $handler = $this->pluginManager
        ->getPermissionProvider($plugin_id);
      $admin_permission = $handler
        ->getAdminPermission();
      $any_permission = $handler
        ->getPermission($operation, 'entity', 'any');
      $own_permission = $handler
        ->getPermission($operation, 'entity', 'own');
      if ($check_published) {
        $any_unpublished_permission = $handler
          ->getPermission("{$operation} unpublished", 'entity', 'any');
        $own_unpublished_permission = $handler
          ->getPermission("{$operation} unpublished", 'entity', 'own');
      }
      foreach ($calculated_permissions
        ->getItems() as $item) {

        // For groups, we need to get the group ID to add to the query.
        $identifier = 'INVALID';
        if ($item
          ->getScope() === CGPII::SCOPE_GROUP) {
          $identifier = $item
            ->getIdentifier();
          $member_group_ids[] = $identifier;
        }
        elseif ($item
          ->getScope() === CGPII::SCOPE_GROUP_TYPE) {
          if ($gct_to_gt_map[$group_content_type_id] !== $item
            ->getIdentifier()) {
            continue;
          }
          $identifier = $group_content_type_id;
        }
        if ($admin_permission !== FALSE && $item
          ->hasPermission($admin_permission)) {
          $allowed_any_ids[$item
            ->getScope()][] = $identifier;
        }
        elseif (!$check_published) {
          if ($any_permission !== FALSE && $item
            ->hasPermission($any_permission)) {
            $allowed_any_ids[$item
              ->getScope()][] = $identifier;
          }
          elseif ($own_permission !== FALSE && $item
            ->hasPermission($own_permission)) {
            $allowed_own_ids[$item
              ->getScope()][] = $identifier;
          }
        }
        else {
          if ($any_permission !== FALSE && $item
            ->hasPermission($any_permission)) {
            $allowed_any_by_status_ids[$item
              ->getScope()][1][] = $identifier;
          }
          elseif ($own_permission !== FALSE && $item
            ->hasPermission($own_permission)) {
            $allowed_own_by_status_ids[$item
              ->getScope()][1][] = $identifier;
          }
          if ($any_unpublished_permission !== FALSE && $item
            ->hasPermission($any_unpublished_permission)) {
            $allowed_any_by_status_ids[$item
              ->getScope()][0][] = $identifier;
          }
          elseif ($own_unpublished_permission !== FALSE && $item
            ->hasPermission($own_unpublished_permission)) {
            $allowed_own_by_status_ids[$item
              ->getScope()][0][] = $identifier;
          }
        }
      }
    }
  }

  // If no group type or group gave access, we deny access altogether.
  if (empty($allowed_any_ids) && empty($allowed_own_ids) && empty($allowed_any_by_status_ids) && empty($allowed_own_by_status_ids)) {
    $query
      ->isNull('gcfd.entity_id');
    return;
  }

  // From this point on, we know there is something that will allow access, so
  // we need to alter the query to check that entity_id is null or the group
  // access checks apply.
  $query
    ->condition($query
    ->orConditionGroup()
    ->isNull('gcfd.entity_id')
    ->condition($group_conditions = $query
    ->orConditionGroup()));

  // We might see multiple values in the $member_group_ids variable because we
  // looped over all calculated permissions multiple times.
  if (!empty($member_group_ids)) {
    $member_group_ids = array_unique($member_group_ids);
  }

  // Add the allowed group types to the query (if any).
  if (!empty($allowed_any_ids[CGPII::SCOPE_GROUP_TYPE])) {
    $sub_condition = $query
      ->andConditionGroup();
    $sub_condition
      ->condition('gcfd.type', array_unique($allowed_any_ids[CGPII::SCOPE_GROUP_TYPE]), 'IN');

    // If the user had memberships, we need to make sure they are excluded
    // from group type based matches as the memberships' permissions take
    // precedence.
    if (!empty($member_group_ids)) {
      $sub_condition
        ->condition('gcfd.gid', $member_group_ids, 'NOT IN');
    }
    $group_conditions
      ->condition($sub_condition);
  }

  // Add the memberships with access to the query (if any).
  if (!empty($allowed_any_ids[CGPII::SCOPE_GROUP])) {
    $group_conditions
      ->condition('gcfd.gid', array_unique($allowed_any_ids[CGPII::SCOPE_GROUP]), 'IN');
  }

  // In order to define query access for grouped entities and at the same time
  // leave the ungrouped alone, we need allow access to all entities that:
  // - Do not belong to a group.
  // - Belong to a group and to which:
  //   - The user has any access.
  //   - The user has owner access and is the owner of.
  //
  // In case the entity supports publishing, the last condition is swapped out
  // for the following two:
  // - The entity is published and:
  //   - The user has any access.
  //   - The user has owner access and is the owner of.
  // - The entity is unpublished and:
  //   - The user has any access.
  //   - The user has owner access and is the owner of.
  //
  // In any case, the first two conditions are always the same and have been
  // added above already.
  //
  // From this point we need to either find the entities the user can access
  // as the owner or the entities accessible as both the owner and non-owner
  // when the entity supports publishing.
  if (!$check_published) {

    // Nothing gave owner access so bail out entirely.
    if (empty($allowed_own_ids[CGPII::SCOPE_GROUP_TYPE]) && empty($allowed_own_ids[CGPII::SCOPE_GROUP])) {
      return;
    }
    $this->cacheableMetadata
      ->addCacheContexts([
      'user',
    ]);
    $data_table = $this
      ->ensureDataTable($base_table, $query, $entity_type);
    $group_conditions
      ->condition($query
      ->andConditionGroup()
      ->condition("{$data_table}.{$owner_key}", $this->currentUser
      ->id())
      ->condition($owner_group_conditions = $query
      ->orConditionGroup()));

    // Add the allowed owner group types to the query (if any).
    if (!empty($allowed_own_ids[CGPII::SCOPE_GROUP_TYPE])) {
      $sub_condition = $query
        ->andConditionGroup();
      $sub_condition
        ->condition('gcfd.type', array_unique($allowed_own_ids[CGPII::SCOPE_GROUP_TYPE]), 'IN');

      // If the user had memberships, we need to make sure they are excluded
      // from group type based matches as the memberships' permissions take
      // precedence.
      if (!empty($member_group_ids)) {
        $sub_condition
          ->condition('gcfd.gid', $member_group_ids, 'NOT IN');
      }
      $owner_group_conditions
        ->condition($sub_condition);
    }

    // Add the owner memberships with access to the query (if any).
    if (!empty($allowed_own_ids[CGPII::SCOPE_GROUP])) {
      $owner_group_conditions
        ->condition('gcfd.gid', array_unique($allowed_own_ids[CGPII::SCOPE_GROUP]), 'IN');
    }
  }
  else {
    foreach ([
      0,
      1,
    ] as $status) {

      // Nothing gave owner access so bail out entirely.
      if (empty($allowed_any_by_status_ids[CGPII::SCOPE_GROUP_TYPE][$status]) && empty($allowed_any_by_status_ids[CGPII::SCOPE_GROUP][$status]) && empty($allowed_own_by_status_ids[CGPII::SCOPE_GROUP_TYPE][$status]) && empty($allowed_own_by_status_ids[CGPII::SCOPE_GROUP][$status])) {
        continue;
      }
      $data_table = $this
        ->ensureDataTable($base_table, $query, $entity_type);
      $group_conditions
        ->condition($query
        ->andConditionGroup()
        ->condition("{$data_table}.{$published_key}", $status)
        ->condition($status_group_conditions = $query
        ->orConditionGroup()));

      // Add the allowed group types to the query (if any).
      if (!empty($allowed_any_by_status_ids[CGPII::SCOPE_GROUP_TYPE][$status])) {
        $sub_condition = $query
          ->andConditionGroup();
        $sub_condition
          ->condition('gcfd.type', array_unique($allowed_any_by_status_ids[CGPII::SCOPE_GROUP_TYPE][$status]), 'IN');

        // If the user had memberships, we need to make sure they are excluded
        // from group type based matches as the memberships' permissions take
        // precedence.
        if (!empty($member_group_ids)) {
          $sub_condition
            ->condition('gcfd.gid', $member_group_ids, 'NOT IN');
        }
        $status_group_conditions
          ->condition($sub_condition);
      }

      // Add the memberships with access to the query (if any).
      if (!empty($allowed_any_by_status_ids[CGPII::SCOPE_GROUP][$status])) {
        $status_group_conditions
          ->condition('gcfd.gid', array_unique($allowed_any_by_status_ids[CGPII::SCOPE_GROUP][$status]), 'IN');
      }

      // Nothing gave owner access so try the next publication status.
      if (empty($allowed_own_by_status_ids[CGPII::SCOPE_GROUP_TYPE][$status]) && empty($allowed_own_by_status_ids[CGPII::SCOPE_GROUP][$status])) {
        continue;
      }
      $this->cacheableMetadata
        ->addCacheContexts([
        'user',
      ]);
      $status_group_conditions
        ->condition($query
        ->andConditionGroup()
        ->condition("{$data_table}.{$owner_key}", $this->currentUser
        ->id())
        ->condition($status_owner_group_conditions = $query
        ->orConditionGroup()));

      // Add the allowed owner group types to the query (if any).
      if (!empty($allowed_own_by_status_ids[CGPII::SCOPE_GROUP_TYPE][$status])) {
        $sub_condition = $query
          ->andConditionGroup();
        $sub_condition
          ->condition('gcfd.type', array_unique($allowed_own_by_status_ids[CGPII::SCOPE_GROUP_TYPE][$status]), 'IN');

        // If the user had memberships, we need to make sure they are excluded
        // from group type based matches as the memberships' permissions take
        // precedence.
        if (!empty($member_group_ids)) {
          $sub_condition
            ->condition('gcfd.gid', $member_group_ids, 'NOT IN');
        }
        $status_owner_group_conditions
          ->condition($sub_condition);
      }

      // Add the owner memberships with access to the query (if any).
      if (!empty($allowed_own_by_status_ids[CGPII::SCOPE_GROUP][$status])) {
        $status_owner_group_conditions
          ->condition('gcfd.gid', array_unique($allowed_own_by_status_ids[CGPII::SCOPE_GROUP][$status]), 'IN');
      }
    }
  }
}