diff --git a/modules/gdpr_fields/config/schema/gdpr_field.schema.yml b/modules/gdpr_fields/config/schema/gdpr_field.schema.yml new file mode 100644 index 0000000..fca48dd --- /dev/null +++ b/modules/gdpr_fields/config/schema/gdpr_field.schema.yml @@ -0,0 +1,10 @@ +field.field.*.*.*.third_party.gdpr_fields: + type: mapping + label: 'GDPR field' + mapping: + gdpr_fields_rta: + type: string + label: 'Whether this field is user sensitive or not.' + gdpr_fields_rtf: + type: string + label: 'Whether this field is user sensitive or not.' \ No newline at end of file diff --git a/modules/gdpr_fields/gdpr_fields.info.yml b/modules/gdpr_fields/gdpr_fields.info.yml new file mode 100644 index 0000000..8b9bdd7 --- /dev/null +++ b/modules/gdpr_fields/gdpr_fields.info.yml @@ -0,0 +1,9 @@ +name: General Data Protection Regulation (GDPR) - Fields +description: Allow tracking and removing of user data in all fields. +core: 8.x +type: module +package: General Data Protection Regulation + +dependencies: + - gdpr:gdpr + - ctools:ctools (>=8.x-3.0-beta1) diff --git a/modules/gdpr_fields/gdpr_fields.links.task.yml b/modules/gdpr_fields/gdpr_fields.links.task.yml new file mode 100644 index 0000000..3787fc9 --- /dev/null +++ b/modules/gdpr_fields/gdpr_fields.links.task.yml @@ -0,0 +1,4 @@ +gdpr_fields.fields_list: + route_name: gdpr_fields.fields_list + title: 'Used in GDPR' + base_route: entity.field_storage_config.collection diff --git a/modules/gdpr_fields/gdpr_fields.module b/modules/gdpr_fields/gdpr_fields.module new file mode 100644 index 0000000..fb145ad --- /dev/null +++ b/modules/gdpr_fields/gdpr_fields.module @@ -0,0 +1,100 @@ +getFormObject()->getEntity(); + // @todo Check that target entity is a content entity. + + $enabled = $field->getThirdPartySetting('gdpr_fields', 'gdpr_fields_enabled', FALSE); + $form['field']['gdpr_fields'] = [ + '#type' => 'details', + '#title' => t('GDPR field settings'), + '#open' => $enabled, + ]; + + $form['field']['gdpr_fields']['gdpr_fields_enabled'] = [ + '#type' => 'checkbox', + '#title' => t('This is a GDPR field'), + '#default_value' => $enabled, + ]; + + $form['field']['gdpr_fields']['gdpr_fields_rta'] = [ + '#type' => 'select', + '#title' => t('Right to access'), + '#options' => [ + 'inc' => 'Included', + 'maybe' => 'Maybe', + 'no' => 'Not', + ], + '#default_value' => $field->getThirdPartySetting('gdpr_fields', 'gdpr_fields_rta', 'no'), + '#states' => array( + 'visible' => array( + ':input[name="gdpr_fields_enabled"]' => array( + 'checked' => TRUE, + ), + ), + ), + ]; + + $form['field']['gdpr_fields']['gdpr_fields_rtf'] = [ + '#type' => 'select', + '#title' => t('Right to be forgotten'), + '#options' => [ + 'obfuscate' => 'Obfuscate', + 'remove' => 'Remove', + 'maybe' => 'Maybe', + 'no' => 'Not', + ], + '#default_value' => $field->getThirdPartySetting('gdpr_fields', 'gdpr_fields_rtf', 'no'), + '#states' => array( + 'visible' => array( + ':input[name="gdpr_fields_enabled"]' => array( + 'checked' => TRUE, + ), + ), + ), + ]; + $form['actions']['submit']['#submit'][] = 'gdpr_fields_form_field_config_edit_form_submit'; +} + +/** + * Form submission handler for gdpr_fields_form_field_config_edit_form_alter. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ +function gdpr_fields_form_field_config_edit_form_submit(array $form, FormStateInterface $form_state) { + /* @var \Drupal\Core\Field\FieldConfigInterface $field */ + $field = $form_state->getFormObject()->getEntity(); + $form_fields = &$form_state->getValues(); + + // If the private option is checked, update settings. + if ($form_fields['gdpr_fields_enabled']) { + $field->setThirdPartySetting('gdpr_fields', 'gdpr_fields_enabled', TRUE); + $field->setThirdPartySetting('gdpr_fields', 'gdpr_fields_rta', $form_fields['gdpr_fields_rta']); + $field->setThirdPartySetting('gdpr_fields', 'gdpr_fields_rtf', $form_fields['gdpr_fields_rtf']); + $field->save(); + } + else { + $field->unsetThirdPartySetting('gdpr_fields', 'gdpr_fields_enabled'); + $field->unsetThirdPartySetting('gdpr_fields', 'gdpr_fields_rta'); + $field->unsetThirdPartySetting('gdpr_fields', 'gdpr_fields_rtf'); + $field->save(); + } + +} diff --git a/modules/gdpr_fields/gdpr_fields.permissions.yml b/modules/gdpr_fields/gdpr_fields.permissions.yml new file mode 100644 index 0000000..79f6247 --- /dev/null +++ b/modules/gdpr_fields/gdpr_fields.permissions.yml @@ -0,0 +1,5 @@ +view gdpr fields: + title: 'View GDPR fields' + +edit gdpr fields: + title: 'Edit GDPR fields' diff --git a/modules/gdpr_fields/gdpr_fields.routing.yml b/modules/gdpr_fields/gdpr_fields.routing.yml new file mode 100644 index 0000000..0b79a36 --- /dev/null +++ b/modules/gdpr_fields/gdpr_fields.routing.yml @@ -0,0 +1,7 @@ +gdpr_fields.fields_list: + path: '/admin/reports/fields/gdpr-fields' + defaults: + _controller: '\Drupal\gdpr_fields\Controller\GDPRController::fieldsList' + _title: 'Used in GDPR' + requirements: + _permission: 'view gdpr fields' diff --git a/modules/gdpr_fields/gdpr_fields.services.yml b/modules/gdpr_fields/gdpr_fields.services.yml new file mode 100644 index 0000000..cf64343 --- /dev/null +++ b/modules/gdpr_fields/gdpr_fields.services.yml @@ -0,0 +1,4 @@ +services: + gdpr_fields.collector: + class: Drupal\gdpr_fields\GDPRCollector + arguments: ['@entity_type.manager', '@plugin.manager.ctools.relationship'] diff --git a/modules/gdpr_fields/src/Controller/GDPRController.php b/modules/gdpr_fields/src/Controller/GDPRController.php new file mode 100644 index 0000000..94d55e8 --- /dev/null +++ b/modules/gdpr_fields/src/Controller/GDPRController.php @@ -0,0 +1,110 @@ +collector = $collector; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('gdpr_fields.collector') + ); + } + + /** + * Lists all fields with GDPR sensitivity. + * + * @return array + * The Views plugins report page. + */ + public function fieldsList() { + $output = []; + $entities = []; + $this->collector->getEntities($entities); + + foreach ($entities as $entity_type => $bundles) { + $output[$entity_type] = array( + '#type' => 'details', + '#title' => t($entity_type), + '#description' => t('@configure entity @entity_type for GDPR.', [ + // @todo Create ability to exclude entity type from GDPR in configuration. + '@configure' => Link::fromTextAndUrl('Configure', Url::fromUri('internal:/'))->toString(), + '@entity_type' => ucfirst($entity_type), + ]), + '#open' => TRUE, + ); + + if (count($bundles) > 1) { + foreach ($bundles as $bundle_id) { + $output[$entity_type][$bundle_id] = array( + '#type' => 'details', + '#title' => t($bundle_id), + '#open' => TRUE, + ); + $output[$entity_type][$bundle_id]['fields'] = $this->buildFieldTable($entity_type, $bundle_id); + } + } + else { + // Don't add another collapsible wrapper around single bundle entities. + $bundle_id = reset($bundles); + $output[$entity_type][$bundle_id]['fields'] = $this->buildFieldTable($entity_type, $bundle_id); + } + } + + return $output; + } + + /** + * Build a table for entity field list. + * + * @param string $entity_type + * The entity type id. + * @param string $bundle_id + * The entity bundle id. + * + * @return array + * Renderable array for field list table. + */ + protected function buildFieldTable($entity_type, $bundle_id) { + $rows = $this->collector->listFields($entity_type, $bundle_id); + // Sort rows by field name. + ksort($rows); + + return [ + '#type' => 'table', + '#header' => [t('Name'), t('Type'), t('Right to access'), t('Right to be forgotten'), ''], + '#rows' => $rows, + '#sticky' => TRUE, + '#empty' => t('There are no GDPR fields for this entity.'), + ]; + } + +} diff --git a/modules/gdpr_fields/src/GDPRCollector.php b/modules/gdpr_fields/src/GDPRCollector.php new file mode 100644 index 0000000..f91edf7 --- /dev/null +++ b/modules/gdpr_fields/src/GDPRCollector.php @@ -0,0 +1,179 @@ +entityTypeManager = $entity_type_manager; + $this->relationshipManager = $relationship_manager; + } + + /** + * Get entity tree for GDPR. + * + * @param $entity_list + * List of all gotten entities keyed by entity type and bundle id. + * @param string $entity_type + * The entity type id. + * @param string|null $bundle_id + * The entity bundle id, NULL if bundles should be loaded. + */ + public function getEntities(&$entity_list, $entity_type = 'user', $bundle_id = NULL) { + $definition = $this->entityTypeManager->getDefinition($entity_type); + + if ($definition instanceof ConfigEntityTypeInterface) { + return; + } + + if (!$bundle_id) { + if ($definition->getBundleEntityType()) { + $bundle_storage = $this->entityTypeManager->getStorage($definition->getBundleEntityType()); + foreach (array_keys($bundle_storage->loadMultiple()) as $bundle_id) { + $this->getEntities($entity_list, $entity_type, $bundle_id); + } + } + else { + $this->getEntities($entity_list, $entity_type, $entity_type); + } + + return; + } + + // Check for recursion. + if (isset($entity_list[$entity_type][$bundle_id])) { + return; + } + + // Set entity. + $entity_list[$entity_type][$bundle_id] = $bundle_id; + + // Find relationships. + $context = new Context(new ContextDefinition("entity:{$entity_type}")); + $definitions = $this->relationshipManager->getDefinitionsForContexts([$context]); + + foreach ($definitions as $definition_id => $definition) { + list($type, , , $field) = explode(':', $definition_id); + + if ($type == 'typed_data_entity_relationship') { + if (isset($definition['target_entity_type'])) { + $this->getEntities($entity_list, $definition['target_entity_type']); + } + } + elseif ($type == 'typed_data_entity_relationship_reverse') { + if (isset($definition['source_entity_type'])) { + $this->getEntities($entity_list, $definition['source_entity_type']); + } + } + else { + continue; + } + } + } + + /** + * List fields on entity including their GDPR values. + * + * @param string $entity_type + * The entity type id. + * @param string $bundle_id + * The entity bundle id. + * + * @return array + * GDPR entity field list. + */ + public function listFields($entity_type = 'user', $bundle_id) { + $storage = $this->entityTypeManager->getStorage($entity_type); + $entity_definition = $this->entityTypeManager->getDefinition($entity_type); + $bundle_type = $entity_definition->getBundleEntityType(); + + // Create a blank entity. + $values = []; + if ($entity_definition->hasKey('bundle')) { + $bundle_key = $entity_definition->getKey('bundle'); + $values[$bundle_key] = $bundle_id; + } + $entity = $storage->create($values); + + // Get fields for entity. + $fields = []; + foreach ($entity as $field_id => $field) { + /** @var \Drupal\Core\Field\FieldItemListInterface $field */ + $field_definition = $field->getFieldDefinition(); + $key = "$entity_type.$bundle_id.$field_id"; + $route_name = "entity.field_config.{$entity_type}_field_edit_form"; + $route_params = [ + 'field_config' => $key, + ]; + + if (isset($bundle_key)) { + $route_params[$bundle_type] = $bundle_id; + } + + $fields[$key] = [ + 'title' => $field_definition->getLabel(), + 'type' => $field_definition->getType(), + 'gdpr_rta' => 'None', + 'gdpr_rtf' => 'None', + 'edit' => '', + ]; + + if ($entity_definition->get('field_ui_base_route')) { + $url = Url::fromRoute($route_name, $route_params); + + if ($url->access()) { + $fields[$key]['edit'] = Link::fromTextAndUrl('edit', $url); + } + } + $config = $field_definition->getConfig($bundle_id); + + if ($config->getThirdPartySetting('gdpr_fields', 'gdpr_fields_enabled', FALSE)) { + $fields[$key]['gdpr_rta'] = $config->getThirdPartySetting('gdpr_fields', 'gdpr_fields_rta', 'no'); + $fields[$key]['gdpr_rtf'] = $config->getThirdPartySetting('gdpr_fields', 'gdpr_fields_rtf', 'no'); + } + } + + return $fields; + } + +} diff --git a/modules/gdpr_fields/src/Plugin/Deriver/TypedDataEntityRelationshipReverseDeriver.php b/modules/gdpr_fields/src/Plugin/Deriver/TypedDataEntityRelationshipReverseDeriver.php new file mode 100644 index 0000000..b1c4638 --- /dev/null +++ b/modules/gdpr_fields/src/Plugin/Deriver/TypedDataEntityRelationshipReverseDeriver.php @@ -0,0 +1,41 @@ +getType() == 'entity_reference') { + parent::generateDerivativeDefinition($base_plugin_definition, $data_type_id, $data_type_definition, $base_definition, $property_name, $property_definition); + + // Provide the entity type. + $derivative_id = $data_type_id . ':' . $property_name; + if (isset($this->derivatives[$derivative_id])) { + if ($base_definition->getConstraint('EntityType')) { + $this->derivatives[$derivative_id]['source_entity_type'] = $base_definition->getConstraint('EntityType'); + } + + $target_data_type = 'entity:' . $this->derivatives[$derivative_id]['target_entity_type']; + $context_definition = new ContextDefinition($target_data_type, $this->typedDataManager->createDataDefinition($target_data_type)); + // Add the constraints of the base definition to the context definition. + if ($property_definition->getFieldStorageDefinition()->getPropertyDefinition('entity')->getConstraint('Bundle')) { + $context_definition->addConstraint('Bundle', $property_definition->getFieldStorageDefinition()->getPropertyDefinition('entity')->getConstraint('Bundle')); + } + $this->derivatives[$derivative_id]['context']['base'] = $context_definition; + } + } + } + +} diff --git a/modules/gdpr_fields/src/Plugin/Relationship/TypedDataEntityRelationshipReverse.php b/modules/gdpr_fields/src/Plugin/Relationship/TypedDataEntityRelationshipReverse.php new file mode 100644 index 0000000..9e7aa61 --- /dev/null +++ b/modules/gdpr_fields/src/Plugin/Relationship/TypedDataEntityRelationshipReverse.php @@ -0,0 +1,53 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@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') + ); + } + +}