diff --git a/composer.json b/composer.json index 795f046..ace483f 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ } ], "require": { - "drupal/checklistapi": "^1.0" + "drupal/checklistapi": "^1.0", + "drupal/entity_reference_revisions": "^1.4" } } diff --git a/gdpr.info.yml b/gdpr.info.yml index ca13e3c..d0f4226 100644 --- a/gdpr.info.yml +++ b/gdpr.info.yml @@ -7,3 +7,4 @@ package: General Data Protection Regulation dependencies: - drupal:checklistapi + - drupal:message \ No newline at end of file diff --git a/gdpr.module b/gdpr.module index dd17037..604fa69 100644 --- a/gdpr.module +++ b/gdpr.module @@ -469,3 +469,56 @@ function _get_modules() { return $modules_list; } } + +/** + * Implements hook_toolbar(). + */ +function gdpr_toolbar() { + $user = \Drupal::currentUser(); + + $items = []; + + $items['gdpr'] = [ + '#cache' => [ + 'contexts' => [ + 'user', + ], + ], + ]; + + // 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')) { + $items['gdpr'] += [ + '#type' => 'toolbar_item', + 'tab' => [ + '#type' => 'link', + '#title' => t('GDPR'), + // Task module adds in the URL. + '#url' => '', + '#attributes' => [ + 'title' => t('GDPR'), + 'class' => ['toolbar-icon', 'toolbar-icon-shortcut'], + ], + ], + 'tray' => [ + 'links' => [ + '#theme' => 'links', + '#links' => [], + '#attributes' => [ + 'class' => ['toolbar-menu'], + ], + ], + ], + '#weight' => -10, + '#attached' => [ + 'library' => [ + 'shortcut/drupal.shortcut', + ], + ], + ]; + + } + return $items; +} \ No newline at end of file diff --git a/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.agreed.yml b/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.agreed.yml new file mode 100644 index 0000000..09ba1f7 --- /dev/null +++ b/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.agreed.yml @@ -0,0 +1,23 @@ +uuid: 3b601cf3-2f83-4b18-a271-3025a16db2ad +langcode: en +status: true +dependencies: + config: + - field.storage.message.agreed + - message.template.consent_agreement_accepted +id: message.consent_agreement_accepted.agreed +field_name: agreed +entity_type: message +bundle: consent_agreement_accepted +label: Agreed +description: '' +required: false +translatable: false +default_value: + - + value: 0 +default_value_callback: '' +settings: + on_label: 'Yes' + off_label: 'No' +field_type: boolean diff --git a/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.agreement.yml b/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.agreement.yml new file mode 100644 index 0000000..88629b5 --- /dev/null +++ b/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.agreement.yml @@ -0,0 +1,27 @@ +uuid: 000c8918-277d-4444-8dac-922d86366238 +langcode: en +status: true +dependencies: + config: + - field.storage.message.agreement + - message.template.consent_agreement_accepted + module: + - entity_reference_revisions +id: message.consent_agreement_accepted.agreement +field_name: agreement +entity_type: message +bundle: consent_agreement_accepted +label: Agreement +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:gdpr_consent_agreement' + handler_settings: + target_bundles: null + sort: + field: _none + auto_create: false +field_type: entity_reference_revisions diff --git a/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.notes.yml b/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.notes.yml new file mode 100644 index 0000000..6f2cdd6 --- /dev/null +++ b/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.notes.yml @@ -0,0 +1,19 @@ +uuid: a6562ec7-6913-4944-9fec-ab9ef0ab2bb1 +langcode: en +status: true +dependencies: + config: + - field.storage.message.notes + - message.template.consent_agreement_accepted +id: message.consent_agreement_accepted.notes +field_name: notes +entity_type: message +bundle: consent_agreement_accepted +label: Notes +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: string_long diff --git a/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.user.yml b/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.user.yml new file mode 100644 index 0000000..7a8c943 --- /dev/null +++ b/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.user.yml @@ -0,0 +1,21 @@ +uuid: 2a6fc21e-93cb-4db8-96a0-7d1f77efc3d1 +langcode: en +status: true +dependencies: + config: + - field.storage.message.user + - message.template.consent_agreement_accepted +id: message.consent_agreement_accepted.user +field_name: user +entity_type: message +bundle: consent_agreement_accepted +label: User +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:user' + handler_settings: { } +field_type: entity_reference diff --git a/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.user_accepted.yml b/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.user_accepted.yml new file mode 100644 index 0000000..4e9c954 --- /dev/null +++ b/modules/gdpr_consent/config/install/field.field.message.consent_agreement_accepted.user_accepted.yml @@ -0,0 +1,28 @@ +uuid: 9168354c-5505-495d-bfce-382aa4f12be7 +langcode: en +status: true +dependencies: + config: + - field.storage.message.user_accepted + - message.template.consent_agreement_accepted +id: message.consent_agreement_accepted.user_accepted +field_name: user_accepted +entity_type: message +bundle: consent_agreement_accepted +label: 'User Accepted' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:user' + handler_settings: + include_anonymous: true + filter: + type: _none + target_bundles: null + sort: + field: _none + auto_create: false +field_type: entity_reference diff --git a/modules/gdpr_consent/config/install/field.storage.message.agreed.yml b/modules/gdpr_consent/config/install/field.storage.message.agreed.yml new file mode 100644 index 0000000..16e564c --- /dev/null +++ b/modules/gdpr_consent/config/install/field.storage.message.agreed.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - message +id: message.agreed +field_name: agreed +entity_type: message +type: boolean +settings: + case_sensitive: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/gdpr_consent/config/install/field.storage.message.agreement.yml b/modules/gdpr_consent/config/install/field.storage.message.agreement.yml new file mode 100644 index 0000000..6f197fa --- /dev/null +++ b/modules/gdpr_consent/config/install/field.storage.message.agreement.yml @@ -0,0 +1,21 @@ +uuid: 83ca8ca3-88c0-4eb4-bdbf-cadb24186abf +langcode: en +status: true +dependencies: + module: + - entity_reference_revisions + - gdpr_consent + - message +id: message.agreement +field_name: agreement +entity_type: message +type: entity_reference_revisions +settings: + target_type: gdpr_consent_agreement +module: entity_reference_revisions +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/gdpr_consent/config/install/field.storage.message.notes.yml b/modules/gdpr_consent/config/install/field.storage.message.notes.yml new file mode 100644 index 0000000..819abaf --- /dev/null +++ b/modules/gdpr_consent/config/install/field.storage.message.notes.yml @@ -0,0 +1,19 @@ +uuid: 24feee44-56c7-4e92-b4d2-fe51c5bf710d +langcode: en +status: true +dependencies: + module: + - message +id: message.notes +field_name: notes +entity_type: message +type: string_long +settings: + case_sensitive: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/gdpr_consent/config/install/field.storage.message.user.yml b/modules/gdpr_consent/config/install/field.storage.message.user.yml new file mode 100644 index 0000000..0a8d3e8 --- /dev/null +++ b/modules/gdpr_consent/config/install/field.storage.message.user.yml @@ -0,0 +1,20 @@ +uuid: 4bcd4001-6d9c-4928-845a-ec518697c55b +langcode: en +status: true +dependencies: + module: + - message + - user +id: message.user +field_name: user +entity_type: message +type: entity_reference +settings: + target_type: user +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/gdpr_consent/config/install/field.storage.message.user_accepted.yml b/modules/gdpr_consent/config/install/field.storage.message.user_accepted.yml new file mode 100644 index 0000000..339ef60 --- /dev/null +++ b/modules/gdpr_consent/config/install/field.storage.message.user_accepted.yml @@ -0,0 +1,20 @@ +uuid: b457e6e8-cf95-493e-b25a-8248bc60449f +langcode: en +status: true +dependencies: + module: + - message + - user +id: message.user_accepted +field_name: user_accepted +entity_type: message +type: entity_reference +settings: + target_type: user +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/gdpr_consent/config/install/message.template.consent_agreement_accepted.yml b/modules/gdpr_consent/config/install/message.template.consent_agreement_accepted.yml new file mode 100644 index 0000000..e466bf4 --- /dev/null +++ b/modules/gdpr_consent/config/install/message.template.consent_agreement_accepted.yml @@ -0,0 +1,17 @@ +uuid: a020749c-7309-4278-9fbf-4d1edfdafd51 +langcode: en +status: true +dependencies: { } +template: consent_agreement_accepted +label: 'GDPR Consent Agreement' +description: '' +text: + - + value: "
Agreement: [message:agreement:entity:title]
\r\nAgreed: [message:agreed]
\r\nNotes: [message:notes]
{{ message }}
{% endif %}', + '#context' => [ + 'date' => $link, + 'username' => \Drupal::service('renderer')->renderPlain($username), + 'message' => [ + '#markup' => $revision->getRevisionLogMessage(), + '#allowed_tags' => Xss::getHtmlTagList(), + ], + ], + ], + ]; + $row[] = $column; + + if ($latest_revision) { + $row[] = [ + 'data' => [ + '#prefix' => '', + '#markup' => $this->t('Current revision'), + '#suffix' => '', + ], + ]; + foreach ($row as &$current) { + $current['class'] = ['revision-current']; + } + $latest_revision = FALSE; + } + else { + $links = []; + if ($revert_permission) { + $links['revert'] = [ + 'title' => $this->t('Revert'), + 'url' => Url::fromRoute('entity.gdpr_consent_agreement.revision_revert', [ + 'gdpr_consent_agreement' => $agreement->id(), + 'gdpr_consent_agreement_revision' => $vid, + ]), + ]; + } + + if ($delete_permission) { + $links['delete'] = [ + 'title' => $this->t('Delete'), + 'url' => Url::fromRoute('entity.gdpr_consent_agreement.revision_delete', [ + 'gdpr_consent_agreement' => $agreement->id(), + 'gdpr_consent_agreement_revision' => $vid, + ]), + ]; + } + + $row[] = [ + 'data' => [ + '#type' => 'operations', + '#links' => $links, + ], + ]; + } + + $rows[] = $row; + } + + $build['gdpr_consent_agreement_revisions_table'] = [ + '#theme' => 'table', + '#rows' => $rows, + '#header' => $header, + ]; + + return $build; + } + + function myAgreements($user) { + $map = $this->entityFieldManager->getFieldMapByFieldType('gdpr_user_consent'); + $agreement_storage = $this->entityTypeManager()->getStorage('gdpr_consent_agreement'); + $rows = []; + + foreach ($map as $entity_type => $fields) { + $field_names = array_keys($fields); + + foreach ($field_names as $field_name) { + + $ids = \Drupal::entityQuery($entity_type) + ->condition($field_name . '.user_id', $user) + ->execute(); + + $entities = $this->entityTypeManager()->getStorage($entity_type) + ->loadMultiple($ids); + + foreach ($entities as $entity) { + $agreement = $agreement_storage->loadRevision($entity->{$field_name}->target_revision_id); + + $row = []; + + $row[] = [ + 'data' => [ + '#markup' => $agreement->toLink($agreement->title->value, 'revision')->toString() + ], + ]; + + $row[] = [ + 'data' => [ + '#markup' => $entity->{$field_name}->date + ], + ]; + + $rows[] = $row; + } + } + } + + + $header = ['Agreement', 'Date Agreed']; + + $build = [ + '#title' => 'Consent Agreements', + 'table' => [ + '#theme' => 'table', + '#rows' => $rows, + '#header' => $header, + ], + ]; + return $build; + } + +} diff --git a/modules/gdpr_consent/src/Entity/ConsentAgreement.php b/modules/gdpr_consent/src/Entity/ConsentAgreement.php new file mode 100644 index 0000000..e200520 --- /dev/null +++ b/modules/gdpr_consent/src/Entity/ConsentAgreement.php @@ -0,0 +1,321 @@ + \Drupal::currentUser()->id(), + ]; + } + + /** + * {@inheritdoc} + */ + protected function urlRouteParameters($rel) { + $uri_route_parameters = parent::urlRouteParameters($rel); + + if ($rel === 'revision_revert' && $this instanceof RevisionableInterface) { + $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId(); + } + elseif ($rel === 'revision_delete' && $this instanceof RevisionableInterface) { + $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId(); + } + + return $uri_route_parameters; + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + foreach (array_keys($this->getTranslationLanguages()) as $langcode) { + $translation = $this->getTranslation($langcode); + + // If no owner has been set explicitly, make the anonymous user the owner. + if (!$translation->getOwner()) { + $translation->setOwnerId(0); + } + } + + // If no revision author has been set explicitly, + // make the ConsentAgreement owner the + // revision author. + if (!$this->getRevisionUser()) { + $this->setRevisionUserId($this->getOwnerId()); + } + } + + public function requiresExplicitAcceptance() { + return $this->get('mode')->value == 'explicit'; + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->get('name')->value; + } + + /** + * {@inheritdoc} + */ + public function setName($name) { + $this->set('name', $name); + return $this; + } + + /** + * {@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 isPublished() { + return (bool) $this->getEntityKey('status'); + } + + /** + * {@inheritdoc} + */ + public function setPublished($published) { + $this->set('status', $published ? TRUE : FALSE); + return $this; + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields = parent::baseFieldDefinitions($entity_type); + + $fields['title'] = BaseFieldDefinition::create('string') + ->setLabel(t('Title')) + ->setRevisionable(TRUE) + ->setDescription(t('Agreement Title')) + ->setRequired(TRUE) + ->setDisplayOptions('view', [ + 'type' => 'textfield', + ]) + ->setDisplayOptions('form', [ + 'type' => 'textfield', + 'weight' => 0, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + $fields['mode'] = BaseFieldDefinition::create('list_string') + ->setLabel(t('Agreement Type')) + ->setRevisionable(TRUE) + ->setDescription(t('Whether consent is implicit or explicit. Set to "Explicit" if the user needs to explicitly agree, otherwise "Implicit".')) + ->setDefaultValue('explicit') + ->setSetting('allowed_values_function', ['\Drupal\gdpr_consent\Entity\ConsentAgreement', 'getModes']) + ->setRequired(TRUE) + ->setDisplayOptions('view', [ + 'type' => 'select', + ]) + ->setDisplayOptions('form', [ + 'type' => 'select', + 'weight' => 1, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + $fields['description'] = BaseFieldDefinition::create('string') + ->setLabel(t('Description')) + ->setRevisionable(TRUE) + ->setDescription(t('Text displayed to the user on the form')) + ->setRequired(TRUE) + ->setDisplayOptions('view', [ + 'type' => 'textfield', + ]) + ->setDisplayOptions('form', [ + 'type' => 'textfield', + 'weight' => 2, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + $fields['long_description'] = BaseFieldDefinition::create('string_long') + ->setLabel(t('Long Description')) + ->setRevisionable(TRUE) + ->setDescription(t('Text shown when the user clicks for more details')) + ->setDisplayOptions('view', [ + 'type' => 'textarea', + ]) + ->setDisplayOptions('form', [ + 'type' => 'textarea', + 'weight' => 3, + ]) + ->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.')) + ->setRevisionable(TRUE) + ->setDefaultValue(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['revision_translation_affected'] = BaseFieldDefinition::create('boolean') + ->setLabel(t('Revision translation affected')) + ->setDescription(t('Indicates if the last edit of a translation belongs to current revision.')) + ->setReadOnly(TRUE) + ->setRevisionable(TRUE) + ->setTranslatable(TRUE); + + $fields['user_id'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Authored by')) + ->setDescription(t('The user ID of author of the Consent Agreement entity.')) + ->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); + + return $fields; + } + + public static function getModes() { + return [ + 'implicit' => 'Implicit', + 'explicit' => 'Explicit', + ]; + } + + /** + * {@inheritdoc} + */ + public function __toString() { + return $this->get('title')->getString(); + } + + +} \ No newline at end of file diff --git a/modules/gdpr_consent/src/Entity/ConsentAgreementInterface.php b/modules/gdpr_consent/src/Entity/ConsentAgreementInterface.php new file mode 100644 index 0000000..1058a58 --- /dev/null +++ b/modules/gdpr_consent/src/Entity/ConsentAgreementInterface.php @@ -0,0 +1,114 @@ + 'Title', + 'mode' => 'Implicit/Explicit', + ]; + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row = [ + 'title' => $entity->get('title')->value, + 'mode' => $entity->get('mode')->value, + ]; + return $row + parent::buildRow($entity); + } + +} diff --git a/modules/gdpr_consent/src/Form/ConsentAgreementForm.php b/modules/gdpr_consent/src/Form/ConsentAgreementForm.php new file mode 100644 index 0000000..7dbc535 --- /dev/null +++ b/modules/gdpr_consent/src/Form/ConsentAgreementForm.php @@ -0,0 +1,106 @@ +entity; + +// $form['title'] = [ +// '#type' => 'textfield', +// '#required' => TRUE, +// '#title' => 'Title', +// '#default_value' => $entity->title->value, +// ]; + +// $form['mode'] = [ +// '#type' => 'select', +// '#title' => 'Agreement Type', +// '#required' => TRUE, +// '#options' => ConsentAgreement::getModes(), +// '#default_value' => $entity->mode->value, +// '#description' => 'Set to "Explicit" if the user needs to explicitly agree, otherwise "Implicit"', +// ]; + +// $form['description'] = [ +// '#type' => 'textfield', +// '#title' => 'Description', +// '#required' => TRUE, +// '#description' => 'Text displayed to the user on the form', +// '#default_value' => $entity->description->value, +// ]; +// +// $form['long_description'] = [ +// '#type' => 'textarea', +// '#title' => 'Long Description', +// '#description' => 'Text shown when the user clicks for more details', +// '#default_value' => $entity->long_description->value, +// ]; + + if (!$this->entity->isNew()) { + $form['new_revision'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Create new revision'), + '#default_value' => TRUE, + '#weight' => 10, + ]; + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $entity = $this->entity; + + // Save as a new revision if requested to do so. + if (!$form_state->isValueEmpty('new_revision') && $form_state->getValue('new_revision') != FALSE) { + $entity->setNewRevision(); + + // If a new revision is created, save the current user as revision author. + $entity->setRevisionCreationTime(REQUEST_TIME); + $entity->setRevisionUserId(\Drupal::currentUser()->id()); + } + else { + $entity->setNewRevision(FALSE); + } + + $status = parent::save($form, $form_state); + + switch ($status) { + case SAVED_NEW: + drupal_set_message($this->t('Created the %label Consent Agreement.', [ + '%label' => $entity->label(), + ])); + break; + + default: + drupal_set_message($this->t('Saved the %label Consent Agreement.', [ + '%label' => $entity->label(), + ])); + } + $form_state->setRedirect('entity.gdpr_consent_agreement.canonical', ['gdpr_consent_agreement' => $entity->id()]); + } + + protected function showRevisionUi() { + return FALSE; + } +} diff --git a/modules/gdpr_consent/src/Form/ConsentAgreementRevisionRevertForm.php b/modules/gdpr_consent/src/Form/ConsentAgreementRevisionRevertForm.php new file mode 100644 index 0000000..c7356d8 --- /dev/null +++ b/modules/gdpr_consent/src/Form/ConsentAgreementRevisionRevertForm.php @@ -0,0 +1,149 @@ +ConsentAgreementStorage = $entity_storage; + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager')->getStorage('gdpr_consent_agreement'), + $container->get('date.formatter') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'gdpr_consent_agreement_revision_revert_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.gdpr_consent_agreement.version_history', ['gdpr_consent_agreement' => $this->revision->id()]); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Revert'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $gdpr_consent_agreement_revision = NULL) { + $this->revision = $this->ConsentAgreementStorage->loadRevision($gdpr_consent_agreement_revision); + $form = parent::buildForm($form, $form_state); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // The revision timestamp will be updated when the revision is saved. Keep + // the original one for the confirmation message. + $original_revision_timestamp = $this->revision->getRevisionCreationTime(); + + $this->revision = $this->prepareRevertedRevision($this->revision, $form_state); + $this->revision->revision_log = t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)]); + $this->revision->save(); + + $this->logger('content')->notice('Consent Agreement: reverted %title revision %revision.', ['%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]); + \Drupal::messenger()->addMessage(t('Consent Agreement %title has been reverted to the revision from %revision-date.', ['%title' => $this->revision->label(), '%revision-date' => $this->dateFormatter->format($original_revision_timestamp)])); + $form_state->setRedirect( + 'entity.gdpr_consent_agreement.version_history', + ['gdpr_consent_agreement' => $this->revision->id()] + ); + } + + /** + * Prepares a revision to be reverted. + * + * @param \Drupal\gdpr_consent\Entity\ConsentAgreementInterface $revision + * The revision to be reverted. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\gdpr_consent\Entity\ConsentAgreementInterface + * The prepared revision ready to be stored. + */ + protected function prepareRevertedRevision(ConsentAgreementInterface $revision, FormStateInterface $form_state) { + $revision->setNewRevision(); + $revision->isDefaultRevision(TRUE); + $revision->setRevisionCreationTime(REQUEST_TIME); + + return $revision; + } + +} diff --git a/modules/gdpr_consent/src/Plugin/Block/GdprMyAgreementsBlock.php b/modules/gdpr_consent/src/Plugin/Block/GdprMyAgreementsBlock.php new file mode 100644 index 0000000..13c7283 --- /dev/null +++ b/modules/gdpr_consent/src/Plugin/Block/GdprMyAgreementsBlock.php @@ -0,0 +1,29 @@ +getContextValue('user'); + // Just delegate to the controller to do the work. + $ctrl = new ConsentAgreementController(\Drupal::getContainer()->get('entity_field.manager')); + return $ctrl->myAgreements($user->id()); + } +} \ No newline at end of file diff --git a/modules/gdpr_consent/src/Plugin/Deriver/GdprMyAgreementsDeriver.php b/modules/gdpr_consent/src/Plugin/Deriver/GdprMyAgreementsDeriver.php new file mode 100644 index 0000000..9f9971e --- /dev/null +++ b/modules/gdpr_consent/src/Plugin/Deriver/GdprMyAgreementsDeriver.php @@ -0,0 +1,27 @@ +derivatives['contacts_dashboard'] = $base_plugin_definition; + $this->derivatives['contacts_dashboard']['admin_label'] = 'GDPR Agreements Accepted'; + $this->derivatives['contacts_dashboard']['context'] = [ + 'user' => new ContextDefinition('entity:user', 'User', FALSE), + ]; + + return $this->derivatives; + } + +} diff --git a/modules/gdpr_consent/src/Plugin/Field/FieldFormatter/ConsentFormatter.php b/modules/gdpr_consent/src/Plugin/Field/FieldFormatter/ConsentFormatter.php new file mode 100644 index 0000000..09fc853 --- /dev/null +++ b/modules/gdpr_consent/src/Plugin/Field/FieldFormatter/ConsentFormatter.php @@ -0,0 +1,44 @@ +getStorage('gdpr_consent_agreement'); + + foreach ($items as $delta => $item) { + $agreement = $storage->loadRevision($item->target_revision_id); + + $output[$delta] = [ + 'name' => [ + '#markup' => $agreement->toLink($agreement->title->value, 'revision')->toString() . ' on ' . $item->date, + ], + ]; + + } + + return $output; + } + +} diff --git a/modules/gdpr_consent/src/Plugin/Field/FieldType/UserConsentItem.php b/modules/gdpr_consent/src/Plugin/Field/FieldType/UserConsentItem.php new file mode 100644 index 0000000..ff730c0 --- /dev/null +++ b/modules/gdpr_consent/src/Plugin/Field/FieldType/UserConsentItem.php @@ -0,0 +1,186 @@ + ''] + + parent::defaultFieldSettings(); + } + + /** + * {@inheritdoc} + */ + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $properties['target_id'] = DataReferenceTargetDefinition::create('integer') + ->setLabel('Target agreement ID') + ->setSetting('unsigned', TRUE) + ->setRequired(TRUE); + + $properties['target_revision_id'] = DataDefinition::create('integer') + ->setLabel('Revision ID'); + + $properties['agreed'] = DataDefinition::create('boolean') + ->setLabel('Agreed'); + + $properties['date'] = DataDefinition::create('datetime_iso8601') + ->setLabel('Date stored'); + + $properties['user_id'] = DataReferenceTargetDefinition::create('integer') + ->setLabel('User ID'); + + $properties['user_id_accepted'] = DataReferenceTargetDefinition::create('integer') + ->setLabel('User ID Accepted'); + + $properties['notes'] = DataReferenceTargetDefinition::create('string') + ->setLabel('Notes'); + + return $properties; + } + + /** + * {@inheritdoc} + */ + public function preSave() { + $definition = $this->getFieldDefinition(); + + /* @var \Drupal\gdpr_consent\ConsentUserResolver\ConsentUserResolverPluginManager $plugin_manager */ + $plugin_manager = \Drupal::service('plugin.manager.gdpr_consent_resolver'); + $resolver = $plugin_manager->getForEntityType($definition->getTargetEntityTypeId(), $definition->getTargetBundle()); + $user = $resolver->resolve($this->getEntity()); + + if ($user != NULL) { + $this->set('user_id', $user->id()); + } + } + + /** + * @inheritDoc + */ + public function postSave($update) { + $should_log = FALSE; + + if (!$update) { + // Always log on a create. + $should_log = TRUE; + } + else { + $field_name = $this->getFieldDefinition()->getName(); + $original_value = $this->getEntity()->original->{$field_name}->agreed; + if ($original_value != $this->agreed) { + $should_log = TRUE; + } + } + + if ($should_log) { + $msg = Message::create(['template' => 'consent_agreement_accepted']); + $msg->set('user', $this->user_id); + $msg->set('user_accepted', $this->user_id_accepted); + $msg->set('agreement', ['target_id' => $this->target_id, 'target_revision_id' => $this->target_revision_id]); + $msg->set('notes', $this->notes); + $msg->set('agreed', $this->agreed); + $msg->save(); + } + } + + /** + * {@inheritdoc} + */ + public function fieldSettingsForm(array $form, FormStateInterface $form_state) { + + $agreement_ids = \Drupal::entityQuery('gdpr_consent_agreement') + ->condition('status', 1) + ->sort('title') + ->execute(); + + $agreements = ConsentAgreement::loadMultiple($agreement_ids); + + $element = []; + + $element['target_id'] = [ + '#type' => 'select', + '#title' => 'Agreement', + '#required' => TRUE, + '#options' => ['' => 'Please select'] + $agreements, + '#default_value' => $this->getSetting('target_id'), + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + $schema = [ + 'indexes' => [ + 'target_id' => ['target_id'], + ], + ]; + + $schema['columns']['target_id'] = [ + 'description' => 'The ID of the target entity.', + 'type' => 'int', + 'unsigned' => TRUE, + ]; + + $schema['columns']['target_revision_id'] = [ + 'description' => 'The Revision ID of the target entity.', + 'type' => 'int', + ]; + + $schema['columns']['agreed'] = [ + 'description' => 'Whether the user has agreed.', + 'type' => 'int', + 'size' => 'tiny', + 'default' => 0, + ]; + + $schema['columns']['user_id'] = [ + 'description' => 'ID of the user who has accepted.', + 'type' => 'int', + ]; + + $schema['columns']['date'] = [ + 'description' => 'Time that the user agreed.', + 'type' => 'varchar', + 'length' => 20, + ]; + + $schema['columns']['user_id_accepted'] = [ + 'description' => 'ID of the user who recorded the acceptance', + 'type' => 'int', + ]; + + $schema['columns']['notes'] = [ + 'description' => 'Additional notes on the acceptance', + 'type' => 'varchar', + 'length' => '255', + ]; + return $schema; + } + +} diff --git a/modules/gdpr_consent/src/Plugin/Field/FieldWidget/ConsentWidget.php b/modules/gdpr_consent/src/Plugin/Field/FieldWidget/ConsentWidget.php new file mode 100644 index 0000000..f37fbd5 --- /dev/null +++ b/modules/gdpr_consent/src/Plugin/Field/FieldWidget/ConsentWidget.php @@ -0,0 +1,148 @@ +hasPermission('grant gdpr any consent'); + $can_edit_own_consent = \Drupal::currentUser()->hasPermission('grant gdpr own consent'); + // Consenting user and current user may not be the same. + // For example, a staff member editing consent on behalf of a user who + // calls the office. + $current_user = \Drupal::currentUser(); + $consenting_user = $this->getConsentingUser($items); + + $agreement_id = $items->getFieldDefinition()->getSetting('target_id'); + + if ($agreement_id == '') { + // Don't display if an agreement hasn't + // been configured for this field yet. + return []; + } + + if (!$can_edit_anyones_consent && $consenting_user->id() != $current_user->id()) { + // Abort if the current user does not have permission + // to edit other user's consent and we're editing another user. + return []; + } + + if (!$can_edit_own_consent && $consenting_user->id() == $current_user->id()) { + // Abort if the current user cannot edit their own consent. + return []; + } + + $agreement = ConsentAgreement::load($agreement_id); + $item = $items[$delta]; + + $element['target_id'] = [ + '#type' => 'hidden', + '#default_value' => $agreement_id, + ]; + + $element['target_revision_id'] = [ + '#type' => 'hidden', + '#default_value' => isset($item->target_revision_id) ? $item->target_revision_id : $agreement->getRevisionId(), + ]; + + $element['agreed'] = [ + '#type' => 'checkbox', + '#title' => $agreement->get('description')->value, + '#description' => $agreement->get('long_description')->value, + '#required' => $items->getFieldDefinition()->isRequired(), + '#default_value' => isset($item->agreed) && $item->agreed == TRUE, + '#attributes' => ['class' => ['gdpr_consent_agreement']], + '#attached' => [ + 'library' => [ + 'gdpr_consent/gdpr_consent_display', + ], + ], + ]; + + // If we only require implicit agreement, + // hide the checkbox and set it to true. + if (!$agreement->requiresExplicitAcceptance()) { + $element['agreed']['#title'] = ''; + $element['agreed']['#type'] = 'item'; + // Just render an empty span that the javascript can hook onto. + $element['agreed']['#markup'] = + ''; + $element['agreed']['#default_value'] = TRUE; + } + + // Only show the notes field if the user has permission. + if ($can_edit_anyones_consent) { + $element['notes'] = [ + '#type' => 'textarea', + '#title' => 'GDPR Consent Notes', + '#required' => FALSE, + '#default_value' => isset($item->notes) ? $item->notes : '', + ]; + } + + return $element; + } + + /** + * {@inheritdoc} + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + for ($i = 0; $i < count($values); ++$i) { + if (!isset($values[$i]['user_id_accepted'])) { + $values[$i]['user_id_accepted'] = \Drupal::currentUser()->id(); + } + if (!isset($values[$i]['date'])) { + $values[$i]['date'] = date('Y-m-d H:i:s'); + } + } + return $values; + } + + /** + * Gets the user who the consent will be stored against. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * The field. + * + * @return \Drupal\user\Entity\User + * The user + * + * @throws \Exception + */ + private function getConsentingUser(FieldItemListInterface $items) { + $definition = $items->getFieldDefinition(); + /* @var \Drupal\gdpr_consent\ConsentUserResolver\ConsentUserResolverPluginManager $plugin_manager */ + $plugin_manager = \Drupal::service('plugin.manager.gdpr_consent_resolver'); + $resolver = $plugin_manager->getForEntityType($definition->getTargetEntityTypeId(), $definition->getTargetBundle()); + $user = $resolver->resolve($items->getEntity()); + return $user; + } + +} diff --git a/modules/gdpr_consent/src/Plugin/Gdpr/ConsentUserResolver/ProfileResolver.php b/modules/gdpr_consent/src/Plugin/Gdpr/ConsentUserResolver/ProfileResolver.php new file mode 100644 index 0000000..f89065e --- /dev/null +++ b/modules/gdpr_consent/src/Plugin/Gdpr/ConsentUserResolver/ProfileResolver.php @@ -0,0 +1,29 @@ +uid->entity; + } + +} diff --git a/modules/gdpr_consent/src/Plugin/Gdpr/ConsentUserResolver/UserResolver.php b/modules/gdpr_consent/src/Plugin/Gdpr/ConsentUserResolver/UserResolver.php new file mode 100644 index 0000000..3ac52fd --- /dev/null +++ b/modules/gdpr_consent/src/Plugin/Gdpr/ConsentUserResolver/UserResolver.php @@ -0,0 +1,29 @@ +hasPermission('view gdpr fields')) { + $items['gdpr']['tray']['links']['#links']['fields'] = [ + 'title' => t('Field list'), + 'url' => Url::fromRoute('gdpr_fields.fields_list'), + 'attributes' => [ + 'title' => t('Field list'), + ], + 'weight' => 100, + ]; + } +} /** * Implements hook_form_FORM_ID_alter(). @@ -22,6 +41,11 @@ 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. + // Do not add GDPR settings to the GDPR Consent Agreement form. + if ($field->getType() == 'gdpr_user_consent') { + return; + } + $form['field']['gdpr_fields'] = [ '#type' => 'details', '#title' => t('GDPR field settings'), diff --git a/modules/gdpr_fields/src/GDPRCollector.php b/modules/gdpr_fields/src/GDPRCollector.php index 2d8dbd8..256ec95 100644 --- a/modules/gdpr_fields/src/GDPRCollector.php +++ b/modules/gdpr_fields/src/GDPRCollector.php @@ -265,7 +265,8 @@ public function fieldValues($entity_type = 'user', EntityInterface $entity, $ext $bundle_id = $entity->bundle(); if ($bundle_type) { $bundle_storage = $this->entityTypeManager->getStorage($bundle_type); - $bundle_label = $bundle_storage->load($bundle_id)->label(); + $bundle_entity = $bundle_storage->load($bundle_id); + $bundle_label = $bundle_entity == NULL ? '' : $bundle_entity->label(); } else { $bundle_label = $entity->getEntityType()->getLabel(); diff --git a/modules/gdpr_tasks/gdpr_tasks.info.yml b/modules/gdpr_tasks/gdpr_tasks.info.yml index 68919c1..bc48549 100644 --- a/modules/gdpr_tasks/gdpr_tasks.info.yml +++ b/modules/gdpr_tasks/gdpr_tasks.info.yml @@ -6,3 +6,4 @@ package: General Data Protection Regulation dependencies: - gdpr:gdpr + - gdpr:dump diff --git a/modules/gdpr_tasks/gdpr_tasks.module b/modules/gdpr_tasks/gdpr_tasks.module index 4bd176f..d81d694 100644 --- a/modules/gdpr_tasks/gdpr_tasks.module +++ b/modules/gdpr_tasks/gdpr_tasks.module @@ -15,65 +15,30 @@ use Drupal\gdpr_tasks\TaskManager; /** * Implements hook_toolbar(). */ -function gdpr_tasks_toolbar() { +function gdpr_tasks_toolbar_alter(&$items) { $user = \Drupal::currentUser(); - $items = []; - $items['gdpr'] = [ - '#cache' => [ - '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'), - ], + // Root menu url. + $items['gdpr']['tab']['#url'] = Url::fromRoute('gdpr_tasks.summary'); + + $items['gdpr']['tray']['links']['#links']['summary'] = [ + 'title' => t('Summary'), + 'url' => Url::fromRoute('gdpr_tasks.summary'), + 'attributes' => [ + 'title' => t('GDPR Summary'), ], + 'weight' => 0, ]; - $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', - ], + + $items['gdpr']['tray']['links']['#links']['tasks'] = [ + 'title' => t('Tasks'), + 'url' => Url::fromRoute('entity.gdpr_task.collection'), + 'attributes' => [ + 'title' => t('GDPR Task list'), ], ]; } - - return $items; } /** diff --git a/modules/gdpr_tasks/src/Anonymizer.php b/modules/gdpr_tasks/src/Anonymizer.php index e5d4497..bdd3cfa 100644 --- a/modules/gdpr_tasks/src/Anonymizer.php +++ b/modules/gdpr_tasks/src/Anonymizer.php @@ -352,7 +352,7 @@ private function checkExportDirectoryExists() { * Log of processed fields. */ private function writeLogToFile(TaskInterface $task, array $log) { - $filename = 'GDPR_RTF_' . date('Y-m-d H-i-s') . '_' . $task->id() . '.json'; + $filename = 'GDPR_RTF_' . date('Y-m-d H-i-s') . '_' . $task->uuid() . '.json'; $dir = $this->configFactory->get(RemovalSettingsForm::CONFIG_KEY) ->get(RemovalSettingsForm::EXPORT_DIRECTORY); @@ -361,6 +361,7 @@ private function writeLogToFile(TaskInterface $task, array $log) { // Don't serialize the whole entity as we don't need all fields. $output = [ 'task_id' => $task->id(), + 'task_uuid' => $task->uuid(), 'owner_id' => $task->getOwnerId(), 'created' => $task->getCreatedTime(), 'processed_by' => $this->currentUser->id(), diff --git a/modules/gdpr_tasks/src/Controller/GDPRController.php b/modules/gdpr_tasks/src/Controller/GDPRController.php index 012d046..18aac7a 100644 --- a/modules/gdpr_tasks/src/Controller/GDPRController.php +++ b/modules/gdpr_tasks/src/Controller/GDPRController.php @@ -79,21 +79,24 @@ public function summaryPage() { * Return user to GDPR requests. */ public function requestPage(AccountInterface $user, $gdpr_task_type) { - $tasks = $this->taskManager->getUserTasks($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'); - } + 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()); + $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/Plugin/Field/FieldType/TaskLogItem.php b/modules/gdpr_tasks/src/Plugin/Field/FieldType/TaskLogItem.php index 8f11393..e6c4dad 100644 --- a/modules/gdpr_tasks/src/Plugin/Field/FieldType/TaskLogItem.php +++ b/modules/gdpr_tasks/src/Plugin/Field/FieldType/TaskLogItem.php @@ -13,6 +13,7 @@ * id = "gdpr_task_item", * label = @Translation("GDPR Removal Task Item"), * description = @Translation("GDPR Removal Task Item"), + * category = @Translation("GDPR"), * default_widget = "gdpr_task_item", * default_formatter = "gdpr_task_item" * )