diff --git a/gdpr.links.task.yml b/gdpr.links.task.yml index 5729f40..4b2d3b3 100644 --- a/gdpr.links.task.yml +++ b/gdpr.links.task.yml @@ -2,3 +2,10 @@ gdpr.collected_user_data: title: 'All your data' route_name: gdpr.collected_user_data base_route: entity.user.canonical + + +gdpr.collected_user_data_default: + title: 'Summary' + parent_id: gdpr.collected_user_data + route_name: gdpr.collected_user_data + base_route: entity.user.canonical diff --git a/gdpr.routing.yml b/gdpr.routing.yml index db68340..951f5ef 100644 --- a/gdpr.routing.yml +++ b/gdpr.routing.yml @@ -1,5 +1,5 @@ gdpr.collected_user_data: - path: '/user/{user}/collected-data' + path: '/user/{user}/gdpr' defaults: _title: 'Data stored about you' _controller: '\Drupal\gdpr\Controller\UserController::collectedData' diff --git a/modules/gdpr_fields/gdpr_fields.module b/modules/gdpr_fields/gdpr_fields.module index fb145ad..1f084d4 100644 --- a/modules/gdpr_fields/gdpr_fields.module +++ b/modules/gdpr_fields/gdpr_fields.module @@ -18,17 +18,16 @@ function gdpr_fields_form_field_config_edit_form_alter(&$form, FormStateInterfac $field = $form_state->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, + '#open' => TRUE, ]; $form['field']['gdpr_fields']['gdpr_fields_enabled'] = [ '#type' => 'checkbox', '#title' => t('This is a GDPR field'), - '#default_value' => $enabled, + '#default_value' => $field->getThirdPartySetting('gdpr_fields', 'gdpr_fields_enabled', FALSE), ]; $form['field']['gdpr_fields']['gdpr_fields_rta'] = [ @@ -53,7 +52,7 @@ function gdpr_fields_form_field_config_edit_form_alter(&$form, FormStateInterfac '#type' => 'select', '#title' => t('Right to be forgotten'), '#options' => [ - 'obfuscate' => 'Obfuscate', + 'anonymise' => 'Anonymise', 'remove' => 'Remove', 'maybe' => 'Maybe', 'no' => 'Not', diff --git a/modules/gdpr_fields/src/Controller/GDPRController.php b/modules/gdpr_fields/src/Controller/GDPRController.php index 94d55e8..f5a1586 100644 --- a/modules/gdpr_fields/src/Controller/GDPRController.php +++ b/modules/gdpr_fields/src/Controller/GDPRController.php @@ -6,6 +6,7 @@ use Drupal\Core\Link; use Drupal\Core\Url; use Drupal\gdpr_fields\GDPRCollector; +use Drupal\user\UserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -107,4 +108,54 @@ protected function buildFieldTable($entity_type, $bundle_id) { ]; } + /** + * Builds data for Right to Access data requests. + * + * @param \Drupal\user\UserInterface $user + * The user to fetch data for. + * + * @return array + * Structured array of user related data. + */ + public function rtaData(UserInterface $user) { + $rows = []; + $entities = []; + $this->collector->getValueEntities($entities, 'user', $user); + + foreach ($entities as $entity_type => $bundles) { + foreach ($bundles as $bundle_entity) { + $rows += $this->collector->fieldValues($entity_type, $bundle_entity, ['rta' => 'rta']); + } + } + + // Sort rows by field name. + ksort($rows); + return $rows; + } + + /** + * Builds data for Right to be Forgotten data requests. + * + * @param \Drupal\user\UserInterface $user + * The user to fetch data for. + * + * @return array + * Structured array of user related data. + */ + public function rtfData(UserInterface $user) { + $rows = []; + $entities = []; + $this->collector->getValueEntities($entities, 'user', $user); + + foreach ($entities as $entity_type => $bundles) { + foreach ($bundles as $bundle_entity) { + $rows += $this->collector->fieldValues($entity_type, $bundle_entity, ['rtf' => 'rtf']); + } + } + + // Sort rows by field name. + ksort($rows); + return $rows; + } + } diff --git a/modules/gdpr_fields/src/GDPRCollector.php b/modules/gdpr_fields/src/GDPRCollector.php index f91edf7..895e6a9 100644 --- a/modules/gdpr_fields/src/GDPRCollector.php +++ b/modules/gdpr_fields/src/GDPRCollector.php @@ -3,6 +3,7 @@ namespace Drupal\gdpr_fields; use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Link; use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Entity\EntityTypeManager; @@ -29,13 +30,6 @@ class GDPRCollector { */ protected $relationshipManager; - /** - * A prepared list of all fields, keyed by base_table and handler type. - * - * @var array - */ - protected $fields; - /** * Constructs a GDPRCollector object. * @@ -66,6 +60,8 @@ public function getEntities(&$entity_list, $entity_type = 'user', $bundle_id = N return; } + // @todo Add way of excluding irrelevant entity types. + if (!$bundle_id) { if ($definition->getBundleEntityType()) { $bundle_storage = $this->entityTypeManager->getStorage($definition->getBundleEntityType()); @@ -93,7 +89,7 @@ public function getEntities(&$entity_list, $entity_type = 'user', $bundle_id = N $definitions = $this->relationshipManager->getDefinitionsForContexts([$context]); foreach ($definitions as $definition_id => $definition) { - list($type, , , $field) = explode(':', $definition_id); + list($type, , ,) = explode(':', $definition_id); if ($type == 'typed_data_entity_relationship') { if (isset($definition['target_entity_type'])) { @@ -111,6 +107,74 @@ public function getEntities(&$entity_list, $entity_type = 'user', $bundle_id = N } } + /** + * Get entity value tree for GDPR entities. + * + * @param array $entity_list + * List of all gotten entities keyed by entity type and bundle id. + * @param string $entity_type + * The entity type id. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The fully loaded entity for which values are gotten. + */ + public function getValueEntities(array &$entity_list, $entity_type, EntityInterface $entity) { + $definition = $this->entityTypeManager->getDefinition($entity_type); + + if ($definition instanceof ConfigEntityTypeInterface) { + return; + } + + // @todo Add way of excluding irrelevant entity types. + + // Check for recursion. + if (isset($entity_list[$entity_type][$entity->id()])) { + return; + } + + // Set entity. + $entity_list[$entity_type][$entity->id()] = $entity; + + // Find relationships. + $context = new Context(new ContextDefinition("entity:{$entity_type}")); + $definitions = $this->relationshipManager->getDefinitionsForContexts([$context]); + + + foreach ($definitions as $definition_id => $definition) { + list($type, $definition_entity, ,) = explode(':', $definition_id); + + // Ignore entity revisions for now. + if ($definition_entity == 'entity_revision') { + continue; + } + + if ($type == 'typed_data_entity_relationship') { + /* @var \Drupal\ctools\Plugin\Relationship\TypedDataEntityRelationship $plugin */ + $plugin = $this->relationshipManager->createInstance($definition_id); + $plugin->setContextValue('base', $entity); + + $relationship = $plugin->getRelationship(); + if ($relationship->hasContextValue()) { + $relationship_entity = $relationship->getContextValue(); + $this->getValueEntities($entity_list, $relationship_entity->getEntityTypeId(), $relationship_entity); + } + } + elseif ($type == 'typed_data_entity_relationship_reverse') { + /* @var \Drupal\gdpr_fields\Plugin\Relationship\TypedDataEntityRelationshipReverse $plugin */ + $plugin = $this->relationshipManager->createInstance($definition_id); + $plugin->setContextValue('base', $entity); + + $relationship = $plugin->getRelationship(); + if ($relationship->hasContextValue()) { + $relationship_entity = $relationship->getContextValue(); + $this->getValueEntities($entity_list, $relationship_entity->getEntityTypeId(), $relationship_entity); + } + } + else { + continue; + } + } + } + /** * List fields on entity including their GDPR values. * @@ -176,4 +240,82 @@ public function listFields($entity_type = 'user', $bundle_id) { return $fields; } + /** + * List field values on an entity including their GDPR values. + * + * @param string $entity_type + * The entity type id. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The fully loaded entity for which values are listed. + * @param array $extra_fields + * Add extra fields if required + * + * @return array + * GDPR entity field value list. + */ + public function fieldValues($entity_type = 'user', EntityInterface $entity, $extra_fields = []) { + $entity_definition = $this->entityTypeManager->getDefinition($entity_type); + $bundle_type = $entity_definition->getBundleEntityType(); + $bundle_id = $entity->bundle(); + if ($bundle_type) { + $bundle_storage = $this->entityTypeManager->getStorage($bundle_type); + $bundle_label = $bundle_storage->load($bundle_id)->label(); + } + else { + $bundle_label = $entity->getEntityType()->getLabel(); + } + + + // Get fields for entity. + $fields = []; + foreach ($entity as $field_id => $field) { + /** @var \Drupal\Core\Field\FieldItemListInterface $field */ + $field_definition = $field->getFieldDefinition(); + + $config = $field_definition->getConfig($bundle_id); + if (!$config->getThirdPartySetting('gdpr_fields', 'gdpr_fields_enabled', FALSE)) { + continue; + } + + + $key = "$entity_type.{$entity->id()}.$field_id"; + + $fieldValue = $field->getString(); + $fields[$key] = [ + 'title' => $field_definition->getLabel(), + 'value' => $fieldValue, + 'entity' => $entity->getEntityType()->getLabel(), + 'bundle' => $bundle_label, + ]; + + if (empty($extra_fields)) { + continue; + } + + // Fetch and validate based on field settings. + if (isset($extra_fields['rta'])) { + $rta_value = $config->getThirdPartySetting('gdpr_fields', 'gdpr_fields_rta', FALSE); + + if ($rta_value && $rta_value !== 'no') { + $fields[$key]['gdpr_rta'] = $rta_value; + } + else { + unset($fields[$key]); + } + } + if (isset($extra_fields['rtf'])) { + $rtf_value = $config->getThirdPartySetting('gdpr_fields', 'gdpr_fields_rtf', FALSE); + + if ($rtf_value && $rtf_value !== 'no') { + $fields[$key]['gdpr_rtf'] = $rtf_value; + } + else { + unset($fields[$key]); + } + } + } + + return $fields; + } + } diff --git a/modules/gdpr_fields/src/Plugin/Relationship/TypedDataEntityRelationshipReverse.php b/modules/gdpr_fields/src/Plugin/Relationship/TypedDataEntityRelationshipReverse.php index 9e7aa61..0adcfb8 100644 --- a/modules/gdpr_fields/src/Plugin/Relationship/TypedDataEntityRelationshipReverse.php +++ b/modules/gdpr_fields/src/Plugin/Relationship/TypedDataEntityRelationshipReverse.php @@ -1,10 +1,15 @@ getPluginDefinition(); + + if (!isset($plugin_definition['source_entity_type'])) { + return parent::getRelationship(); + } + $source_entity_type = $plugin_definition['source_entity_type']; + $context_definition = new ContextDefinition("entity:{$source_entity_type}", $plugin_definition['label']); + $context_value = NULL; + + // If the 'base' context has a value, then get the property value to put on + // the context (otherwise, mapping hasn't occurred yet and we just want to + // return the context with the right definition and no value). + if ($this->getContext('base')->hasContextValue()) { + $data = $this->getData($this->getContext('base'), $source_entity_type); + if ($data) { + $context_value = $data; + } + } + + $context_definition->setDefaultValue($context_value); + return new Context($context_definition, $context_value); + } + + /** + * {@inheritdoc} + */ + protected function getData(ContextInterface $context, $source_entity_type = NULL) { + if (!$source_entity_type) { + return FALSE; + } + + /** @var \Drupal\Core\Entity\EntityInterface $base */ + $base = $context->getContextValue(); + $name = $this->getPluginDefinition()['property_name']; + + try { + $query = \Drupal::entityQuery($source_entity_type) + ->condition($name, $base->id(), '=') + ->range(0, 1) + ->execute(); + } + catch (QueryException $e) { + return FALSE; + } + + $data = reset($query); + if ($data) { + $data = \Drupal::entityTypeManager()->getStorage($source_entity_type)->load($data); + } + + return $data; + } + } diff --git a/modules/gdpr_tasks/config/install/core.entity_form_display.gdpr_task.gdpr_remove.default.yml b/modules/gdpr_tasks/config/install/core.entity_form_display.gdpr_task.gdpr_remove.default.yml new file mode 100644 index 0000000..207579d --- /dev/null +++ b/modules/gdpr_tasks/config/install/core.entity_form_display.gdpr_task.gdpr_remove.default.yml @@ -0,0 +1,14 @@ +langcode: en +status: true +dependencies: + config: + - gdpr_tasks.gdpr_task_type.gdpr_remove +third_party_settings: { } +id: gdpr_task.gdpr_remove.default +targetEntityType: gdpr_task +bundle: gdpr_remove +mode: default +content: { } +hidden: + status: true + user_id: true diff --git a/modules/gdpr_tasks/config/install/core.entity_form_display.gdpr_task.gdpr_sar.default.yml b/modules/gdpr_tasks/config/install/core.entity_form_display.gdpr_task.gdpr_sar.default.yml new file mode 100644 index 0000000..ed97a31 --- /dev/null +++ b/modules/gdpr_tasks/config/install/core.entity_form_display.gdpr_task.gdpr_sar.default.yml @@ -0,0 +1,25 @@ +langcode: en +status: true +dependencies: + config: + - field.field.gdpr_task.gdpr_sar.manual_data + - field.field.gdpr_task.gdpr_sar.sar_export + - gdpr_tasks.gdpr_task_type.gdpr_sar +third_party_settings: { } +id: gdpr_task.gdpr_sar.default +targetEntityType: gdpr_task +bundle: gdpr_sar +mode: default +content: + manual_data: + type: string_textarea + weight: 0 + region: content + settings: + rows: 5 + placeholder: '' + third_party_settings: { } +hidden: + sar_export: true + status: true + user_id: true diff --git a/modules/gdpr_tasks/config/install/core.entity_view_display.gdpr_task.gdpr_remove.default.yml b/modules/gdpr_tasks/config/install/core.entity_view_display.gdpr_task.gdpr_remove.default.yml new file mode 100644 index 0000000..b797ef9 --- /dev/null +++ b/modules/gdpr_tasks/config/install/core.entity_view_display.gdpr_task.gdpr_remove.default.yml @@ -0,0 +1,28 @@ +langcode: en +status: true +dependencies: + config: + - gdpr_tasks.gdpr_task_type.gdpr_remove + module: + - options + - user +id: gdpr_task.gdpr_remove.default +targetEntityType: gdpr_task +bundle: gdpr_remove +mode: default +content: + status: + type: list_default + weight: 1 + region: content + label: inline + settings: { } + third_party_settings: { } + user_id: + label: inline + type: author + weight: 0 + region: content + settings: { } + third_party_settings: { } +hidden: { } diff --git a/modules/gdpr_tasks/config/install/core.entity_view_display.gdpr_task.gdpr_sar.default.yml b/modules/gdpr_tasks/config/install/core.entity_view_display.gdpr_task.gdpr_sar.default.yml new file mode 100644 index 0000000..2c834b4 --- /dev/null +++ b/modules/gdpr_tasks/config/install/core.entity_view_display.gdpr_task.gdpr_sar.default.yml @@ -0,0 +1,40 @@ +langcode: en +status: true +dependencies: + config: + - field.field.gdpr_task.gdpr_sar.manual_data + - field.field.gdpr_task.gdpr_sar.sar_export + - gdpr_tasks.gdpr_task_type.gdpr_sar + module: + - file + - options + - user +id: gdpr_task.gdpr_sar.default +targetEntityType: gdpr_task +bundle: gdpr_sar +mode: default +content: + sar_export: + type: file_default + weight: 2 + region: content + label: inline + settings: + use_description_as_link_text: true + third_party_settings: { } + status: + type: list_default + weight: 1 + region: content + label: inline + settings: { } + third_party_settings: { } + user_id: + label: inline + type: author + weight: 0 + region: content + settings: { } + third_party_settings: { } +hidden: + manual_data: true diff --git a/modules/gdpr_tasks/config/install/field.field.gdpr_task.gdpr_sar.manual_data.yml b/modules/gdpr_tasks/config/install/field.field.gdpr_task.gdpr_sar.manual_data.yml new file mode 100644 index 0000000..8b682e4 --- /dev/null +++ b/modules/gdpr_tasks/config/install/field.field.gdpr_task.gdpr_sar.manual_data.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.gdpr_task.manual_data + - gdpr_tasks.gdpr_task_type.gdpr_sar +id: gdpr_task.gdpr_sar.manual_data +field_name: manual_data +entity_type: gdpr_task +bundle: gdpr_sar +label: 'Data Override' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string_long diff --git a/modules/gdpr_tasks/config/install/field.field.gdpr_task.gdpr_sar.sar_export.yml b/modules/gdpr_tasks/config/install/field.field.gdpr_task.gdpr_sar.sar_export.yml new file mode 100644 index 0000000..da935fa --- /dev/null +++ b/modules/gdpr_tasks/config/install/field.field.gdpr_task.gdpr_sar.sar_export.yml @@ -0,0 +1,26 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.gdpr_task.sar_export + - gdpr_tasks.gdpr_task_type.gdpr_sar + module: + - file +id: gdpr_task.gdpr_sar.sar_export +field_name: sar_export +entity_type: gdpr_task +bundle: gdpr_sar +label: Export +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + file_directory: 'gdpr-exports' + file_extensions: csv + max_filesize: '' + description_field: false + handler: 'default:file' + handler_settings: { } +field_type: file diff --git a/modules/gdpr_tasks/config/install/field.storage.gdpr_task.manual_data.yml b/modules/gdpr_tasks/config/install/field.storage.gdpr_task.manual_data.yml new file mode 100644 index 0000000..5d68892 --- /dev/null +++ b/modules/gdpr_tasks/config/install/field.storage.gdpr_task.manual_data.yml @@ -0,0 +1,17 @@ +langcode: en +status: true +dependencies: + module: + - gdpr_tasks +id: gdpr_task.manual_data +field_name: manual_data +entity_type: gdpr_task +type: string_long +settings: + case_sensitive: false +module: core +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/gdpr_tasks/config/install/field.storage.gdpr_task.sar_export.yml b/modules/gdpr_tasks/config/install/field.storage.gdpr_task.sar_export.yml new file mode 100644 index 0000000..fe5ced1 --- /dev/null +++ b/modules/gdpr_tasks/config/install/field.storage.gdpr_task.sar_export.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + module: + - file + - gdpr_tasks +id: gdpr_task.sar_export +field_name: sar_export +entity_type: gdpr_task +type: file +settings: + display_field: false + display_default: false + uri_scheme: private + target_type: file +module: file +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/gdpr_tasks/config/install/gdpr_tasks.gdpr_task_type.gdpr_remove.yml b/modules/gdpr_tasks/config/install/gdpr_tasks.gdpr_task_type.gdpr_remove.yml new file mode 100644 index 0000000..21f52ec --- /dev/null +++ b/modules/gdpr_tasks/config/install/gdpr_tasks.gdpr_task_type.gdpr_remove.yml @@ -0,0 +1,5 @@ +langcode: en +status: true +dependencies: { } +id: gdpr_remove +label: 'Removal Request' diff --git a/modules/gdpr_tasks/config/install/gdpr_tasks.gdpr_task_type.gdpr_sar.yml b/modules/gdpr_tasks/config/install/gdpr_tasks.gdpr_task_type.gdpr_sar.yml new file mode 100644 index 0000000..46a8f07 --- /dev/null +++ b/modules/gdpr_tasks/config/install/gdpr_tasks.gdpr_task_type.gdpr_sar.yml @@ -0,0 +1,5 @@ +langcode: en +status: true +dependencies: { } +id: gdpr_sar +label: 'SARs Request' diff --git a/modules/gdpr_tasks/config/install/views.view.gdpr_tasks_my_data_requests.yml b/modules/gdpr_tasks/config/install/views.view.gdpr_tasks_my_data_requests.yml new file mode 100644 index 0000000..01d75e4 --- /dev/null +++ b/modules/gdpr_tasks/config/install/views.view.gdpr_tasks_my_data_requests.yml @@ -0,0 +1,354 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.gdpr_task.sar_export + - gdpr_tasks.gdpr_task_type.gdpr_sar + module: + - file + - gdpr_tasks + - user +id: gdpr_tasks_my_data_requests +label: 'GDRP User Data Requests' +module: views +description: '' +tag: '' +base_table: gdpr_task +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'view gdpr tasks' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: none + options: + items_per_page: 0 + offset: 0 + style: + type: table + row: + type: fields + fields: + created: + id: created + table: gdpr_task + field: created + relationship: none + group_type: group + admin_label: '' + label: 'Requested date' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: timestamp + settings: + date_format: html_date + custom_date_format: '' + timezone: '' + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: gdpr_task + entity_field: created + plugin_id: field + sar_export: + id: sar_export + table: gdpr_task__sar_export + field: sar_export + relationship: none + group_type: group + admin_label: '' + label: 'Data export' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: file_default + settings: + use_description_as_link_text: true + group_column: '' + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + plugin_id: field + filters: + type: + id: type + table: gdpr_task + field: type + relationship: none + group_type: group + admin_label: '' + operator: in + value: + gdpr_sar: gdpr_sar + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: gdpr_task + entity_field: type + plugin_id: bundle + status: + id: status + table: gdpr_task + field: status + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: closed + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + placeholder: '' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: gdpr_task + entity_field: status + plugin_id: string + sorts: { } + title: 'My data requests' + header: { } + footer: { } + empty: + area: + id: area + table: views + field: area + relationship: none + group_type: group + admin_label: '' + empty: true + tokenize: false + content: + value: 'No data requests have been completed yet.' + format: basic_html + plugin_id: text + relationships: { } + arguments: + user_id: + id: user_id + table: gdpr_task + field: user_id + relationship: none + group_type: group + admin_label: '' + default_action: default + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: user + default_argument_options: + user: false + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + break_phrase: false + not: false + entity_type: gdpr_task + entity_field: user_id + plugin_id: numeric + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - user.permissions + tags: + - 'config:field.storage.gdpr_task.sar_export' + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: user/%user/gdpr/requests + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - user.permissions + tags: + - 'config:field.storage.gdpr_task.sar_export' diff --git a/modules/gdpr_tasks/config/schema/gdpr_task_type.schema.yml b/modules/gdpr_tasks/config/schema/gdpr_task_type.schema.yml new file mode 100644 index 0000000..7920474 --- /dev/null +++ b/modules/gdpr_tasks/config/schema/gdpr_task_type.schema.yml @@ -0,0 +1,12 @@ +gdpr_tasks.gdpr_task_type.*: + type: config_entity + label: 'Task type config' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + uuid: + type: string diff --git a/modules/gdpr_tasks/gdpr_task.page.inc b/modules/gdpr_tasks/gdpr_task.page.inc new file mode 100644 index 0000000..37b4f3d --- /dev/null +++ b/modules/gdpr_tasks/gdpr_task.page.inc @@ -0,0 +1,80 @@ +getStorage($gdpr_task->getEntityType()->getBundleEntityType()); + $variables['type'] = $bundle_storage->load($gdpr_task->bundle())->label();; + + /* @var \Drupal\gdpr_fields\Controller\GDPRController $controller */ + $controller = \Drupal::classResolver()->getInstanceFromDefinition(GDPRController::class); + + /* @var \Drupal\gdpr_tasks\Entity\TaskInterface $entity */ + switch ($gdpr_task->bundle()) { + case 'gdpr_remove': + $rows = $controller->rtfData($gdpr_task->getOwner()); + + $variables['data'] = [ + '#type' => 'table', + '#header' => [ + t('Name'), + t('Type'), + t('Entity'), + t('Bundle'), + t('Right to be forgotten'), + ], + '#rows' => $rows, + '#sticky' => TRUE, + '#empty' => t('There are no GDPR fields.'), + ]; + + $variables['actions'] = \Drupal::service('entity.form_builder')->getForm($gdpr_task, 'process'); + break; + + case 'gdpr_sar': + $rows = $controller->rtaData($gdpr_task->getOwner()); + + $variables['data'] = [ + '#type' => 'table', + '#header' => [ + t('Name'), + t('Type'), + t('Entity'), + t('Bundle'), + t('Right to access'), + ], + '#rows' => $rows, + '#sticky' => TRUE, + '#empty' => t('There are no GDPR fields.'), + ]; + $variables['actions'] = \Drupal::service('entity.form_builder')->getForm($gdpr_task, 'process'); + break; + } + + // Helpful $content variable for templates. + foreach (Element::children($variables['elements']) as $key) { + $variables['content'][$key] = $variables['elements'][$key]; + } +} diff --git a/modules/gdpr_tasks/gdpr_tasks.info.yml b/modules/gdpr_tasks/gdpr_tasks.info.yml new file mode 100644 index 0000000..68919c1 --- /dev/null +++ b/modules/gdpr_tasks/gdpr_tasks.info.yml @@ -0,0 +1,8 @@ +name: General Data Protection Regulation (GDPR) - Tasks +description: Allow tracking of SARs and Removal requests. +core: 8.x +type: module +package: General Data Protection Regulation + +dependencies: + - gdpr:gdpr diff --git a/modules/gdpr_tasks/gdpr_tasks.links.action.yml b/modules/gdpr_tasks/gdpr_tasks.links.action.yml new file mode 100644 index 0000000..b2e1412 --- /dev/null +++ b/modules/gdpr_tasks/gdpr_tasks.links.action.yml @@ -0,0 +1,23 @@ +gdpr_tasks.request_export: + route_name: gdpr_tasks.request + route_parameters: + gdpr_task_type: 'gdpr_sar' + user: '1' + title: 'Request data export' + appears_on: + - view.gdpr_tasks_my_data_requests.page_1 + +gdpr_tasks.request_remove: + route_name: gdpr_tasks.request + route_parameters: + gdpr_task_type: 'gdpr_remove' + user: '1' + title: 'Request data removal' + appears_on: + - view.gdpr_tasks_my_data_requests.page_1 + +entity.gdpr_task_type.add_form: + route_name: entity.gdpr_task_type.add_form + title: 'Add Task type' + appears_on: + - entity.gdpr_task_type.collection diff --git a/modules/gdpr_tasks/gdpr_tasks.links.menu.yml b/modules/gdpr_tasks/gdpr_tasks.links.menu.yml new file mode 100644 index 0000000..054f5ef --- /dev/null +++ b/modules/gdpr_tasks/gdpr_tasks.links.menu.yml @@ -0,0 +1,15 @@ +# Task menu items definition +entity.gdpr_task.collection: + title: 'Task list' + route_name: entity.gdpr_task.collection + description: 'List Task entities' + parent: system.admin_structure + weight: 100 + +# Task type menu items definition +entity.gdpr_task_type.collection: + title: 'Task type' + route_name: entity.gdpr_task_type.collection + description: 'List Task type (bundles)' + parent: system.admin_structure + weight: 99 diff --git a/modules/gdpr_tasks/gdpr_tasks.links.task.yml b/modules/gdpr_tasks/gdpr_tasks.links.task.yml new file mode 100644 index 0000000..bcf151a --- /dev/null +++ b/modules/gdpr_tasks/gdpr_tasks.links.task.yml @@ -0,0 +1,28 @@ +gdpr_tasks.summary: + route_name: gdpr_tasks.summary + title: 'GDPR Summary' + base_route: gdpr_tasks.summary + +view.gdpr_tasks_my_data_requests.page_1: + title: 'My data requests' + parent_id: gdpr.collected_user_data + route_name: view.gdpr_tasks_my_data_requests.page_1 + base_route: entity.user.canonical + +entity.gdpr_task.collection: + route_name: entity.gdpr_task.collection + title: 'Task list' + base_route: gdpr_tasks.summary + +# Task routing definition +entity.gdpr_task.canonical: + route_name: entity.gdpr_task.canonical + base_route: entity.gdpr_task.canonical + title: 'View' + +entity.gdpr_task.delete_form: + route_name: entity.gdpr_task.delete_form + base_route: entity.gdpr_task.canonical + title: Delete + weight: 10 + diff --git a/modules/gdpr_tasks/gdpr_tasks.module b/modules/gdpr_tasks/gdpr_tasks.module new file mode 100644 index 0000000..783a5ca --- /dev/null +++ b/modules/gdpr_tasks/gdpr_tasks.module @@ -0,0 +1,195 @@ + [ + 'contexts' => [ + 'user', + ], + ], + ]; + + if ($user->hasPermission('view gdpr tasks')) { + $links = [ + 'summary' => [ + 'title' => t('Summary'), + 'url' => Url::fromRoute('gdpr_tasks.summary'), + 'attributes' => [ + 'title' => t('GDPR Summary'), + ], + ], + 'tasks' => [ + 'title' => t('Tasks'), + 'url' => Url::fromRoute('entity.gdpr_task.collection'), + 'attributes' => [ + 'title' => t('GDPR Task list'), + ], + ], + ]; + $items['gdpr'] += [ + '#type' => 'toolbar_item', + 'tab' => [ + '#type' => 'link', + '#title' => t('GDPR'), + '#url' => Url::fromRoute('gdpr_tasks.summary'), + '#attributes' => [ + 'title' => t('GDPR'), + 'class' => ['toolbar-icon', 'toolbar-icon-shortcut'], + ], + ], + 'tray' => [ + 'links' => [ + '#theme' => 'links', + '#links' => $links, + '#attributes' => [ + 'class' => ['toolbar-menu'], + ], + ], + ], + '#weight' => -10, + '#attached' => [ + 'library' => [ + 'shortcut/drupal.shortcut', + ], + ], + ]; + } + + return $items; +} + +/** + * Implements hook_theme(). + */ +function gdpr_tasks_theme() { + $theme = []; + $theme['gdpr_task'] = [ + 'render element' => 'elements', + 'file' => 'gdpr_task.page.inc', + 'template' => 'gdpr_task', + ]; + $theme['gdpr_task_content_add_list'] = [ + 'render element' => 'content', + 'variables' => ['content' => NULL], + 'file' => 'gdpr_task.page.inc', + ]; + return $theme; +} + +/** +* Implements hook_theme_suggestions_HOOK(). +*/ +function gdpr_tasks_theme_suggestions_gdpr_task(array $variables) { + $suggestions = []; + /* @var \Drupal\gdpr_tasks\Entity\TaskInterface $entity */ + $entity = $variables['elements']['#gdpr_task']; + $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_'); + + $suggestions[] = 'gdpr_task__' . $sanitized_view_mode; + $suggestions[] = 'gdpr_task__' . $entity->bundle(); + $suggestions[] = 'gdpr_task__' . $entity->bundle() . '__' . $sanitized_view_mode; + $suggestions[] = 'gdpr_task__' . $entity->id(); + $suggestions[] = 'gdpr_task__' . $entity->id() . '__' . $sanitized_view_mode; + return $suggestions; +} + +function gdpr_tasks_generate_sar_report(UserInterface $user) { + /* @var \Drupal\gdpr_fields\Controller\GDPRController $controller */ + $controller = \Drupal::classResolver()->getInstanceFromDefinition(GDPRController::class); + + return $controller->rtaData($user); +} + +/** + * Implements hook_entity_presave(). + * + * Process data for task requests. + * @todo Consider creating plugin or event listeners for task processing. + */ +function gdpr_tasks_entity_presave(EntityInterface $entity) { + if ($entity->getEntityTypeId() != 'gdpr_task') { + return; + } + + /* @var \Drupal\gdpr_tasks\Entity\TaskInterface $entity */ + switch ($entity->bundle()) { + case 'gdpr_remove': + break; + + // Collect relevant data and save to downloadable csv. + case 'gdpr_sar': + if (empty($entity->get('sar_export')->count())) { + /* @var \Drupal\file\Plugin\Field\FieldType\FileFieldItemList $field */ + $field = $entity->get('sar_export'); + /* @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */ + $field_definition = $field->getFieldDefinition(); + $settings = $field_definition->getSettings(); + + $config = [ + 'field_definition' => $field_definition, + 'name' => $field->getName(), + 'parent' => $field->getParent(), + ]; + /* @var \Drupal\file\Plugin\Field\FieldType\FileItem $field_type */ + $field_type = Drupal::service('plugin.manager.field.field_type')->createInstance($field_definition->getType(), $config); + + // Prepare destination. + $dirname = $field_type->getUploadLocation(); + file_prepare_directory($dirname, FILE_CREATE_DIRECTORY); + + $user = $entity->getOwner(); + $data = gdpr_tasks_generate_sar_report($user); + + $inc = []; + $maybe = []; + foreach ($data as $key => $values) { + $rta = $values['gdpr_rta']; + unset($values['gdpr_rta']); + $inc[$key] = $values; + + if ($rta == 'maybe') { + $maybe[$key] = $values; + } + } + + /* @var \Drupal\gdpr_tasks\TaskManager $task_manager */ + $task_manager = \Drupal::service('gdpr_tasks.manager'); + $destination = $task_manager->toCSV($inc, $dirname); + $export = file_get_contents($destination); + + // Generate a file entity. + // @todo Add headers to csv export. + $file = file_save_data($export, $destination, FILE_EXISTS_REPLACE); + + $values = [ + 'target_id' => $file->id(), + 'display' => (int) $settings['display_default'], + 'description' => '', + ]; + + $entity->sar_export = $values; + + $temp = $task_manager->toCSV($maybe, $dirname); + $entity->manual_data = file_get_contents($temp); + } + break; + } +} \ No newline at end of file diff --git a/modules/gdpr_tasks/gdpr_tasks.permissions.yml b/modules/gdpr_tasks/gdpr_tasks.permissions.yml new file mode 100644 index 0000000..90c6d48 --- /dev/null +++ b/modules/gdpr_tasks/gdpr_tasks.permissions.yml @@ -0,0 +1,24 @@ +create gdpr tasks: + title: 'Create GDPR tasks' + +view gdpr tasks: + title: 'View GDPR tasks' + +edit gdpr tasks: + title: 'Edit GDPR tasks' +add task entities: + title: 'Create new Task entities' + +administer task entities: + title: 'Administer Task entities' + description: 'Allow to access the administration form to configure Task entities.' + restrict access: true + +delete task entities: + title: 'Delete Task entities' + +edit task entities: + title: 'Edit Task entities' + +view task entities: + title: 'View Task entities' diff --git a/modules/gdpr_tasks/gdpr_tasks.routing.yml b/modules/gdpr_tasks/gdpr_tasks.routing.yml new file mode 100644 index 0000000..949af76 --- /dev/null +++ b/modules/gdpr_tasks/gdpr_tasks.routing.yml @@ -0,0 +1,21 @@ +gdpr_tasks.summary: + path: '/admin/gdpr/summary' + defaults: + _controller: '\Drupal\gdpr_tasks\Controller\GDPRController::summaryPage' + _title: 'GDPR Summary' + requirements: + _permission: 'view gdpr tasks' + +gdpr_tasks.request: + path: '/user/{user}/gdpr-request/{gdpr_task_type}' + defaults: + _controller: '\Drupal\gdpr_tasks\Controller\GDPRController::requestPage' + _title: 'GDPR Summary' + requirements: + _permission: 'create gdpr tasks' + options: + parameters: + gdpr_task_type: + type: string + user: + type: entity:user diff --git a/modules/gdpr_tasks/gdpr_tasks.services.yml b/modules/gdpr_tasks/gdpr_tasks.services.yml new file mode 100644 index 0000000..b956a2f --- /dev/null +++ b/modules/gdpr_tasks/gdpr_tasks.services.yml @@ -0,0 +1,4 @@ +services: + gdpr_tasks.manager: + class: Drupal\gdpr_tasks\TaskManager + arguments: ['@entity_type.manager', '@current_user'] diff --git a/modules/gdpr_tasks/src/Controller/GDPRController.php b/modules/gdpr_tasks/src/Controller/GDPRController.php new file mode 100644 index 0000000..012d046 --- /dev/null +++ b/modules/gdpr_tasks/src/Controller/GDPRController.php @@ -0,0 +1,100 @@ +entityTypeManager = $entity_type_manager; + $this->currentUser = $current_user; + $this->messenger = $messenger; + $this->taskManager = $task_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('current_user'), + $container->get('messenger'), + $container->get('gdpr_tasks.manager') + ); + } + + /** + * Placeholder for a GDPR Dashboard. + * + * @return array + * Renderable Drupal markup. + */ + public function summaryPage() { + return ['#markup' => $this->t('Summary')]; + } + + /** + * Request a GDPR Task. + * + * @param \Drupal\Core\Session\AccountInterface $user + * The user for whom the request is being made. + * @param $gdpr_task_type + * Type of task to be created. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * Return user to GDPR requests. + */ + public function requestPage(AccountInterface $user, $gdpr_task_type) { + $tasks = $this->taskManager->getUserTasks($user, $gdpr_task_type); + + if (!empty($tasks)) { + $this->messenger->addWarning('You already have a pending task.'); + } + else { + $values = [ + 'type' => $gdpr_task_type, + 'user_id' => $user->id(), + ]; + $this->entityTypeManager->getStorage('gdpr_task')->create($values)->save(); + $this->messenger->addStatus('Your request has been logged'); + } + + $response = new RedirectResponse(Url::fromRoute('view.gdpr_tasks_my_data_requests.page_1', ['user' => $user->id()])->toString()); + return $response; + } + +} diff --git a/modules/gdpr_tasks/src/Entity/Task.php b/modules/gdpr_tasks/src/Entity/Task.php new file mode 100644 index 0000000..fdd5c45 --- /dev/null +++ b/modules/gdpr_tasks/src/Entity/Task.php @@ -0,0 +1,205 @@ + \Drupal::currentUser()->id(), + ]; + } + + /** + * {@inheritdoc} + */ + public function getCreatedTime() { + return $this->get('created')->value; + } + + /** + * {@inheritdoc} + */ + public function setCreatedTime($timestamp) { + $this->set('created', $timestamp); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getOwner() { + return $this->get('user_id')->entity; + } + + /** + * {@inheritdoc} + */ + public function getOwnerId() { + return $this->get('user_id')->target_id; + } + + /** + * {@inheritdoc} + */ + public function setOwnerId($uid) { + $this->set('user_id', $uid); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setOwner(UserInterface $account) { + $this->set('user_id', $account->id()); + return $this; + } + + /** + * {@inheritdoc} + */ + public function label() { + $label = t('Task @id', ['@id' => $this->id()]); + return $label; + } + + /** + * {@inheritdoc} + */ + public function getStatus() { + $statuses = $this->getStatuses(); + $value = $this->status->value; + if (isset($statuses[$value])) { + $value = $statuses[$value]; + } + return $value; + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields = parent::baseFieldDefinitions($entity_type); + + $fields['user_id'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Requested by')) + ->setDescription(t('The user who requested this task.')) + ->setRevisionable(TRUE) + ->setSetting('target_type', 'user') + ->setSetting('handler', 'default') + ->setTranslatable(TRUE) + ->setDisplayOptions('view', [ + 'label' => 'hidden', + 'type' => 'author', + 'weight' => 0, + ]) + ->setDisplayOptions('form', [ + 'type' => 'entity_reference_autocomplete', + 'weight' => 5, + 'settings' => [ + 'match_operator' => 'CONTAINS', + 'size' => '60', + 'autocomplete_type' => 'tags', + 'placeholder' => '', + ], + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + $fields['status'] = BaseFieldDefinition::create('list_string') + ->setLabel(t('Status')) + ->setRequired(TRUE) + ->setDefaultValue('requested') + ->setSetting('allowed_values_function', ['\Drupal\gdpr_tasks\Entity\Task', 'getStatuses']) + ->setDisplayOptions('form', [ + 'type' => 'hidden', + ]) + ->setDisplayConfigurable('view', TRUE) + ->setDisplayConfigurable('form', TRUE); + + $fields['created'] = BaseFieldDefinition::create('created') + ->setLabel(t('Created')) + ->setDescription(t('The time that the entity was created.')); + + $fields['changed'] = BaseFieldDefinition::create('changed') + ->setLabel(t('Changed')) + ->setDescription(t('The time that the entity was last edited.')); + + $fields['complete'] = BaseFieldDefinition::create('changed') + ->setLabel(t('Changed')) + ->setDescription(t('The time that the entity was last edited.')); + + return $fields; + } + + /** + * Gets the allowed values for the 'billing_countries' base field. + * + * @return array + * The allowed values. + */ + public static function getStatuses() { + return [ + 'requested' => 'Requested', + 'closed' => 'Closed', + ]; + } + +} diff --git a/modules/gdpr_tasks/src/Entity/TaskInterface.php b/modules/gdpr_tasks/src/Entity/TaskInterface.php new file mode 100644 index 0000000..18ca63c --- /dev/null +++ b/modules/gdpr_tasks/src/Entity/TaskInterface.php @@ -0,0 +1,43 @@ +entity; + + if ($entity->status->value == 'closed') { + $form['manual_data']['widget']['#disabled'] = TRUE; + $form['actions']['#access'] = FALSE; + } + + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + + if (isset($actions['delete'])) { + unset($actions['delete']); + } + + if (isset($actions['submit'])) { + $actions['submit']['#value'] = 'Process'; + } + + return $actions; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + + $entity = $this->entity; + $manual = $form_state->getValue(['manual_data', 0, 'value']); + + $data = gdpr_tasks_generate_sar_report($entity->getOwner()); + + $inc = []; + foreach ($data as $key => $values) { + $rta = $values['gdpr_rta']; + unset($values['gdpr_rta']); + if ($rta == 'inc') { + $inc[$key] = $values; + } + } + + $file_name = $entity->sar_export->entity->getFilename(); + $file_uri = $entity->sar_export->entity->getFileUri(); + $dirname = str_replace($file_name, '', $file_uri); + + /* @var \Drupal\gdpr_tasks\TaskManager $task_manager */ + $task_manager = \Drupal::service('gdpr_tasks.manager'); + $destination = $task_manager->toCSV($inc, $dirname); + $export = file_get_contents($destination); + + $export .= $manual; + + // @todo Add headers to csv export. + file_save_data($export, $file_uri, FILE_EXISTS_REPLACE); + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /* @var $entity \Drupal\gdpr_tasks\Entity\Task */ + $entity = $this->entity; + $entity->status = 'closed'; + + \Drupal::messenger()->addStatus('Task has been processed.'); + parent::save($form, $form_state); + } + +} diff --git a/modules/gdpr_tasks/src/Form/TaskDeleteForm.php b/modules/gdpr_tasks/src/Form/TaskDeleteForm.php new file mode 100644 index 0000000..62e9794 --- /dev/null +++ b/modules/gdpr_tasks/src/Form/TaskDeleteForm.php @@ -0,0 +1,15 @@ +entity; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $entity = $this->entity; + + $status = parent::save($form, $form_state); + + switch ($status) { + case SAVED_NEW: + drupal_set_message($this->t('Created the %label Task.', [ + '%label' => $entity->label(), + ])); + break; + + default: + drupal_set_message($this->t('Saved the %label Task.', [ + '%label' => $entity->label(), + ])); + } + $form_state->setRedirect('entity.gdpr_task.canonical', ['gdpr_task' => $entity->id()]); + } + +} diff --git a/modules/gdpr_tasks/src/Form/TaskSettingsForm.php b/modules/gdpr_tasks/src/Form/TaskSettingsForm.php new file mode 100644 index 0000000..f9c1edb --- /dev/null +++ b/modules/gdpr_tasks/src/Form/TaskSettingsForm.php @@ -0,0 +1,53 @@ +t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.gdpr_task_type.collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->delete(); + + drupal_set_message( + $this->t('content @type: deleted @label.', + [ + '@type' => $this->entity->bundle(), + '@label' => $this->entity->label(), + ] + ) + ); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/modules/gdpr_tasks/src/Form/TaskTypeForm.php b/modules/gdpr_tasks/src/Form/TaskTypeForm.php new file mode 100644 index 0000000..9ee8c57 --- /dev/null +++ b/modules/gdpr_tasks/src/Form/TaskTypeForm.php @@ -0,0 +1,65 @@ +entity; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $gdpr_task_type->label(), + '#description' => $this->t("Label for the Task type."), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $gdpr_task_type->id(), + '#machine_name' => [ + 'exists' => '\Drupal\gdpr_tasks\Entity\TaskType::load', + ], + '#disabled' => !$gdpr_task_type->isNew(), + ]; + + /* You will need additional form elements for your custom properties. */ + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $gdpr_task_type = $this->entity; + $status = $gdpr_task_type->save(); + + switch ($status) { + case SAVED_NEW: + drupal_set_message($this->t('Created the %label Task type.', [ + '%label' => $gdpr_task_type->label(), + ])); + break; + + default: + drupal_set_message($this->t('Saved the %label Task type.', [ + '%label' => $gdpr_task_type->label(), + ])); + } + $form_state->setRedirectUrl($gdpr_task_type->toUrl('collection')); + } + +} diff --git a/modules/gdpr_tasks/src/TaskAccessControlHandler.php b/modules/gdpr_tasks/src/TaskAccessControlHandler.php new file mode 100644 index 0000000..4294b08 --- /dev/null +++ b/modules/gdpr_tasks/src/TaskAccessControlHandler.php @@ -0,0 +1,44 @@ +id(); + + if ($settings_form_route = $this->getSettingsFormRoute($entity_type)) { + $collection->add("$entity_type_id.settings", $settings_form_route); + } + + return $collection; + } + + /** + * Gets the settings form route. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return \Symfony\Component\Routing\Route|null + * The generated route, if available. + */ + protected function getSettingsFormRoute(EntityTypeInterface $entity_type) { + if (!$entity_type->getBundleEntityType()) { + $route = new Route("/admin/structure/{$entity_type->id()}/settings"); + $route + ->setDefaults([ + '_form' => 'Drupal\gdpr_tasks\Form\TaskSettingsForm', + '_title' => "{$entity_type->getLabel()} settings", + ]) + ->setRequirement('_permission', $entity_type->getAdminPermission()) + ->setOption('_admin_route', TRUE); + + return $route; + } + } + +} diff --git a/modules/gdpr_tasks/src/TaskListBuilder.php b/modules/gdpr_tasks/src/TaskListBuilder.php new file mode 100644 index 0000000..61faf32 --- /dev/null +++ b/modules/gdpr_tasks/src/TaskListBuilder.php @@ -0,0 +1,157 @@ +get('entity_type.manager')->getStorage($entity_type->id()), + $container->get('entity_type.manager')->getStorage($entity_type->getBundleEntityType()) + ); + } + + /** + * Constructs a new EntityListBuilder object. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\Core\Entity\EntityStorageInterface $storage + * The entity storage class. + * @param \Drupal\Core\Entity\EntityStorageInterface $bundle_storage + * The entity bundle storage class. + */ + public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, EntityStorageInterface $bundle_storage) { + parent::__construct($entity_type, $storage); + $this->bundleStorage = $bundle_storage; + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header['id'] = $this->t('Task ID'); + $header['name'] = $this->t('Name'); + $header['user'] = $this->t('User'); + $header['type'] = $this->t('Type'); + $header['created'] = $this->t('Requested'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /* @var $entity \Drupal\gdpr_tasks\Entity\Task */ + $row['id'] = $entity->id(); + $row['name'] = Link::createFromRoute( + $entity->label(), + 'entity.gdpr_task.canonical', + ['gdpr_task' => $entity->id()] + ); + $row['user'] = $entity->getOwner()->toLink()->toString(); + $row['type'] = $this->bundleStorage->load($entity->bundle())->label(); + $row['created'] = DateTimePlus::createFromTimestamp($entity->getCreatedTime())->format('j/m/Y - H:m'); + + $date_formatter = \Drupal::service('date.formatter'); + $row['created'] .= ' - ' . $date_formatter->formatDiff($entity->getCreatedTime(), \Drupal::time()->getRequestTime(), [ + 'granularity' => 1, + ]) . ' ago'; + + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + protected function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + if ($entity->access('view') && $entity->hasLinkTemplate('canonical')) { + $operations['view'] = [ + 'title' => $this->t('View'), + 'weight' => 0, + 'url' => $this->ensureDestination($entity->toUrl()), + ]; + } + + return $operations; + } + + /** + * {@inheritdoc} + */ + public function render() { + $build['requested']['title'] = [ + '#type' => 'html_tag', + '#tag' => 'h3', + '#value' => 'Requested tasks', + ]; + + $build['requested']['table'] = [ + '#type' => 'table', + '#header' => $this->buildHeader(), + '#rows' => [], + '#empty' => $this->t('There is no open @label yet.', ['@label' => $this->entityType->getLabel()]), + '#cache' => [ + 'contexts' => $this->entityType->getListCacheContexts(), + 'tags' => $this->entityType->getListCacheTags(), + ], + ]; + + $build['closed']['title'] = [ + '#type' => 'html_tag', + '#tag' => 'h3', + '#value' => 'Closed tasks', + ]; + + $build['closed']['table'] = [ + '#type' => 'table', + '#header' => $this->buildHeader(), + '#rows' => [], + '#empty' => $this->t('There is no closed @label yet.', ['@label' => $this->entityType->getLabel()]), + '#cache' => [ + 'contexts' => $this->entityType->getListCacheContexts(), + 'tags' => $this->entityType->getListCacheTags(), + ], + ]; + foreach ($this->load() as $entity) { + if ($row = $this->buildRow($entity)) { + $build[$entity->status->value]['table']['#rows'][$entity->id()] = $row; + } + } + + // Only add the pager if a limit is specified. + if ($this->limit) { + $build['pager'] = [ + '#type' => 'pager', + ]; + } + + return $build; + } + +} diff --git a/modules/gdpr_tasks/src/TaskManager.php b/modules/gdpr_tasks/src/TaskManager.php new file mode 100644 index 0000000..b8368e5 --- /dev/null +++ b/modules/gdpr_tasks/src/TaskManager.php @@ -0,0 +1,99 @@ +entityTypeManager = $entity_type_manager; + $this->taskStorage = $entity_type_manager->getStorage('gdpr_task'); + $this->currentUser = $current_user; + } + + public function getUserTasks($account = NULL, $type = NULL) { + $tasks = []; + + if (!$account) { + $account = $this->currentUser->getAccount(); + } + + $query = $this->taskStorage->getQuery(); + $query->condition('user_id', $account->id(), '='); + + if ($type) { + $query->condition('type', $type, '='); + } + + if (!empty($ids = $query->execute())) { + $tasks = $this->taskStorage->loadMultiple($ids); + } + + return $tasks; + } + + /** + * Writes array data to a csv file. + * + * @param array $data + * The data to be stored in csv. + * @param string $dirname + * The local path or stream wrapper for destination directory. + * + * @return string + * The uri path of the created file. + */ + public function toCSV($data, $dirname = 'private://') { + // Prepare destination. + file_prepare_directory($dirname, FILE_CREATE_DIRECTORY); + + // Generate a file entity. + $random = new Random(); + $destination = $dirname . '/' . $random->name(10, TRUE) . '.csv'; + + // Update csv with actual data. + $fp = fopen($destination, 'w'); + foreach($data as $line){ + fputcsv($fp, $line); + } + fclose($fp); + + return $destination; + } + +} diff --git a/modules/gdpr_tasks/src/TaskTypeHtmlRouteProvider.php b/modules/gdpr_tasks/src/TaskTypeHtmlRouteProvider.php new file mode 100644 index 0000000..8c6ef20 --- /dev/null +++ b/modules/gdpr_tasks/src/TaskTypeHtmlRouteProvider.php @@ -0,0 +1,28 @@ +t('Task type'); + $header['id'] = $this->t('Machine name'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $entity->label(); + $row['id'] = $entity->id(); + // You probably want a few more properties here... + return $row + parent::buildRow($entity); + } + +} diff --git a/modules/gdpr_tasks/templates/gdpr-task-content-add-list.html.twig b/modules/gdpr_tasks/templates/gdpr-task-content-add-list.html.twig new file mode 100644 index 0000000..311dd7a --- /dev/null +++ b/modules/gdpr_tasks/templates/gdpr-task-content-add-list.html.twig @@ -0,0 +1,23 @@ +{# +/** + * @file + * Default theme implementation to present a list of custom content entity types/bundles. + * + * Available variables: + * - types: A collection of all the available custom entity types/bundles. + * Each type/bundle contains the following: + * - link: A link to add a content entity of this type. + * - description: A description of this content entity types/bundle. + * + * @see template_preprocess_gdpr_task_content_add_list() + * + * @ingroup themeable + */ +#} +{% spaceless %} +
+ {% for type in types %} +
{{ type.link }}
+ {% endfor %} +
+{% endspaceless %} diff --git a/modules/gdpr_tasks/templates/gdpr_task.html.twig b/modules/gdpr_tasks/templates/gdpr_task.html.twig new file mode 100644 index 0000000..eb35a07 --- /dev/null +++ b/modules/gdpr_tasks/templates/gdpr_task.html.twig @@ -0,0 +1,44 @@ +{# +/** + * @file gdpr_task.html.twig + * Default theme implementation to present Task data. + * + * This template is used when viewing Task pages. + * + * + * Available variables: + * - content: A list of content items. Use 'content' to print all content, or + * - attributes: HTML attributes for the container element. + * + * @see template_preprocess_gdpr_task() + * + * @ingroup themeable + */ +#} + +{% if content %} +
+ Task Summary +
+ + Type: {{ type }} + {{- content -}} +
+ +
+{% endif %} + +{% if data %} +
+ Data +
+ + {{ data }} +
+ +
+{% endif %} + +{% if actions %} + {{ actions }} +{% endif %}