access_policy-1.0.x-dev/access_policy.module
access_policy.module
<?php /** * @file * Hook implementations for the access_policy module. */ use Drupal\access_policy\AccessPolicyEntityAccessControlHandler; use Drupal\access_policy\AccessPolicyQueryAlter; use Drupal\access_policy\EntityFieldAccessPolicyData; use Drupal\access_policy\EntityOperations; use Drupal\access_policy\EntityTypeInfo; use Drupal\access_policy\FieldMatchAccessPolicyData; use Drupal\access_policy\NodeFormAlter; use Drupal\access_policy\UserFieldAccessPolicyData; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Entity\EntityFormInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\field\FieldConfigInterface; use Drupal\node\NodeForm; use Drupal\views\Plugin\views\cache\CachePluginBase; use Drupal\views\Plugin\views\query\QueryPluginBase; use Drupal\views\Plugin\views\query\Sql; use Drupal\views\ViewExecutable; use Drupal\views\Views; /** * Implements hook_entity_access(). */ function access_policy_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { return \Drupal::service('class_resolver') ->getInstanceFromDefinition(AccessPolicyEntityAccessControlHandler::class) ->access($entity, $account, $operation); } /** * Implements hook_module_implements_alter(). */ function access_policy_module_implements_alter(&$implementations, $hook) { // Make sure that access_policy query alters happen last. if (in_array($hook, ['views_query_alter'], TRUE)) { $access_policy = $implementations['access_policy']; unset($implementations['access_policy']); $implementations['access_policy'] = $access_policy; } } /** * Implements hook_query_alter(). */ function access_policy_query_alter(AlterableInterface $query) { $accessPolicyInfo = \Drupal::service('access_policy.information'); $has_tag = FALSE; // If the entity type is provided then use that so we don't need to do a // lookup. $entity_type_id = $query->getMetaData('entity_type'); if ($entity_type_id) { $entity_type_ids[] = $entity_type_id; } // Otherwise check all the currently supported entity types. else { $entity_types = $accessPolicyInfo->getAllEnabledEntityTypes(); $entity_type_ids = array_map(function ($entity_type) { return $entity_type->id(); }, $entity_types); } foreach ($entity_type_ids as $entity_type_id) { if ($query->hasTag($entity_type_id . '_access')) { $has_tag = TRUE; break; } } if ($has_tag) { $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); $is_access_controlled = $accessPolicyInfo->isAccessControlledEntityType($entity_type); if ($is_access_controlled) { \Drupal::service('class_resolver') ->getInstanceFromDefinition(AccessPolicyQueryAlter::class) ->queryAlter($query, $entity_type); } } } /** * Implements hook_views_query_alter(). */ function access_policy_views_query_alter(ViewExecutable $view, QueryPluginBase $query) { if ($query instanceof Sql) { $table_info = $query->getEntityTableInfo(); $base_table = reset($table_info); if (empty($base_table['entity_type']) || $base_table['relationship_id'] != 'none') { return; } // Add the entity type to the query so that it's easier to determine when // to run the query alter. $entity_type_id = $base_table['entity_type']; $query->build($view); $view->built = TRUE; // Add metadata to the DB query. $query = $view->build_info['query']; $query->addMetaData('entity_type', $entity_type_id); // Determine whether the access tag is missing from the view. // Sometimes a view will be missing an access tag. For example, it might not // have any applicable filters that rely on access checks. // If access policy is enabled then we always check access on views. $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); $accessPolicyInfo = \Drupal::service('access_policy.information'); if (!$query->hasTag($entity_type_id . '_access') && $accessPolicyInfo->isAccessControlledEntityType($entity_type)) { $query->addTag($entity_type_id . '_access'); } } } /** * Implements hook_query_entity_reference_alter(). */ function access_policy_query_entity_reference_alter(AlterableInterface $query) { $entity_type_id = $query->getMetaData('entity_type'); if ($query instanceof SelectInterface && $query->hasTag($entity_type_id . '_access')) { $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); $is_access_controlled = \Drupal::service('access_policy.information')->isAccessControlledEntityType($entity_type); if ($is_access_controlled) { return \Drupal::service('class_resolver') ->getInstanceFromDefinition(AccessPolicyQueryAlter::class) ->queryAlter($query, $entity_type); } } } /** * Implements hook_entity_type_alter(). */ function access_policy_entity_type_alter(array &$entity_types) { return \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityTypeInfo::class) ->entityTypeAlter($entity_types); } /** * Implements hook_entity_base_field_info(). */ function access_policy_entity_base_field_info(EntityTypeInterface $entity_type) { return \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityTypeInfo::class) ->entityBaseFieldInfo($entity_type); } /** * Implements hook_entity_operation_alter(). */ function access_policy_entity_operation_alter(array &$operations, EntityInterface $entity) { return \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityOperations::class) ->entityOperationAlter($operations, $entity); } /** * Implements hook_entity_presave(). */ function access_policy_entity_presave(EntityInterface $entity) { return \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityOperations::class) ->entityPresave($entity); } /** * Implements hook_ENTITY_TYPE_presave(). */ function access_policy_access_policy_presave(EntityInterface $entity) { return \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityOperations::class) ->accessPolicyPresave($entity); } /** * Implements hook_form_alter(). */ function access_policy_form_alter(&$form, FormStateInterface $form_state, $form_id) { if ($form_state->getFormObject() instanceof EntityFormInterface) { \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityOperations::class) ->entityFormAlter($form, $form_state, $form_id); } if ($form_state->getFormObject() instanceof NodeForm) { \Drupal::service('class_resolver') ->getInstanceFromDefinition(NodeFormAlter::class) ->nodeFormAlter($form, $form_state, $form_id); } } /** * Implements hook_ENTITY_TYPE_update(). */ function access_policy_access_policy_update(EntityInterface $entity) { // Clear field cache so extra field is added or removed. \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); // Clear the listing pages. $cache_tags = $entity->getTargetEntityType()->getListCacheTags(); \Drupal::service('cache_tags.invalidator')->invalidateTags($cache_tags); // Clear the views data cache so the extra field is available in views. if (\Drupal::moduleHandler()->moduleExists('views')) { Views::viewsData()->clear(); } } /** * Implements hook_ENTITY_TYPE_insert(). */ function access_policy_access_policy_insert(EntityInterface $entity) { $existing_policies = \Drupal::entityTypeManager()->getStorage('access_policy') ->loadByProperties(['target_entity_type_id' => $entity->getTargetEntityTypeId()]); // If this is the first policy for a given entity type then clear the // routes and register the access policy data. if (count($existing_policies) == 1) { \Drupal::service('router.builder')->setRebuildNeeded(); \Drupal::service('access_policy.access_policy_data')->clear(); } // Ensure that the access policy field is installed. _access_policy_install_access_policy_field($entity->getTargetEntityTypeId()); // Clear field cache so extra field is added or removed. \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); // Clear the views data cache so the extra field is available in views. if (\Drupal::moduleHandler()->moduleExists('views')) { Views::viewsData()->clear(); } \Drupal::service('access_policy.information')->resetCache(); } /** * Install the access policy field if it is missing. * * In rare cases the access_policy field might not be properly installed. This * can happen if an entity becomes compatible with Access policy after that * entity was originally installed. For example, perhaps a contrib module adds * bundle support to an entity that did not have it before. * * @param string $entity_type_id * The entity type id. * * @see \Drupal\access_policy\EntityTypeInfo::entityBaseFieldInfo() */ function _access_policy_install_access_policy_field($entity_type_id) { $definition_update_manager = \Drupal::entityDefinitionUpdateManager(); $exists = $definition_update_manager->getFieldStorageDefinition('access_policy', $entity_type_id); if (!$exists) { $field_manager = \Drupal::service('entity_field.manager'); $schema_repository = \Drupal::service('entity.last_installed_schema.repository'); $field_manager->useCaches(FALSE); $storage_definitions = $field_manager->getFieldStorageDefinitions($entity_type_id); $field_manager->useCaches(TRUE); $installed_storage_definitions = $schema_repository->getLastInstalledFieldStorageDefinitions($entity_type_id); foreach (array_diff_key($storage_definitions, $installed_storage_definitions) as $storage_definition) { if ($storage_definition->getProvider() == 'access_policy') { $definition_update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type_id, 'access_policy', $storage_definition); } } } } /** * Implements hook_entity_insert(). */ function access_policy_entity_insert(EntityInterface $entity) { return \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityOperations::class) ->entityInsert($entity); } /** * Implements hook_entity_update(). */ function access_policy_entity_update(EntityInterface $entity) { return \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityOperations::class) ->entityUpdate($entity); } /** * Implements hook_entity_field_access(). */ function access_policy_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface|null $items = NULL) { if ($items && $items->getEntity() instanceof AccountInterface) { return \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityOperations::class) ->userFieldAccess($operation, $field_definition, $account, $items); } return \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityOperations::class) ->entityFieldAccess($operation, $field_definition, $account, $items); } /** * Implements hook_field_widget_complete_form_alter(). */ function access_policy_field_widget_complete_form_alter(&$elements, FormStateInterface $form_state, $context) { return \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityOperations::class) ->fieldWidgetCompleteFormAlter($elements, $form_state, $context); } /** * Implements hook_field_config_insert(). */ function access_policy_field_config_insert(FieldConfigInterface $field_config) { // When a new field is added, clear the access policy data definitions. \Drupal::service('access_policy.access_policy_data')->clear(); } /** * Implements hook_access_policy_data(). */ function access_policy_access_policy_data() { $data = []; // Define the broken access rule handler. $data['access_rule']['broken'] = [ 'label' => t('Broken/missing handler'), 'description' => t('The handler is broken or missing.'), 'plugin_id' => 'broken', 'settings' => [ 'query' => FALSE, ], 'selection_rule' => [ 'plugin_id' => 'broken', ], ]; $data['global']['weekday_range'] = [ 'label' => t('Weekday range'), 'description' => t('Grant access on certain days or hours of the week.'), 'plugin_id' => 'weekday_range', ]; $entity_data = \Drupal::service('class_resolver') ->getInstanceFromDefinition(EntityFieldAccessPolicyData::class) ->accessPolicyData(); $user_data = \Drupal::service('class_resolver') ->getInstanceFromDefinition(UserFieldAccessPolicyData::class) ->accessPolicyData(); $matching_data = \Drupal::service('class_resolver') ->getInstanceFromDefinition(FieldMatchAccessPolicyData::class) ->accessPolicyData(); return NestedArray::mergeDeepArray([ $data, $entity_data, $user_data, $matching_data, ], TRUE); } /** * Implements hook_views_post_render(). */ function access_policy_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) { $accessPolicyInfo = \Drupal::service('access_policy.information'); $entity_type = $view->getBaseEntityType(); if ($entity_type instanceof EntityTypeInterface && $accessPolicyInfo->isAccessControlledEntityType($entity_type)) { $view->addCacheContext('user'); } } /** * Implements hook_preprocess_HOOK() for dropbutton operations. * * This addresses a core bug where the operations links do not always have * proper cacheability support, causing the wrong dropbuttons to appear. * * @see https://www.drupal.org/project/drupal/issues/2473873 */ function access_policy_preprocess_links__dropbutton__operations(&$variables) { if (!isset($variables['#cache']['contexts']) || !in_array('user', $variables['#cache']['contexts'])) { $variables['#cache']['contexts'][] = 'user'; } }