diff --git a/composer.json b/composer.json index ace483f..449f2a9 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,9 @@ ], "require": { "drupal/checklistapi": "^1.0", - "drupal/entity_reference_revisions": "^1.4" + "drupal/entity_reference_revisions": "^1.4", + "drupal/message": "^1.0@RC", + "drupal/token": "^1.1", + "drupal/views_data_export": "^1.0@alpha" } } diff --git a/gdpr.info.yml b/gdpr.info.yml index d0f4226..2dc9fa8 100644 --- a/gdpr.info.yml +++ b/gdpr.info.yml @@ -6,5 +6,5 @@ configure: checklistapi.checklists.gdpr_checklist package: General Data Protection Regulation dependencies: - - drupal:checklistapi - - drupal:message \ No newline at end of file + - checklistapi:checklistapi + - message:message diff --git a/gdpr.module b/gdpr.module index 604fa69..dbe926d 100644 --- a/gdpr.module +++ b/gdpr.module @@ -489,7 +489,7 @@ function gdpr_toolbar() { // Just add the root menu item. // GDPR Tasks and GDPR Consent modules // will modify this in hook_toolbar_alter. - if ($user->hasPermission('view gdpr tasks') || $user->hasPermission('manage gdpr agreements')) { + if ($user->hasPermission('view task entities') || $user->hasPermission('manage gdpr agreements')) { $items['gdpr'] += [ '#type' => 'toolbar_item', 'tab' => [ diff --git a/gdpr.routing.yml b/gdpr.routing.yml index 951f5ef..be322fb 100644 --- a/gdpr.routing.yml +++ b/gdpr.routing.yml @@ -4,7 +4,7 @@ gdpr.collected_user_data: _title: 'Data stored about you' _controller: '\Drupal\gdpr\Controller\UserController::collectedData' requirements: - _custom_access: '\Drupal\gdpr\Controller\UserController::access' + _custom_access: '\Drupal\gdpr\Controller\UserController::accessCollectedData' options: parameters: user: diff --git a/modules/gdpr_consent/gdpr_consent.info.yml b/modules/gdpr_consent/gdpr_consent.info.yml index 0c26ccc..3acd6b0 100644 --- a/modules/gdpr_consent/gdpr_consent.info.yml +++ b/modules/gdpr_consent/gdpr_consent.info.yml @@ -6,6 +6,6 @@ package: General Data Protection Regulation dependencies: - gdpr:gdpr - - token - - message - - entity_reference_revisions \ No newline at end of file + - token:token + - message:message + - entity_reference_revisions:entity_reference_revisions diff --git a/modules/gdpr_consent/src/Controller/ConsentAgreementController.php b/modules/gdpr_consent/src/Controller/ConsentAgreementController.php index a66a82b..de5d96c 100644 --- a/modules/gdpr_consent/src/Controller/ConsentAgreementController.php +++ b/modules/gdpr_consent/src/Controller/ConsentAgreementController.php @@ -97,7 +97,7 @@ public function revisionOverview($gdpr_consent_agreement) { $account = $this->currentUser(); $storage = $this->entityManager()->getStorage('gdpr_consent_agreement'); - $build['#title'] = $this->t('Revisions for %title', ['%title' => $agreement->label()]); + $build['#title'] = $this->t('Revisions for %title', ['%title' => $agreement->title->value]); $header = [$this->t('Revision'), $this->t('Operations')]; $revert_permission = $account->hasPermission('create gdpr agreements'); diff --git a/modules/gdpr_consent/src/Entity/ConsentAgreement.php b/modules/gdpr_consent/src/Entity/ConsentAgreement.php index e200520..3d91f77 100644 --- a/modules/gdpr_consent/src/Entity/ConsentAgreement.php +++ b/modules/gdpr_consent/src/Entity/ConsentAgreement.php @@ -256,6 +256,20 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDisplayConfigurable('form', TRUE) ->setDisplayConfigurable('view', TRUE); + $fields['notes'] = BaseFieldDefinition::create('string_long') + ->setLabel(t('Notes')) + ->setRevisionable(TRUE) + ->setDescription(t('This should contain the rationale behind the agreement.')) + ->setDisplayOptions('view', [ + 'type' => 'textarea', + ]) + ->setDisplayOptions('form', [ + 'type' => 'textarea', + 'weight' => 4, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + $fields['status'] = BaseFieldDefinition::create('boolean') ->setLabel(t('Publishing status')) ->setDescription(t('A boolean indicating whether the Consent Agreement is published.')) @@ -317,5 +331,4 @@ public function __toString() { return $this->get('title')->getString(); } - -} \ No newline at end of file +} diff --git a/modules/gdpr_dump/gdpr_dump.info.yml b/modules/gdpr_dump/gdpr_dump.info.yml index bf17fbf..dc541b2 100644 --- a/modules/gdpr_dump/gdpr_dump.info.yml +++ b/modules/gdpr_dump/gdpr_dump.info.yml @@ -6,4 +6,4 @@ configure: gdpr_dump.settings package: General Data Protection Regulation dependencies: - - drupal:gdpr + - gdpr:gdpr diff --git a/modules/gdpr_fields/config/schema/gdpr_field.schema.yml b/modules/gdpr_fields/config/schema/gdpr_field.schema.yml index fca48dd..21681bc 100644 --- a/modules/gdpr_fields/config/schema/gdpr_field.schema.yml +++ b/modules/gdpr_fields/config/schema/gdpr_field.schema.yml @@ -7,4 +7,4 @@ field.field.*.*.*.third_party.gdpr_fields: 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 + label: 'Whether this field is user sensitive or not.' diff --git a/modules/gdpr_fields/config/schema/gdpr_fields.schema.yml b/modules/gdpr_fields/config/schema/gdpr_fields.schema.yml new file mode 100644 index 0000000..9df259e --- /dev/null +++ b/modules/gdpr_fields/config/schema/gdpr_fields.schema.yml @@ -0,0 +1,30 @@ +gdpr_fields.gdpr_fields_config.*: + type: config_entity + label: 'GDPR Field Config' + mapping: + id: + type: string + label: 'ID' + bundles: + type: sequence + label: 'Entity Bundles' + sequence: + type: sequence + label: 'Fields' + sequence: + type: mapping + mapping: + bundle: + type: string + name: + type: string + enabled: + type: boolean + rta: + type: string + rtf: + type: string + sanitizer: + type: string + notes: + type: string diff --git a/modules/gdpr_fields/css/field-list.css b/modules/gdpr_fields/css/field-list.css new file mode 100644 index 0000000..98d8d44 --- /dev/null +++ b/modules/gdpr_fields/css/field-list.css @@ -0,0 +1,24 @@ +.notes:before { + content: attr(data-icon); + font-weight: bold; + display: inline-block; + width: 1em; + font-size: 2em; +} + +.notes + div { + float: right; + margin-top: -3em; + position: absolute; + z-index: 3; + display: none; + border: 1px solid #ccc; + background: #fff; + padding: 10px; + min-width: 200px; +} + +.notes:hover + div, +.notes + div:hover { + display: inline-block; +} \ No newline at end of file diff --git a/modules/gdpr_fields/gdpr_fields.libraries.yml b/modules/gdpr_fields/gdpr_fields.libraries.yml new file mode 100644 index 0000000..04b780f --- /dev/null +++ b/modules/gdpr_fields/gdpr_fields.libraries.yml @@ -0,0 +1,4 @@ +field-list: + css: + theme: + css/field-list.css: {} \ No newline at end of file diff --git a/modules/gdpr_fields/gdpr_fields.module b/modules/gdpr_fields/gdpr_fields.module index 72f0af5..6d35cec 100644 --- a/modules/gdpr_fields/gdpr_fields.module +++ b/modules/gdpr_fields/gdpr_fields.module @@ -7,6 +7,8 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; +use Drupal\gdpr_fields\Entity\GdprFieldConfigEntity; +use Drupal\gdpr_fields\Form\GdprFieldSettingsForm; /** * Implements hook_toolbar_alter(). @@ -52,72 +54,17 @@ function gdpr_fields_form_field_config_edit_form_alter(&$form, FormStateInterfac '#open' => TRUE, ]; - $form['field']['gdpr_fields']['gdpr_fields_enabled'] = [ - '#type' => 'checkbox', - '#title' => t('This is a GDPR field'), - '#default_value' => $field->getThirdPartySetting('gdpr_fields', 'gdpr_fields_enabled', FALSE), - ]; - - $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' => [ - 'visible' => [ - ':input[name="gdpr_fields_enabled"]' => [ - 'checked' => TRUE, - ], - ], - ], - ]; - - $form['field']['gdpr_fields']['gdpr_fields_rtf'] = [ - '#type' => 'select', - '#title' => t('Right to be forgotten'), - '#options' => [ - 'anonymise' => 'Anonymise', - 'remove' => 'Remove', - 'maybe' => 'Maybe', - 'no' => 'Not', - ], - '#default_value' => $field->getThirdPartySetting('gdpr_fields', 'gdpr_fields_rtf', 'no'), - '#states' => [ - 'visible' => [ - ':input[name="gdpr_fields_enabled"]' => [ - 'checked' => TRUE, - ], - ], - ], - ]; - - $sanitizer_options = ['' => ''] + array_map(function ($s) { - return $s['label']; - }, $sanitizer_definitions); + GdprFieldSettingsForm::buildFormFields($form['field']['gdpr_fields'], $field->getTargetEntityTypeId(), $field->getTargetBundle(), $field->getName()); - $form['field']['gdpr_fields']['gdpr_fields_sanitizer'] = [ - '#type' => 'select', - '#title' => t('Sanitizer to use'), - '#options' => $sanitizer_options, - '#default_value' => $field->getThirdPartySetting('gdpr_fields', 'gdpr_fields_sanitizer', ''), - '#states' => [ - 'visible' => [ - ':input[name="gdpr_fields_enabled"]' => ['checked' => TRUE], - ':input[name="gdpr_fields_rtf"]' => ['value' => 'anonymise'], - ], - ], - ]; - $form['actions']['submit']['#submit'][] = 'gdpr_fields_form_field_config_edit_form_submit'; } /** * Form submission handler for gdpr_fields_form_field_config_edit_form_alter. * + * This version of the GDPR fields form is embedded on the standard field + * settings page. + * * @param array $form * The form array. * @param \Drupal\Core\Form\FormStateInterface $form_state @@ -128,21 +75,16 @@ function gdpr_fields_form_field_config_edit_form_submit(array $form, FormStateIn $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->setThirdPartySetting('gdpr_fields', 'gdpr_fields_sanitizer', $form_fields['gdpr_fields_sanitizer']); - $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->unsetThirdPartySetting('gdpr_fields', 'gdpr_fields_sanitizer'); - - $field->save(); - } - + $config = GdprFieldConfigEntity::load($field->getTargetEntityTypeId()) ?? GdprFieldConfigEntity::create(['id' => $field->getTargetEntityTypeId()]); + $config->setField( + $field->getTargetBundle(), + $field->getName(), [ + 'enabled' => $form_state->getValue('gdpr_enabled'), + 'rta' => $form_state->getValue('gdpr_rta'), + 'rtf' => $form_state->getValue('gdpr_rtf'), + 'sanitizer' => $form_state->getValue('gdpr_sanitizer'), + 'notes' => $form_state->getValue('gdpr_notes'), + ] + ); + $config->save(); } diff --git a/modules/gdpr_fields/gdpr_fields.routing.yml b/modules/gdpr_fields/gdpr_fields.routing.yml index 0b79a36..b0df8b9 100644 --- a/modules/gdpr_fields/gdpr_fields.routing.yml +++ b/modules/gdpr_fields/gdpr_fields.routing.yml @@ -1,7 +1,15 @@ gdpr_fields.fields_list: - path: '/admin/reports/fields/gdpr-fields' + path: '/admin/reports/fields/gdpr-fields/{mode}' defaults: _controller: '\Drupal\gdpr_fields\Controller\GDPRController::fieldsList' _title: 'Used in GDPR' + mode: 'all' requirements: _permission: 'view gdpr fields' + +gdpr_fields.edit_field: + path: '/admin/gdpr/fields/{entity_type}/{bundle_name}/{field_name}' + defaults: + _form: 'Drupal\gdpr_fields\Form\GdprFieldSettingsForm' + requirements: + _permission: 'view gdpr fields' \ No newline at end of file diff --git a/modules/gdpr_fields/src/Controller/GDPRController.php b/modules/gdpr_fields/src/Controller/GDPRController.php index f5a1586..9b89288 100644 --- a/modules/gdpr_fields/src/Controller/GDPRController.php +++ b/modules/gdpr_fields/src/Controller/GDPRController.php @@ -46,37 +46,37 @@ public static function create(ContainerInterface $container) { * @return array * The Views plugins report page. */ - public function fieldsList() { + public function fieldsList($mode) { $output = []; $entities = []; + $include_not_configured = $mode == 'all'; $this->collector->getEntities($entities); + $output['filter'] = $this->formBuilder()->getForm('Drupal\gdpr_fields\Form\GdprFieldFilterForm'); + $output['#attached']['library'][] = 'gdpr_fields/field-list'; + + foreach ($entities as $entity_type => $bundles) { - $output[$entity_type] = array( + $output[$entity_type] = [ '#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( + $output[$entity_type][$bundle_id] = [ '#type' => 'details', '#title' => t($bundle_id), '#open' => TRUE, - ); - $output[$entity_type][$bundle_id]['fields'] = $this->buildFieldTable($entity_type, $bundle_id); + ]; + $output[$entity_type][$bundle_id]['fields'] = $this->buildFieldTable($entity_type, $bundle_id, $include_not_configured); } } 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); + $output[$entity_type][$bundle_id]['fields'] = $this->buildFieldTable($entity_type, $bundle_id, $include_not_configured); } } @@ -94,18 +94,49 @@ public function fieldsList() { * @return array * Renderable array for field list table. */ - protected function buildFieldTable($entity_type, $bundle_id) { - $rows = $this->collector->listFields($entity_type, $bundle_id); + protected function buildFieldTable($entity_type, $bundle_id, $include_not_configured) { + $rows = $this->collector->listFields($entity_type, $bundle_id, $include_not_configured); // Sort rows by field name. ksort($rows); - return [ + $table = [ '#type' => 'table', - '#header' => [t('Name'), t('Type'), t('Right to access'), t('Right to be forgotten'), ''], - '#rows' => $rows, + '#header' => [t('Name'), t('Type'), t('Right to access'), t('Right to be forgotten'), t('Notes'), ''], + // '#rows' => $rows, '#sticky' => TRUE, '#empty' => t('There are no GDPR fields for this entity.'), ]; + + $i = 0; + foreach ($rows as $row) { + $table[$i]['title'] = [ + '#plain_text' => $row['title'], + ]; + + $table[$i]['type'] = [ + '#plain_text' => $row['type'], + ]; + + $table[$i]['gdpr_rta'] = [ + '#plain_text' => $row['gdpr_rta'], + ]; + + $table[$i]['gdpr_rtf'] = [ + '#plain_text' => $row['gdpr_rtf'], + ]; + + $table[$i]['notes'] = [ + '#markup' => empty($row['notes']) ? '' : '
' . $row['notes'] . '
', + ]; + + $table[$i]['edit'] = [ + '#markup' => !empty($row['edit']) ? $row['edit']->toString() : '', + ]; + + $i++; + } + + return $table; } /** diff --git a/modules/gdpr_fields/src/Entity/GdprField.php b/modules/gdpr_fields/src/Entity/GdprField.php new file mode 100644 index 0000000..ad7147b --- /dev/null +++ b/modules/gdpr_fields/src/Entity/GdprField.php @@ -0,0 +1,150 @@ +bundle = $bundle; + $this->name = $name; + } + + /** + * Creates a GdprField instance based on array data from the config entity. + * + * @param array $values + * The undlerying data. + * + * @return \Drupal\gdpr_fields\Entity\GdprField + * The field metadata instance. + */ + public static function create(array $values) { + $field = new static($values['bundle'], $values['name']); + $field->rtf = $values['rtf']; + $field->rta = $values['rta']; + $field->enabled = $values['enabled']; + $field->sanitizer = $values['sanitizer']; + $field->notes = $values['notes']; + $field->configured = TRUE; + return $field; + } + + /** + * Gets the RTF description. + * + * @return string + * The description. + */ + public function rtfDescription() { + switch ($this->rtf) { + case 'anonymise': + return 'Anonymise'; + + case 'remove': + return 'Remove'; + + case 'maybe': + return 'Maybe'; + + case 'no': + return 'Not Included'; + + default: + return 'Not Configured'; + + } + } + + /** + * Gets the RTA description. + * + * @return string + * The description. + */ + public function rtaDescription() { + switch ($this->rta) { + case 'inc': + return 'Included'; + + case 'maybe': + return 'Maybe'; + + case 'no': + return 'Not Included'; + + default: + return 'Not Configured'; + + } + } +} \ No newline at end of file diff --git a/modules/gdpr_fields/src/Entity/GdprFieldConfigEntity.php b/modules/gdpr_fields/src/Entity/GdprFieldConfigEntity.php new file mode 100644 index 0000000..b844495 --- /dev/null +++ b/modules/gdpr_fields/src/Entity/GdprFieldConfigEntity.php @@ -0,0 +1,93 @@ + $value) { + $this->bundles[$bundle][$field_name][$key] = $value; + } + + return $this; + } + + /** + * Gets field metadata. + * + * @param string $bundle + * Bundle. + * @param string $field_name + * Field name. + * + * @return \Drupal\gdpr_fields\Entity\GdprField + * Field metadata. + */ + public function getField($bundle, $field_name) { + $bundle_fields = array_filter($this->bundles, function ($key) use ($bundle) { + return $key == $bundle; + }, ARRAY_FILTER_USE_KEY); + + if (count($bundle_fields)) { + $result = array_filter($bundle_fields[$bundle], function ($f) use ($field_name) { + return $f['name'] == $field_name; + }); + + if (count($result)) { + return GdprField::create(reset($result)); + } + } + + return new GdprField($bundle, $field_name); + } + +} diff --git a/modules/gdpr_fields/src/Form/GdprFieldFilterForm.php b/modules/gdpr_fields/src/Form/GdprFieldFilterForm.php new file mode 100644 index 0000000..45aa94e --- /dev/null +++ b/modules/gdpr_fields/src/Form/GdprFieldFilterForm.php @@ -0,0 +1,49 @@ +getParameter('mode') == 'all' ? 1 : 0; + + $form['filter'] = [ + '#type' => 'checkbox', + '#title' => 'Include Not Configured', + '#default_value' => $filter_value, + ]; + + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Apply'), + '#button_type' => 'primary', + '#name' => 'Apply', + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $mode = $form_state->getValue('filter') == '1' ? 'all' : 'configured'; + $form_state->setRedirect('gdpr_fields.fields_list', ['mode' => $mode]); + } +} \ No newline at end of file diff --git a/modules/gdpr_fields/src/Form/GdprFieldSettingsDeleteForm.php b/modules/gdpr_fields/src/Form/GdprFieldSettingsDeleteForm.php new file mode 100644 index 0000000..e3c64d7 --- /dev/null +++ b/modules/gdpr_fields/src/Form/GdprFieldSettingsDeleteForm.php @@ -0,0 +1,27 @@ +t('Are you sure you want to remove the GDPR settings from this field?'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('gdpr_fields.fields_list'); + } +} \ No newline at end of file diff --git a/modules/gdpr_fields/src/Form/GdprFieldSettingsForm.php b/modules/gdpr_fields/src/Form/GdprFieldSettingsForm.php new file mode 100644 index 0000000..f803362 --- /dev/null +++ b/modules/gdpr_fields/src/Form/GdprFieldSettingsForm.php @@ -0,0 +1,307 @@ +entityFieldManager = $entity_field_manager; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_field.manager'), + $container->get('entity_type.manager') + ); + } + + /** + * Gets the configuration for an entity/bundle/field. + * + * @param string $entity_type + * Entity type. + * @param string $bundle + * Bundle. + * @param string $field_name + * Field. + * + * @return \Drupal\gdpr_fields\Entity\GdprField + * Field metadata. + */ + private static function getConfig($entity_type, $bundle, $field_name): GdprField { + $config = GdprFieldConfigEntity::load($entity_type) ?? GdprFieldConfigEntity::create(['id' => $entity_type]); + $field_config = $config->getField($bundle, $field_name); + return $field_config; + } + + /** + * Sets the GDPR settings for a field. + * + * @param string $entity_type + * Entity type. + * @param string $bundle + * Bundle. + * @param string $field_name + * Field. + * @param boolean $enabled + * Whether GDPR is enabled for this field + * @param string $rta + * Right to Access setting + * @param string $rtf + * Right to be forgotten + * @param string $sanitizer + * Sanitizer to use. + * @param string $notes + * Notes + * + * @return \Drupal\gdpr_fields\Entity\GdprFieldConfigEntity + * The config entity. + */ + private static function setConfig($entity_type, $bundle, $field_name, $enabled, $rta, $rtf, $sanitizer, $notes) { + $config = GdprFieldConfigEntity::load($entity_type) ?? GdprFieldConfigEntity::create(['id' => $entity_type]); + $config->setField($bundle, $field_name, [ + 'enabled' => $enabled, + 'rta' => $rta, + 'rtf' => $rtf, + 'sanitizer' => $sanitizer, + 'notes' => $notes, + ]); + return $config; + } + + /** + * Returns a unique string identifying the form. + * + * @return string + * The unique string identifying the form. + */ + public function getFormId() { + return 'gdpr_fields_edit_field_form'; + } + + /** + * Form constructor. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + * The form structure. + */ + public function buildForm(array $form, FormStateInterface $form_state, $entity_type = NULL, $bundle_name = NULL, $field_name = NULL) { + if (empty($entity_type) || empty($bundle_name) || empty($field_name)) { + $this->messenger()->addWarning('Could not load field.'); + return []; + } + + $field_defs = $this->entityFieldManager->getFieldDefinitions($entity_type, $bundle_name); + + if (!array_key_exists($field_name, $field_defs)) { + $this->messenger()->addWarning("The field $field_name does not exist."); + return []; + } + $field_def = $field_defs[$field_name]; + $form['#title'] = 'GDPR Settings for ' . $field_def->getLabel(); + + static::buildFormFields($form, $entity_type, $bundle_name, $field_name); + + + $form['entity_type'] = [ + '#type' => 'hidden', + '#default_value' => $entity_type, + ]; + + $form['bundle'] = [ + '#type' => 'hidden', + '#default_value' => $bundle_name, + ]; + + $form['field_name'] = [ + '#type' => 'hidden', + '#default_value' => $field_name, + ]; + + $form['actions'] = [ + '#type' => 'actions', + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#button_type' => 'primary', + '#name' => 'Save', + ], + 'submit_cancel' => [ + '#type' => 'submit', + '#weight' => 99, + '#value' => $this->t('Cancel'), + '#name' => 'Cancel', + '#limit_validation_errors' => [], + ], + ]; + + return $form; + } + + /** + * Builds the form fields for GDPR settings. + * + * This is in a separate method so it can also be attached to the regular + * field settings page by hook. + * + * @see gdpr_fields_form_field_config_edit_form_submit + * + * @param array $form + * Form + * @param string $entity_type + * Entity type + * @param string $bundle_name + * Bundle + * @param string $field_name + * Field + */ + public static function buildFormFields(array &$form, $entity_type = NULL, $bundle_name = NULL, $field_name = NULL) { + $config = static::getConfig($entity_type, $bundle_name, $field_name); + + /* @var \Drupal\gdpr_dump\Sanitizer\GdprSanitizerFactory $sanitizer_factory */ + $sanitizer_factory = \Drupal::service('gdpr_dump.sanitizer_factory'); + $sanitizer_definitions = $sanitizer_factory->getDefinitions(); + + $form['gdpr_enabled'] = [ + '#type' => 'checkbox', + '#title' => t('This is a GDPR field'), + '#default_value' => $config->enabled, + ]; + + $form['gdpr_rta'] = [ + '#type' => 'select', + '#title' => t('Right to access'), + '#options' => [ + 'inc' => 'Included', + 'maybe' => 'Maybe', + 'no' => 'Not Included', + ], + '#default_value' => $config->rta, + '#states' => [ + 'visible' => [ + ':input[name="gdpr_enabled"]' => [ + 'checked' => TRUE, + ], + ], + ], + ]; + + $form['gdpr_rtf'] = [ + '#type' => 'select', + '#title' => t('Right to be forgotten'), + '#options' => [ + 'anonymise' => 'Anonymise', + 'remove' => 'Remove', + 'maybe' => 'Maybe', + 'no' => 'Not Included', + ], + '#default_value' => $config->rtf, + '#states' => [ + 'visible' => [ + ':input[name="gdpr_enabled"]' => [ + 'checked' => TRUE, + ], + ], + ], + ]; + + $sanitizer_options = ['' => ''] + array_map(function ($s) { + return $s['label']; + }, $sanitizer_definitions); + + $form['gdpr_sanitizer'] = [ + '#type' => 'select', + '#title' => t('Sanitizer to use'), + '#options' => $sanitizer_options, + '#default_value' => $config->sanitizer, + '#states' => [ + 'visible' => [ + ':input[name="gdpr_enabled"]' => ['checked' => TRUE], + ':input[name="gdpr_rtf"]' => ['value' => 'anonymise'], + ], + ], + ]; + + $form['gdpr_notes'] = [ + '#type' => 'textarea', + '#title' => 'Notes', + '#default_value' => $config->notes, + '#states' => [ + 'visible' => [ + ':input[name="gdpr_enabled"]' => [ + 'checked' => TRUE, + ], + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + if ($form_state->getTriggeringElement()['#name'] == 'Cancel') { + $form_state->setRedirect('gdpr_fields.fields_list'); + return; + } + + $config = static::setConfig( + $form_state->getValue('entity_type'), + $form_state->getValue('bundle'), + $form_state->getValue('field_name'), + $form_state->getValue('gdpr_enabled'), + $form_state->getValue('gdpr_rta'), + $form_state->getValue('gdpr_rtf'), + $form_state->getValue('gdpr_sanitizer'), + $form_state->getValue('gdpr_notes') + ); + + $config->save(); + \Drupal::messenger()->addMessage('Field settings saved.'); + $form_state->setRedirect('gdpr_fields.fields_list'); + } + +} \ No newline at end of file diff --git a/modules/gdpr_fields/src/GDPRCollector.php b/modules/gdpr_fields/src/GDPRCollector.php index 256ec95..881159a 100644 --- a/modules/gdpr_fields/src/GDPRCollector.php +++ b/modules/gdpr_fields/src/GDPRCollector.php @@ -10,6 +10,7 @@ use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\Url; use Drupal\ctools\Plugin\RelationshipManager; +use Drupal\gdpr_fields\Entity\GdprFieldConfigEntity; /** * Defines a helper class for stuff related to views data. @@ -135,9 +136,14 @@ public function getValueEntities(array &$entity_list, $entity_type, EntityInterf $entity_list[$entity_type][$entity->id()] = $entity; // Find relationships. - $context = new Context(new ContextDefinition("entity:{$entity_type}")); - $definitions = $this->relationshipManager->getDefinitionsForContexts([$context]); + $context_definition = new ContextDefinition("entity:{$entity_type}"); + // @todo Error handling for broken bundles. (Eg. file module). + if ($entity->bundle() != 'undefined') { + $context_definition->addConstraint('Bundle', [$entity->bundle()]); + } + $context = new Context($context_definition); + $definitions = $this->relationshipManager->getDefinitionsForContexts([$context]); foreach ($definitions as $definition_id => $definition) { list($type, $definition_entity, $related_entity_type,) = explode(':', $definition_id); @@ -149,7 +155,7 @@ public function getValueEntities(array &$entity_list, $entity_type, EntityInterf // Ignore links back to gdpr_task. // @todo Remove this once we have solved how to deal with ignored/excluded relationships - if ($related_entity_type == 'gdpr_task') { + if ($related_entity_type == 'gdpr_task' || $related_entity_type == 'message') { continue; } @@ -192,10 +198,11 @@ public function getValueEntities(array &$entity_list, $entity_type, EntityInterf * @return array * GDPR entity field list. */ - public function listFields($entity_type = 'user', $bundle_id) { + public function listFields($entity_type = 'user', $bundle_id, $include_not_configured) { $storage = $this->entityTypeManager->getStorage($entity_type); $entity_definition = $this->entityTypeManager->getDefinition($entity_type); $bundle_type = $entity_definition->getBundleEntityType(); + $gdpr_settings = GdprFieldConfigEntity::load($entity_type); // Create a blank entity. $values = []; @@ -207,13 +214,20 @@ public function listFields($entity_type = 'user', $bundle_id) { // Get fields for entity. $fields = []; + + if (!$include_not_configured && $gdpr_settings == NULL) { + return $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_name = 'gdpr_fields.edit_field'; $route_params = [ - 'field_config' => $key, + 'entity_type' => $entity_type, + 'bundle_name' => $bundle_id, + 'field_name' => $field_id, ]; if (isset($bundle_key)) { @@ -223,8 +237,9 @@ public function listFields($entity_type = 'user', $bundle_id) { $fields[$key] = [ 'title' => $field_definition->getLabel(), 'type' => $field_definition->getType(), - 'gdpr_rta' => 'None', - 'gdpr_rtf' => 'None', + 'gdpr_rta' => 'Not Configured', + 'gdpr_rtf' => 'Not Configured', + 'notes' => '', 'edit' => '', ]; @@ -235,11 +250,17 @@ public function listFields($entity_type = 'user', $bundle_id) { $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'); + if ($gdpr_settings != NULL) { + $field_settings = $gdpr_settings->getField($bundle_id, $field_id); + if ($field_settings->configured) { + $fields[$key]['gdpr_rta'] = $field_settings->rtaDescription(); + $fields[$key]['gdpr_rtf'] = $field_settings->rtfDescription(); + $fields[$key]['notes'] = $field_settings->notes; + } + elseif (!$field_settings->configured && !$include_not_configured) { + unset($fields[$key]); + } } } @@ -272,19 +293,26 @@ public function fieldValues($entity_type = 'user', EntityInterface $entity, $ext $bundle_label = $entity->getEntityType()->getLabel(); } - // Get fields for entity. $fields = []; + + $gdpr_config = GdprFieldConfigEntity::load($entity_type); + + if ($gdpr_config == NULL) { + // No fields have been configured on this entity for GDPR. + return $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)) { + $field_config = $gdpr_config->getField($bundle_id, $field->getName()); + + if (!$field_config->enabled) { continue; } - $key = "$entity_type.{$entity->id()}.$field_id"; $fieldValue = $field->getString(); @@ -293,6 +321,7 @@ public function fieldValues($entity_type = 'user', EntityInterface $entity, $ext 'value' => $fieldValue, 'entity' => $entity->getEntityType()->getLabel(), 'bundle' => $bundle_label, + 'notes' => $field_config->notes, ]; if (empty($extra_fields)) { @@ -301,20 +330,23 @@ public function fieldValues($entity_type = 'user', EntityInterface $entity, $ext // Fetch and validate based on field settings. if (isset($extra_fields['rta'])) { - $rta_value = $config->getThirdPartySetting('gdpr_fields', 'gdpr_fields_rta', FALSE); + $rta_value = $field_config->rta; if ($rta_value && $rta_value !== 'no') { $fields[$key]['gdpr_rta'] = $rta_value; + //$fields[$key]['gdpr_rta_desc'] = $field_config->rtaDescription(); } else { unset($fields[$key]); } } if (isset($extra_fields['rtf'])) { - $rtf_value = $config->getThirdPartySetting('gdpr_fields', 'gdpr_fields_rtf', FALSE); + $rtf_value = $field_config->rtf; if ($rtf_value && $rtf_value !== 'no') { $fields[$key]['gdpr_rtf'] = $rtf_value; + //$fields[$key]['gdpr_rtf_desc'] = $field_config->rtfDescription(); + // For 'maybes', provide a link to edit the entity. if ($rtf_value == 'maybe') { $fields[$key]['link'] = $entity->toLink('Edit', 'edit-form'); 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 index 878976a..5c1e341 100644 --- 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 @@ -1,3 +1,4 @@ +uuid: 5b759d60-61ce-4d87-82f4-6b81433c1ca3 langcode: en status: true dependencies: @@ -8,23 +9,39 @@ dependencies: - gdpr_tasks - options - user +_core: + default_config_hash: _19d0Pj6VIDv7zxT310E2eIpMDepNswpF3BBp07sH6k id: gdpr_task.gdpr_remove.default targetEntityType: gdpr_task bundle: gdpr_remove mode: default content: + notes: + type: basic_string + region: content + weight: 3 + label: inline + settings: { } + third_party_settings: { } processed_by: label: inline type: author - weight: 2 + weight: 4 region: content settings: { } third_party_settings: { } removal_log: type: gdpr_task_item - weight: 3 + weight: 5 + region: content + label: inline + settings: { } + third_party_settings: { } + requested_by: + label: inline + type: author + weight: 2 region: content - label: above settings: { } third_party_settings: { } status: 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 index 77dec62..c013d1d 100644 --- 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 @@ -1,3 +1,4 @@ +uuid: 5a0777c8-0adc-4245-b1d2-9361cfea7062 langcode: en status: true dependencies: @@ -9,11 +10,34 @@ dependencies: - file - options - user +_core: + default_config_hash: Iqo94n1T957Bb8Qw1BPLcM679fLMl83eGGGZsiqEvOg id: gdpr_task.gdpr_sar.default targetEntityType: gdpr_task bundle: gdpr_sar mode: default content: + notes: + type: basic_string + region: content + weight: 7 + label: inline + settings: { } + third_party_settings: { } + processed_by: + type: author + weight: 6 + region: content + label: inline + settings: { } + third_party_settings: { } + requested_by: + label: inline + type: author + weight: 5 + region: content + settings: { } + third_party_settings: { } sar_export: type: file_default weight: 2 @@ -36,12 +60,5 @@ content: region: content settings: { } third_party_settings: { } - processed_by: - type: author - weight: 5 - region: content - label: inline - settings: { } - third_party_settings: { } hidden: manual_data: true diff --git a/modules/gdpr_tasks/gdpr_task.page.inc b/modules/gdpr_tasks/gdpr_task.page.inc index 9328872..b5a012f 100644 --- a/modules/gdpr_tasks/gdpr_task.page.inc +++ b/modules/gdpr_tasks/gdpr_task.page.inc @@ -43,6 +43,7 @@ function template_preprocess_gdpr_task(array &$variables) { t('Type'), t('Entity'), t('Bundle'), + t('Notes'), t('Right to be forgotten'), '', ], @@ -64,6 +65,7 @@ function template_preprocess_gdpr_task(array &$variables) { t('Type'), t('Entity'), t('Bundle'), + t('Notes'), t('Right to access'), ], '#rows' => $rows, diff --git a/modules/gdpr_tasks/gdpr_tasks.info.yml b/modules/gdpr_tasks/gdpr_tasks.info.yml index bc48549..793fae2 100644 --- a/modules/gdpr_tasks/gdpr_tasks.info.yml +++ b/modules/gdpr_tasks/gdpr_tasks.info.yml @@ -6,4 +6,5 @@ package: General Data Protection Regulation dependencies: - gdpr:gdpr - - gdpr:dump + - gdpr:gdpr_dump + - gdpr:gdpr_fields diff --git a/modules/gdpr_tasks/gdpr_tasks.links.action.yml b/modules/gdpr_tasks/gdpr_tasks.links.action.yml index b2e1412..604a742 100644 --- a/modules/gdpr_tasks/gdpr_tasks.links.action.yml +++ b/modules/gdpr_tasks/gdpr_tasks.links.action.yml @@ -2,7 +2,6 @@ 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 @@ -11,7 +10,6 @@ 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 diff --git a/modules/gdpr_tasks/gdpr_tasks.module b/modules/gdpr_tasks/gdpr_tasks.module index d81d694..9a6555b 100644 --- a/modules/gdpr_tasks/gdpr_tasks.module +++ b/modules/gdpr_tasks/gdpr_tasks.module @@ -158,27 +158,3 @@ function gdpr_tasks_entity_presave(EntityInterface $entity) { break; } } - -/** - * Implements hook_entity_base_field_info_alter(). - * - * Ensures that anonymize/removal settings are always set for username, email - * password and roles. These fields are not UI-editable so we set these in code. - */ -function gdpr_tasks_entity_base_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type) { - if ($entity_type->id() == 'user') { - $fields['mail']['third_party_settings']['gdpr_fields']['gdpr_fields_enabled'] = TRUE; - $fields['mail']['third_party_settings']['gdpr_fields']['gdpr_fields_rta'] = 'inc'; - $fields['mail']['third_party_settings']['gdpr_fields']['gdpr_fields_rtf'] = 'anonymise'; - $fields['mail']['third_party_settings']['gdpr_fields']['gdpr_fields_sanitizer'] = 'gdpr_email_sanitizer'; - - $fields['name']['third_party_settings']['gdpr_fields']['gdpr_fields_enabled'] = TRUE; - $fields['name']['third_party_settings']['gdpr_fields']['gdpr_fields_rta'] = 'inc'; - $fields['name']['third_party_settings']['gdpr_fields']['gdpr_fields_rtf'] = 'anonymise'; - $fields['name']['third_party_settings']['gdpr_fields']['gdpr_fields_sanitizer'] = 'gdpr_random_text_sanitizer'; - - $fields['roles']['third_party_settings']['gdpr_fields']['gdpr_fields_enabled'] = TRUE; - $fields['roles']['third_party_settings']['gdpr_fields']['gdpr_fields_rta'] = 'no'; - $fields['roles']['third_party_settings']['gdpr_fields']['gdpr_fields_rtf'] = 'remove'; - } -} diff --git a/modules/gdpr_tasks/gdpr_tasks.permissions.yml b/modules/gdpr_tasks/gdpr_tasks.permissions.yml index 90c6d48..99208a2 100644 --- a/modules/gdpr_tasks/gdpr_tasks.permissions.yml +++ b/modules/gdpr_tasks/gdpr_tasks.permissions.yml @@ -1,11 +1,16 @@ +view gdpr data summary: + title: 'Access GDPR Data Summary' + description: 'Allows users to access their data without staff involvement' + create gdpr tasks: title: 'Create GDPR tasks' view gdpr tasks: title: 'View GDPR tasks' + description: 'Allow a user to view completed tasks that they requested' -edit gdpr tasks: - title: 'Edit GDPR tasks' +#edit gdpr tasks: +# title: 'Edit GDPR tasks' add task entities: title: 'Create new Task entities' @@ -16,9 +21,12 @@ administer task entities: delete task entities: title: 'Delete Task entities' + description: 'Allows staff to delete GDPR tasks' edit task entities: title: 'Edit Task entities' + description: 'Allows staff to edit GDPR tasks' view task entities: title: 'View Task entities' + description: 'Allows staff to view the list of GDPR tasks' \ No newline at end of file diff --git a/modules/gdpr_tasks/gdpr_tasks.routing.yml b/modules/gdpr_tasks/gdpr_tasks.routing.yml index 2d244f0..35bd197 100644 --- a/modules/gdpr_tasks/gdpr_tasks.routing.yml +++ b/modules/gdpr_tasks/gdpr_tasks.routing.yml @@ -10,7 +10,7 @@ gdpr_tasks.request: path: '/user/{user}/gdpr-request/{gdpr_task_type}' defaults: _controller: '\Drupal\gdpr_tasks\Controller\GDPRController::requestPage' - _title: 'GDPR Summary' + _title: 'Create Data Request' requirements: _permission: 'create gdpr tasks' options: diff --git a/modules/gdpr_tasks/gdpr_tasks.rules.events.yml b/modules/gdpr_tasks/gdpr_tasks.rules.events.yml new file mode 100644 index 0000000..dc784b3 --- /dev/null +++ b/modules/gdpr_tasks/gdpr_tasks.rules.events.yml @@ -0,0 +1,18 @@ +gdpr_tasks.rules_rta_complete: + label: 'Data access request completed' + category: 'GDPR' + context: + user: + type: 'entity:user' + label: 'The user' + link: + type: 'string' + label: 'The URL to download the report' + +gdpr_tasks.rules_rtf_complete: + label: 'Right to be forgotten completed' + category: 'GDPR' + context: + email: + type: 'string' + label: 'The email address of the recipient' \ No newline at end of file diff --git a/modules/gdpr_tasks/src/Anonymizer.php b/modules/gdpr_tasks/src/Anonymizer.php index bdd3cfa..96795fe 100644 --- a/modules/gdpr_tasks/src/Anonymizer.php +++ b/modules/gdpr_tasks/src/Anonymizer.php @@ -11,6 +11,7 @@ use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\TypedData\Exception\ReadOnlyException; use Drupal\gdpr_dump\Sanitizer\GdprSanitizerFactory; +use Drupal\gdpr_fields\Entity\GdprFieldConfigEntity; use Drupal\gdpr_fields\GDPRCollector; use Drupal\gdpr_tasks\Entity\TaskInterface; use Drupal\gdpr_tasks\Form\RemovalSettingsForm; @@ -253,11 +254,12 @@ private function anonymize(FieldItemListInterface $field, EntityInterface $bundl */ private function getSanitizerId(FieldItemListInterface $field, EntityInterface $bundle_entity) { // First check if this field has a sanitizer defined. + + $config = GdprFieldConfigEntity::load($bundle_entity->getEntityTypeId()); + $field_config = $config->getField($bundle_entity->bundle(), $field->getName()); + $sanitizer = $field_config->sanitizer; $fieldDefinition = $field->getFieldDefinition(); $type = $fieldDefinition->getType(); - $sanitizer = $fieldDefinition - ->getConfig($bundle_entity->bundle()) - ->getThirdPartySetting('gdpr_fields', 'gdpr_fields_sanitizer'); if (!$sanitizer) { // No sanitizer defined directly on the field. @@ -285,20 +287,19 @@ private function getSanitizerId(FieldItemListInterface $field, EntityInterface $ */ private function getFieldsToProcess(EntityInterface $entity) { $bundle_id = $entity->bundle(); + $config = GdprFieldConfigEntity::load($entity->getEntityTypeId()); // 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); + $field_config = $config->getField($bundle_id, $field->getName()); - if (!$config->getThirdPartySetting('gdpr_fields', 'gdpr_fields_enabled', FALSE)) { + if (!$field_config->enabled) { continue; } - $rtf_value = $config->getThirdPartySetting('gdpr_fields', 'gdpr_fields_rtf', FALSE); + $rtf_value = $field_config->rtf; if ($rtf_value && $rtf_value !== 'no') { $fields[] = [ diff --git a/modules/gdpr_tasks/src/Controller/GDPRController.php b/modules/gdpr_tasks/src/Controller/GDPRController.php index 18aac7a..b2871bc 100644 --- a/modules/gdpr_tasks/src/Controller/GDPRController.php +++ b/modules/gdpr_tasks/src/Controller/GDPRController.php @@ -75,8 +75,7 @@ public function summaryPage() { * @param $gdpr_task_type * Type of task to be created. * - * @return \Symfony\Component\HttpFoundation\RedirectResponse - * Return user to GDPR requests. + * @return */ public function requestPage(AccountInterface $user, $gdpr_task_type) { $tasks = $this->taskManager->getUserTasks($user, $gdpr_task_type); @@ -85,6 +84,16 @@ public function requestPage(AccountInterface $user, $gdpr_task_type) { $this->messenger->addWarning('You already have a pending task.'); } else { + // If the current user is making a request for themselves, just create it. + // However, if we're a member of staff making a request on behalf + // of someone else, we need to collect further details + // so render a form to get the notes. + if ($this->currentUser()->id() != $user->id()) { + return [ + 'form' => $this->formBuilder()->getForm('\Drupal\gdpr_tasks\Form\CreateGdprRequestOnBehalfOfUserForm'), + ]; + } + $values = [ 'type' => $gdpr_task_type, 'user_id' => $user->id(), @@ -95,9 +104,7 @@ public function requestPage(AccountInterface $user, $gdpr_task_type) { $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; + return $this->redirect('view.gdpr_tasks_my_data_requests.page_1', ['user' => $user->id()]); } } diff --git a/modules/gdpr_tasks/src/Entity/Task.php b/modules/gdpr_tasks/src/Entity/Task.php index d554e44..c5f3932 100644 --- a/modules/gdpr_tasks/src/Entity/Task.php +++ b/modules/gdpr_tasks/src/Entity/Task.php @@ -63,7 +63,7 @@ class Task extends ContentEntityBase implements TaskInterface { public static function preCreate(EntityStorageInterface $storage_controller, array &$values) { parent::preCreate($storage_controller, $values); $values += [ - 'user_id' => \Drupal::currentUser()->id(), + 'requested_by' => \Drupal::currentUser()->id(), ]; } @@ -169,8 +169,8 @@ 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.')) + ->setLabel(t('User')) + ->setDescription(t('The user whose data should be processed.')) ->setRevisionable(TRUE) ->setSetting('target_type', 'user') ->setSetting('handler', 'default') @@ -240,6 +240,40 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDisplayConfigurable('form', TRUE) ->setDisplayConfigurable('view', TRUE); + $fields['requested_by'] = 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', [ + 'type' => 'author', + 'weight' => 6, + ]) + ->setDisplayOptions('form', [ + 'type' => 'hidden', + 'weight' => 6, + 'settings' => [ + 'match_operator' => 'CONTAINS', + 'size' => '60', + 'autocomplete_type' => 'tags', + 'placeholder' => '', + ], + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + $fields['notes'] = BaseFieldDefinition::create('string_long') + ->setLabel(t('Notes')) + ->setDescription(t('Notes with this request')) + ->setRequired(FALSE) + ->setDisplayOptions('view', [ + 'type' => 'textfield', + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + return $fields; } diff --git a/modules/gdpr_tasks/src/Event/RightToAccessCompleteEvent.php b/modules/gdpr_tasks/src/Event/RightToAccessCompleteEvent.php new file mode 100644 index 0000000..b69ce9d --- /dev/null +++ b/modules/gdpr_tasks/src/Event/RightToAccessCompleteEvent.php @@ -0,0 +1,44 @@ +user = $user; + $this->link = $link; + } + +} diff --git a/modules/gdpr_tasks/src/Event/RightToBeForgottenCompleteEvent.php b/modules/gdpr_tasks/src/Event/RightToBeForgottenCompleteEvent.php new file mode 100644 index 0000000..77a3dfe --- /dev/null +++ b/modules/gdpr_tasks/src/Event/RightToBeForgottenCompleteEvent.php @@ -0,0 +1,37 @@ +email = $email; + } + +} diff --git a/modules/gdpr_tasks/src/Form/CreateGdprRequestOnBehalfOfUserForm.php b/modules/gdpr_tasks/src/Form/CreateGdprRequestOnBehalfOfUserForm.php new file mode 100644 index 0000000..d403829 --- /dev/null +++ b/modules/gdpr_tasks/src/Form/CreateGdprRequestOnBehalfOfUserForm.php @@ -0,0 +1,58 @@ + $this->t('Create request on behalf of user'), + 'notes' => [ + '#type' => 'textarea', + '#title' => $this->t('Notes'), + '#description' => $this->t('Enter the reason for creating this request.'), + ], + 'actions' => [ + '#type' => 'actions', + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Create Request'), + ] + ] + ]; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $user_id = $this->getRouteMatch()->getParameter('user'); + $request_type = $this->getRouteMatch()->getParameter('gdpr_task_type'); + $notes = $form_state->getValue('notes'); + + $task = Task::create([ + 'type' => $request_type, + 'user_id' => $user_id, + 'notes' => $notes, + ]); + $task->save(); + $this->messenger()->addStatus('The request has been logged'); + return $this->redirect('view.gdpr_tasks_my_data_requests.page_1', ['user' => $user_id]); + } +} \ No newline at end of file diff --git a/modules/gdpr_tasks/src/Form/TaskActionsForm.php b/modules/gdpr_tasks/src/Form/TaskActionsForm.php index 4ae5795..af918d7 100644 --- a/modules/gdpr_tasks/src/Form/TaskActionsForm.php +++ b/modules/gdpr_tasks/src/Form/TaskActionsForm.php @@ -5,6 +5,8 @@ use Drupal\Console\Bootstrap\Drupal; use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Form\FormStateInterface; +use Drupal\gdpr_tasks\Event\RightToAccessCompleteEvent; +use Drupal\gdpr_tasks\Event\RightToBeForgottenCompleteEvent; use Drupal\user\Entity\User; /** @@ -35,6 +37,8 @@ public function buildForm(array $form, FormStateInterface $form_state) { * Performs the SAR export. */ private function doSarExport(FormStateInterface $form_state): void { + /* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */ + $dispatcher = \Drupal::service('event_dispatcher'); $entity = $this->entity; $manual = $form_state->getValue(['manual_data', 0, 'value']); @@ -62,6 +66,8 @@ private function doSarExport(FormStateInterface $form_state): void { // @todo Add headers to csv export. file_save_data($export, $file_uri, FILE_EXISTS_REPLACE); + + $dispatcher->dispatch(RightToAccessCompleteEvent::EVENT_NAME, new RightToAccessCompleteEvent($entity->getOwner(), $file_uri)); } /** @@ -69,8 +75,18 @@ private function doSarExport(FormStateInterface $form_state): void { */ private function doRemoval(FormStateInterface $form_state) { // @todo Should be injected + /* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher */ + /* @var $entity \Drupal\gdpr_tasks\Entity\Task */ + $entity = $this->entity; + $email = $entity->getOwner()->getEmail(); + $dispatcher = \Drupal::service('event_dispatcher'); $anonymizer = \Drupal::service('gdpr_tasks.anonymizer'); $errors = $anonymizer->run($this->entity); + + if (count($errors) == 0) { + $dispatcher->dispatch(RightToBeForgottenCompleteEvent::EVENT_NAME, new RightToBeForgottenCompleteEvent($email)); + } + return $errors; } diff --git a/modules/gdpr_tasks/src/TaskListBuilder.php b/modules/gdpr_tasks/src/TaskListBuilder.php index 61faf32..2a524c0 100644 --- a/modules/gdpr_tasks/src/TaskListBuilder.php +++ b/modules/gdpr_tasks/src/TaskListBuilder.php @@ -59,6 +59,7 @@ public function buildHeader() { $header['user'] = $this->t('User'); $header['type'] = $this->t('Type'); $header['created'] = $this->t('Requested'); + $header['requested_by'] = $this->t('Requested by'); return $header + parent::buildHeader(); } @@ -82,6 +83,7 @@ public function buildRow(EntityInterface $entity) { 'granularity' => 1, ]) . ' ago'; + $row['requested_by'] = $entity->requested_by->entity->toLink()->toString(); return $row + parent::buildRow($entity); } @@ -132,7 +134,7 @@ public function render() { '#type' => 'table', '#header' => $this->buildHeader(), '#rows' => [], - '#empty' => $this->t('There is no closed @label yet.', ['@label' => $this->entityType->getLabel()]), + '#empty' => $this->t('There are no closed tasks yet.'), '#cache' => [ 'contexts' => $this->entityType->getListCacheContexts(), 'tags' => $this->entityType->getListCacheTags(), diff --git a/modules/gdpr_view_export_log/gdpr_view_export_log.info.yml b/modules/gdpr_view_export_log/gdpr_view_export_log.info.yml new file mode 100644 index 0000000..76b2bdf --- /dev/null +++ b/modules/gdpr_view_export_log/gdpr_view_export_log.info.yml @@ -0,0 +1,10 @@ +name: General Data Protection Regulation (GDPR) - Log View Exports +description: Logs view exports +core: 8.x +type: module +package: General Data Protection Regulation + +dependencies: + - gdpr:gdpr + - views_data_export:views_data_export + - rules:rules diff --git a/modules/gdpr_view_export_log/gdpr_view_export_log.install b/modules/gdpr_view_export_log/gdpr_view_export_log.install new file mode 100644 index 0000000..9cf4acf --- /dev/null +++ b/modules/gdpr_view_export_log/gdpr_view_export_log.install @@ -0,0 +1,77 @@ +getEditable('views.settings'); + $display_extenders = $config->get('display_extenders') ?: []; + $display_extenders[] = 'gdpr_view_export_logging'; + $config->set('display_extenders', $display_extenders); + $config->save(); +} + +/** + * Implements hook_uninstall(). + */ +function gdpr_view_export_log_uninstall() { + $config = \Drupal::service('config.factory')->getEditable('views.settings'); + $display_extenders = $config->get('display_extenders') ?: []; + $key = array_search('gdpr_view_export_logging', $display_extenders); + if ($key !== FALSE) { + unset($display_extenders[$key]); + $config->set('display_extenders', $display_extenders); + $config->save(); + } +} + + +/** + * Implements hook_schema(). + * + * User IDs are stored in a custom table rather than an entity field. + * This is so that they can be lazily loaded for performance reasons, + * as we don't want to be loading thousands of users when looking up a log entry. + */ +function gdpr_view_export_log_schema() { + $schema['gdpr_view_export_audit_user_ids'] = [ + 'description' => 'Stores when Merge Processes are happening on an entity.', + 'fields' => [ + 'id' => [ + 'type' => 'serial', + 'description' => 'Unique Identifier.', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'log_id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The entity id of the corresponding log entry.', + ], + 'user_id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The ID of the user.', + ], + 'name' => [ + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Cache of the user\'s name', + ], + ], + 'primary key' => ['id'], + ]; + + return $schema; +} \ No newline at end of file diff --git a/modules/gdpr_view_export_log/gdpr_view_export_log.module b/modules/gdpr_view_export_log/gdpr_view_export_log.module new file mode 100644 index 0000000..4985a54 --- /dev/null +++ b/modules/gdpr_view_export_log/gdpr_view_export_log.module @@ -0,0 +1,146 @@ +hasPermission('create gdpr export audits')) { + $items['gdpr']['tray']['links']['#links']['exports'] = [ + 'title' => t('Exports'), + 'url' => Url::fromRoute('entity.gdpr_view_export_audit.collection'), + 'attributes' => [ + 'title' => t('Exports'), + ], + 'weight' => 100, + ]; + } +} + +/** + * Implements hook_views_post_render(). + */ +function gdpr_view_export_log_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) { +} + + +/** + * Implements hook_views_post_build(). + */ +function gdpr_view_export_log_views_post_build(ViewExecutable $view) { + if (GdprExportLogDisplayExtender::isLoggingEnabled($view)) { + + + // Logging is enabled for this view. + // Instead of just letting it render, redirect to the audit page, + // if we haven't been there already. + $already_audited = \Drupal::request() + ->getSession() + ->get('gdpr_audit_id') > 0; + + if (!$already_audited) { + + //@todo: Do not run in preview mode. + $session = \Drupal::request()->getSession(); + + $session->set('gdpr_export_audit_file', $view->display_handler->options["filename"]); + $session->set('gdpr_export_audit_view', $view->id()); + $session->set('gdpr_export_audit_continue', \Drupal::request() + ->getRequestUri()); + + $url = Url::fromRoute('entity.gdpr_view_export_audit.add_form') + ->toString(); + $response = new RedirectResponse($url); + $response->send(); + } + } + +} + +/** + * Implements hook_views_post_execute(). + */ +function gdpr_view_export_log_views_post_execute(ViewExecutable $view) { + // After the view is executed, if there has been an audit entry recorded, + // Modify the audit to store any user IDs included in the output. + if (GdprExportLogDisplayExtender::isLoggingEnabled($view)) { + $session = \Drupal::request()->getSession(); + $audit_entry_id = $session->get('gdpr_audit_id'); + + $value_accessors = []; + + if ($audit_entry_id > 0) { + $audit_entry = ExportAudit::load($audit_entry_id); + + // fieldDefinition is unfortunately protected. + // Use reflection to get it for now + // @todo look at not using reflection in future + $r = new ReflectionMethod('Drupal\views\Plugin\views\field\EntityField', 'getFieldDefinition'); + $r->setAccessible(TRUE); + + // Are any of the fields defined against user? + foreach ($view->field as $field_id => $field) { + // Only process entity fields. + if ($field->definition['class'] == 'Drupal\views\Plugin\views\field\EntityField') { + + // If the field is directly defined on the user, log the ID. + if ($field->definition['entity_type'] == 'user') { + $value_accessors[] = function ($row) use ($field) { + return $field->getEntity($row)->id(); + }; + } + + $field_definition = $r->invoke($field); + + // If the field is a reference to the user, log the id. + if ($field_definition->getType() == 'entity_reference' && $field_definition->getSetting('target_type') == 'user') { + $value_accessors[] = function ($row) use ($field) { + return $field->getValue($row); + }; + } + } + } + + $ids = []; + + foreach ($view->result as $row) { + foreach ($value_accessors as $accessor) { + $id = $accessor($row); + if (!in_array($id, $ids)) { + $ids[] = $id; + } + } + } + + if (count($ids) > 0) { + $audit_entry->save(); + // User IDs are stored in a custom table rather than an entity field. + // This is so that they can be lazily loaded for performance reasons, + // as we don't want to be loading thousands of users + // when looking up a log entry. + + foreach($ids as $id) { + //@todo cache the user's name in here too? + \Drupal::database()->insert('gdpr_view_export_audit_user_ids') + ->fields([ + 'log_id' => $audit_entry->id(), + 'user_id' => $id, + ])->execute(); + } + } + } + + $session->remove('gdpr_audit_id'); + + } + +} + diff --git a/modules/gdpr_view_export_log/gdpr_view_export_log.permissions.yml b/modules/gdpr_view_export_log/gdpr_view_export_log.permissions.yml new file mode 100644 index 0000000..a1a717a --- /dev/null +++ b/modules/gdpr_view_export_log/gdpr_view_export_log.permissions.yml @@ -0,0 +1,2 @@ +create gdpr export audits: + title: 'Create audit entries for exporting views' diff --git a/modules/gdpr_view_export_log/gdpr_view_export_log.routing.yml b/modules/gdpr_view_export_log/gdpr_view_export_log.routing.yml new file mode 100644 index 0000000..9377f84 --- /dev/null +++ b/modules/gdpr_view_export_log/gdpr_view_export_log.routing.yml @@ -0,0 +1,21 @@ +gdpr_view_export_log.view_users: + path: '/admin/gdpr/export/{id}/users' + defaults: + _controller: '\Drupal\gdpr_view_export_log\Controller\ExportAuditController::viewUsers' + requirements: + _permission: 'create gdpr export audits' + +gdpr_view_export_log.delete_user: + path: '/admin/gdpr/export/{id}/users/{user_id}/remove' + defaults: + _form: '\Drupal\gdpr_view_export_log\Form\RemoveUserFromExportForm' + requirements: + _permission: 'create gdpr export audits' + +gdpr_view_export_log.exports_containing_user: + path: '/admin/gdpr/exports_containing_user/{user_id}' + defaults: + #_controller: '\Drupal\gdpr_view_export_log\Controller\ExportAuditController::viewExports' + _entity_list: 'gdpr_view_export_audit' + requirements: + _permission: 'create gdpr export audits' \ No newline at end of file diff --git a/modules/gdpr_view_export_log/src/Controller/ExportAuditController.php b/modules/gdpr_view_export_log/src/Controller/ExportAuditController.php new file mode 100644 index 0000000..164b7a0 --- /dev/null +++ b/modules/gdpr_view_export_log/src/Controller/ExportAuditController.php @@ -0,0 +1,99 @@ +select('gdpr_view_export_audit_user_ids', 'g'); + + $count_query = clone $query; + $count_query->addExpression('count(g.id)'); + + $paged_query = $query->extend('Drupal\Core\Database\Query\PagerSelectExtender'); + $paged_query->limit(50); + $paged_query->setCountQuery($count_query); + $paged_query->addJoin('left', 'users_field_data', 'u', 'g.user_id = u.uid'); + + $results = $paged_query->fields('g', ['user_id']) + ->fields('u', ['name']) + ->condition('g.log_id', $id) + ->execute() + ->fetchAll(\PDO::FETCH_ASSOC); + + + $output = [ + 'back' => [ + '#type' => 'link', + '#url' => Url::fromRoute('entity.gdpr_view_export_audit.collection'), + '#title' => $this->t('Back to export list'), + ], + + 'table' => [ + '#type' => 'table', + '#header' => ['ID', 'Username', ''], + '#empty' => 'Could not locate any users in this export.', + ], + + 'pager' => [ + '#theme' => 'pager', + '#weight' => 5, + '#element' => 0, + '#parameters' => [], + '#quantity' => 9, + '#route_name' => '', + '#tags' => '', + ], + ]; + + + foreach ($results as $result) { + $output['table'][$result['user_id']]['userid'] = [ + '#markup' => $result['user_id'], + ]; + + $output['table'][$result['user_id']]['username'] = [ + '#type' => 'link', + '#url' => Url::fromRoute('entity.user.canonical', ['user' => $result['user_id']]), + '#title' => $result['name'], + ]; + + + $output['table'][$result['user_id']]['operations'] = [ + '#type' => 'operations', + '#links' => [ + 'remove' => [ + 'title' => $this->t('Remove'), + 'url' => Url::fromRoute('gdpr_view_export_log.delete_user', [ + 'id' => $id, + 'user_id' => $result['user_id'], + ]), + ], + ], + ]; + } + + return $output; + } + +} diff --git a/modules/gdpr_view_export_log/src/Entity/ExportAudit.php b/modules/gdpr_view_export_log/src/Entity/ExportAudit.php new file mode 100644 index 0000000..0ca6f06 --- /dev/null +++ b/modules/gdpr_view_export_log/src/Entity/ExportAudit.php @@ -0,0 +1,129 @@ +setLabel(t('Location')) + ->setDescription(t('Where will this export be stored? (For example, a user\'s PC)')) + ->setRequired(TRUE) + ->setDisplayOptions('form', [ + 'type' => 'textfield', + 'weight' => 1, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + + $fields['reason'] = BaseFieldDefinition::create('string_long') + ->setLabel(t('Reason')) + ->setDescription(t('The reason for the export')) + ->setRequired(TRUE) + ->setDisplayOptions('form', [ + 'type' => 'textfield_long', + 'weight' => 2, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + $fields['length'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Length')) + ->setDescription(t('Length (in a days) that the export should live for.')) + ->setDefaultValue(90) + ->setRequired(TRUE) + ->setDisplayOptions('form', [ + 'type' => 'number', + 'weight' => 3, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + + + $fields['owner'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Authored by')) + ->setDescription(t('The user ID of author of the audit entry.')) + ->setRevisionable(TRUE) + ->setSetting('target_type', 'user') + ->setSetting('handler', 'default') + ->setTranslatable(TRUE) + ->setDisplayOptions('view', [ + // 'label' => 'hidden', + 'type' => 'author', + ]) + ->setDisplayOptions('form', [ + 'type' => 'hidden', + 'settings' => [ + 'match_operator' => 'CONTAINS', + 'size' => '60', + 'autocomplete_type' => 'tags', + 'placeholder' => '', + ], + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + $fields['created'] = BaseFieldDefinition::create('created') + ->setLabel(t('Created')) + ->setDescription(t('The time that the entity was created.')); + + $fields['filename'] = BaseFieldDefinition::create('string') + ->setLabel(t('File name')) + ->setDescription(t('The name of the file')); + + $fields['view'] = BaseFieldDefinition::create('string') + ->setLabel(t('View')) + ->setDescription(t('The name of the view')); + + return $fields; + } + +} \ No newline at end of file diff --git a/modules/gdpr_view_export_log/src/Entity/ExportAuditListBuilder.php b/modules/gdpr_view_export_log/src/Entity/ExportAuditListBuilder.php new file mode 100644 index 0000000..6c40b41 --- /dev/null +++ b/modules/gdpr_view_export_log/src/Entity/ExportAuditListBuilder.php @@ -0,0 +1,99 @@ + 'File Name', + 'location' => 'Location', + 'reason' => 'Reason', + 'created' => 'Date Exported', + 'expires' => 'Expires In', + 'owner' => 'Exported By', + ] + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + protected function getEntityIds() { + // If a user ID was passed in the route then only load the exports + // that contain the specified user ID. + $user_id = \Drupal::routeMatch()->getParameter('user_id'); + if ($user_id) { + $query = $this->getStorage()->getQuery() + ->condition('user_ids', $user_id) + ->sort($this->entityType->getKey('id')); + + // Only add the pager if a limit is specified. + if ($this->limit) { + $query->pager($this->limit); + } + return $query->execute(); + + + } + else { + return parent::getEntityIds(); + } + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $expires = $entity->get('length')->value; + + $date_exported = new \DateTimeImmutable(); + $date_exported = $date_exported->setTimestamp($entity->get('created')->value); + $date_expires = $date_exported->modify("+{$expires} day"); + + $now = new \DateTimeImmutable('now'); + $diff = $now->diff($date_expires); + + $row = [ + 'filename' => $entity->get('filename')->value, + 'location' => $entity->get('location')->value, + 'reason' => $entity->get('reason')->value, + 'created' => date('Y-m-d H:i:s', $entity->get('created')->value), + ]; + + $row['expires']['data'] = [ + '#markup' => $diff->invert ? "{$diff->format('%r%a days')}" : $diff->format('%r%a days'), + ]; + + $row['user']['data'] = [ + '#theme' => 'username', + '#account' => $entity->get('owner')->entity, + ]; + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function buildOperations(EntityInterface $entity) { + $ops = parent::buildOperations($entity); + $ops['#links']['view_users'] = [ + 'title' => $this->t('View Users'), + 'url' => Url::fromRoute('gdpr_view_export_log.view_users', ['id' => $entity->id()]), + 'weight' => 1, + ]; + return $ops; + } + +} diff --git a/modules/gdpr_view_export_log/src/Form/AuditDeleteForm.php b/modules/gdpr_view_export_log/src/Form/AuditDeleteForm.php new file mode 100644 index 0000000..6b3548d --- /dev/null +++ b/modules/gdpr_view_export_log/src/Form/AuditDeleteForm.php @@ -0,0 +1,26 @@ +t('Please only remove this export log if you have completely destroyed the export on the target computer and any copies of it. Are you sure you want to continue?'); + return $form; + } + +} \ No newline at end of file diff --git a/modules/gdpr_view_export_log/src/Form/AuditForm.php b/modules/gdpr_view_export_log/src/Form/AuditForm.php new file mode 100644 index 0000000..1370c98 --- /dev/null +++ b/modules/gdpr_view_export_log/src/Form/AuditForm.php @@ -0,0 +1,74 @@ +t('Please provide some information about your export.'); + + $form['intro'] = [ + '#markup' => 'To continue, please enter details about how this export will be used.', + ]; + + $session = $this->getRequest()->getSession(); + + $form['continue_url'] = [ + '#type' => 'hidden', + '#default_value' => $session->get('gdpr_export_audit_continue'), + ]; + + $form['filename'] = [ + '#type' => 'hidden', + '#default_value' => $session->get('gdpr_export_audit_file'), + ]; + + $form['view'] = [ + '#type' => 'hidden', + '#default_value' => $session->get('gdpr_export_audit_view'), + ]; + + $session->remove('gdpr_export_audit_view'); + $session->remove('gdpr_export_audit_file'); + $session->remove('gdpr_export_audit_continue'); + + $form['actions']['submit']['#value'] = $this->t('Continue'); + return $form; + } + + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $entity = $this->entity; + $entity->set('owner', \Drupal::currentUser()->id()); + parent::save($form, $form_state); + + $url = urldecode($form_state->getValue('continue_url')); + if (strpos($url, '?') > -1) { + $url .= '&audited=1'; + } + else { + $url .= '?audited=1'; + } + + $this->getRequest()->getSession()->set('gdpr_audit_id', $entity->id()); + $form_state->setRedirectUrl(Url::fromUserInput($url)); + } + +} diff --git a/modules/gdpr_view_export_log/src/Form/RemoveUserFromExportForm.php b/modules/gdpr_view_export_log/src/Form/RemoveUserFromExportForm.php new file mode 100644 index 0000000..51b22a7 --- /dev/null +++ b/modules/gdpr_view_export_log/src/Form/RemoveUserFromExportForm.php @@ -0,0 +1,75 @@ + $this->t('Only remove a user from this export if you have actually removed all their information from the export on the target machine and any copies'), + ]; + + $form['actions'] = [ + '#type' => 'actions', + 'submit' => [ + '#type' => 'submit', + '#value' => 'Remove', + ], + ]; + + $form['id'] = [ + '#type' => 'hidden', + '#default_value' => $id, + ]; + + $form['user_id'] = [ + '#type' => 'hidden', + '#default_value' => $user_id, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $id_to_remove = $form_state->getValue('id'); + $audit_entry = ExportAudit::load($form_state->getValue('id')); + + foreach ($audit_entry->get('user_ids') as $index => $field) { + if ($field->value == $id_to_remove) { + $index_to_remove = $index; + break; + } + } + + if (isset($index_to_remove)) { + $audit_entry->get('user_ids')->removeItem($index_to_remove); + $audit_entry->save(); + } + + \Drupal::messenger()->addMessage($this->t('User removed')); + $form_state->setRedirect('gdpr_view_export_log.view_users', ['id' => $audit_entry->id()]); + } +} \ No newline at end of file diff --git a/modules/gdpr_view_export_log/src/Plugin/views/display_extender/GdprExportLogDisplayExtender.php b/modules/gdpr_view_export_log/src/Plugin/views/display_extender/GdprExportLogDisplayExtender.php new file mode 100644 index 0000000..b549723 --- /dev/null +++ b/modules/gdpr_view_export_log/src/Plugin/views/display_extender/GdprExportLogDisplayExtender.php @@ -0,0 +1,100 @@ +displayHandler) == ExportAudit::exportDisplayHandler(); + } + + /** + * {@inheritdoc} + */ + public function defineOptionsAlter(&$options) { + $options['gdpr_log'] = [ + 'default' => FALSE, + 'contains' => [ + 'log_exports' => ['default' => 0], + ], + ]; + } + + /** + * @inheritDoc + */ + public function optionsSummary(&$categories, &$options) { + if ($this->isExportView()) { + $categories['gdpr_log'] = [ + 'title' => 'GDPR', + 'column' => 'second', + ]; + + $options['gdpr_log'] = [ + 'category' => 'gdpr_log', + 'title' => 'Log Exports', + 'value' => $this->loggingEnabled() ? 'Yes' : 'No', + ]; + } + } + + public function loggingEnabled() { + return array_key_exists('gdpr_log', $this->options) && $this->options['gdpr_log']['log_exports'] == TRUE; + } + + /** + * @inheritDoc + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + if ($form_state->get('section') == 'gdpr_log') { + $form['#title'] .= 'The GDPR Log settings'; + + $form['gdpr_log']['#type'] = 'container'; + $form['gdpr_log']['#tree'] = TRUE; + $form['gdpr_log']['log_exports'] = [ + '#title' => $this->t('Log Exports'), + '#description' => $this->t('Whether to log exports of this view'), + '#type' => 'checkbox', + '#default_value' => $this->loggingEnabled(), + ]; + } + } + + /** + * @inheritDoc + */ + public function submitOptionsForm(&$form, FormStateInterface $form_state) { + if ($form_state->get('section') == 'gdpr_log') { + //$should_log = $form_state->getValue('gdpr_log')['log_exports']; + $this->options['gdpr_log'] = $form_state->getValue('gdpr_log'); + } + } + + public static function isLoggingEnabled(ViewExecutable $view) { + if (get_class($view->display_handler) == ExportAudit::exportDisplayHandler()) { + $extenders = $view->getDisplay()->getExtenders(); + if (array_key_exists('gdpr_view_export_logging', $extenders)) { + return $extenders['gdpr_view_export_logging']->options['gdpr_log']['log_exports'] == 1; + } + } + + return FALSE; + } + +} \ No newline at end of file diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 8940724..6b6820d 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -22,10 +22,22 @@ class UserController extends ControllerBase { * @return \Drupal\Core\Access\AccessResultInterface * The result. */ - public function access(UserInterface $user) { + public function accessCollectedData(UserInterface $user) { + // Must have the appropriate permission to access the page. + // This allows site admins to completely disable the View My Data page. + if (!$this->currentUser()->hasPermission('view gdpr data summary')) { + return AccessResult::forbidden(); + } + + // If access to the page is enabled, + // only show the tab if we're viewing our OWN profile + // or we're a GDPR admin. if ((int) $user->id() === (int) $this->currentUser()->id()) { return AccessResult::allowed(); } + elseif ($this->currentUser()->hasPermission('view task entities')) { + return AccessResult::allowed(); + } return AccessResult::forbidden(); }