diff --git a/gdpr_consent/css/gdpr_consent.css b/gdpr_consent/css/gdpr_consent.css new file mode 100644 index 0000000..0744974 --- /dev/null +++ b/gdpr_consent/css/gdpr_consent.css @@ -0,0 +1,3 @@ +.gdpr_agreed_toggle { + padding-left: 5px; +} diff --git a/gdpr_consent/gdpr_consent.info b/gdpr_consent/gdpr_consent.info new file mode 100644 index 0000000..5388034 --- /dev/null +++ b/gdpr_consent/gdpr_consent.info @@ -0,0 +1,8 @@ +name = General Data Protection Regulation (GDPR) - Consent Tracking +description = Allow tracking of GDPR Consent +core = 7.x +dependencies[] = entity +dependencies[] = gdpr +dependencies[] = message +dependencies[] = entityreference +dependencies[] = entity_token diff --git a/gdpr_consent/gdpr_consent.install b/gdpr_consent/gdpr_consent.install new file mode 100644 index 0000000..4638757 --- /dev/null +++ b/gdpr_consent/gdpr_consent.install @@ -0,0 +1,606 @@ + 'Base table for GDPR Consent Agreement entity.', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'Primary key of the GDPR Consent Agreement entity.', + ), + 'name' => array( + 'description' => 'The machine-readable name of this consent agreement.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + ), + 'revision_id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + 'description' => 'The ID of consent agreement\'s default revision.', + ), + 'title' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Title of the consent agreement.', + ), + 'created' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The Unix timestamp of the entity creation time.', + ), + 'changed' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'The Unix timestamp the entity was last edited.', + ), + 'author_uid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + 'description' => "The {users}.uid of the consent author.", + ), + 'agreement_type' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + 'description' => 'Consent agreement\'s type: implicit (0) or explicit (1).', + ), + 'status' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + 'description' => 'Entity status.', + ), + 'module' => array( + 'description' => 'The name of the providing module if the entity has been defined in code.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + ), + 'foreign keys' => array( + 'author_uid' => array( + 'table' => 'users', + 'columns' => array('author_uid' => 'uid'), + ), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'name' => array('name'), + 'revision_id' => array('revision_id'), + ), + 'indexes' => array( + 'author_uid' => array('author_uid'), + 'agreement_type' => array('agreement_type'), + ), + ); + + $schema['gdpr_consent_agreement_revision'] = array( + 'description' => 'GDPR Consent Agreement entity revisions.', + 'fields' => array( + 'id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + 'description' => 'The ID of the GDPR Consent Agreement entity.', + ), + 'revision_id' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key: Unique revision ID.', + ), + 'title' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Title of the consent agreement.', + ), + 'timestamp' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'A Unix timestamp indicating when this version was created.', + ), + 'author_uid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + 'description' => "The {users}.uid of the consent author.", + ), + 'revision_uid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + 'description' => "The {users}.uid of the revising user.", + ), + 'description' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'description' => 'A description of the consent agreement.', + ), + 'long_description' => array( + 'type' => 'text', + 'size' => 'medium', + 'not null' => TRUE, + 'description' => 'A long description of the consent agreement.', + ), + 'agreement_type' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + 'description' => 'Consent agreement\'s type: implicit or explicit.', + ), + 'notes' => array( + 'type' => 'text', + 'size' => 'medium', + 'not null' => FALSE, + 'description' => 'Notes for staff to put their rationale for why they have done this for auditors.', + ), + 'log' => array( + 'type' => 'text', + 'not null' => TRUE, + 'size' => 'big', + 'description' => 'The log entry explaining the changes in this version.', + ), + ), + 'primary key' => array('revision_id'), + 'indexes' => array( + 'id' => array('id'), + 'revision_uid' => array('revision_uid'), + 'author_uid' => array('author_uid'), + 'agreement_type' => array('agreement_type'), + ), + 'foreign keys' => array( + 'author_uid' => array( + 'table' => 'users', + 'columns' => array('author_uid' => 'uid'), + ), + 'revision_uid' => array( + 'table' => 'users', + 'columns' => array('revision_uid' => 'uid'), + ), + ), + ); + + return $schema; +} + +/** + * Implements hook_field_schema(). + */ +function gdpr_consent_field_schema($field) { + $columns = array( + 'target_id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'target_revision_id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'agreed' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ), + 'date' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'consenter' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + 'description' => 'The user uid of the user giving consent', + ), + 'recorder' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + 'description' => 'The user uid of the user recording the consent', + ), + 'notes' => array( + 'type' => 'text', + 'size' => 'medium', + 'not null' => FALSE, + ), + ); + $indexes = array( + 'target_id' => array('target_id'), + 'target_revision_id' => array('target_revision_id'), + 'agreed' => array('agreed' => 'agreed'), + 'consenter' => array('consenter' => 'consenter'), + 'recorder' => array('recorder' => 'recorder'), + ); + return array( + 'columns' => $columns, + 'indexes' => $indexes, + 'foreign keys' => array( + 'target_id' => array( + 'table' => 'gdpr_consent_agreement', + 'columns' => array('target_id' => 'id'), + ), + 'target_revision_id' => array( + 'table' => 'gdpr_consent_agreement_revision', + 'columns' => array('target_revision_id' => 'revision_id'), + ), + 'consenter' => array( + 'table' => 'users', + 'columns' => array('consenter' => 'uid'), + ), + 'recorder' => array( + 'table' => 'users', + 'columns' => array('recorder' => 'uid'), + ), + ), + ); +} + +/** + * Implements hook_install(). + */ +function gdpr_consent_install() { + $field_bases = array(); + + // Exported field_base: 'agreed'. + $field_bases['agreed'] = array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'agreed', + 'indexes' => array( + 'value' => array( + 0 => 'value', + ), + ), + 'locked' => 0, + 'module' => 'list', + 'settings' => array( + 'allowed_values' => array( + 0 => 'No', + 1 => 'Yes', + ), + 'allowed_values_function' => '', + ), + 'translatable' => 0, + 'type' => 'list_boolean', + ); + // + // Exported field_base: 'agreement'. + $field_bases['agreement'] = array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'agreement', + 'indexes' => array( + 'target_id' => array( + 0 => 'target_id', + ), + ), + 'locked' => 0, + 'module' => 'entityreference', + 'settings' => array( + 'handler' => 'base', + 'handler_settings' => array( + 'sort' => array( + 'type' => 'none', + ), + 'target_bundles' => array(), + ), + 'target_type' => 'gdpr_consent_agreement', + ), + 'translatable' => 0, + 'type' => 'entityreference', + ); + + // Exported field_base: 'notes'. + $field_bases['notes'] = array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'notes', + 'indexes' => array( + 'format' => array( + 0 => 'format', + ), + ), + 'locked' => 0, + 'module' => 'text', + 'settings' => array(), + 'translatable' => 0, + 'type' => 'text_long', + ); + + // Exported field_base: 'owner_user'. + $field_bases['owner_user'] = array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'owner_user', + 'indexes' => array( + 'target_id' => array( + 0 => 'target_id', + ), + ), + 'locked' => 0, + 'module' => 'entityreference', + 'settings' => array( + 'handler' => 'base', + 'handler_settings' => array( + 'sort' => array( + 'type' => 'none', + ), + 'target_bundles' => array(), + ), + 'target_type' => 'user', + ), + 'translatable' => 0, + 'type' => 'entityreference', + ); + + // Exported field_base: 'user_accepted'. + $field_bases['user_accepted'] = array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'user_accepted', + 'indexes' => array( + 'target_id' => array( + 0 => 'target_id', + ), + ), + 'locked' => 0, + 'module' => 'entityreference', + 'settings' => array( + 'handler' => 'base', + 'handler_settings' => array( + 'sort' => array( + 'type' => 'none', + ), + 'target_bundles' => array(), + ), + 'target_type' => 'node', + ), + 'translatable' => 0, + 'type' => 'entityreference', + ); + + foreach ($field_bases as $field_base) { + if (!field_info_field($field_base['field_name'])) { + field_create_field($field_base); + } + } + + $field_instances = array(); + + // Exported field_instance: 'message-consent_agreement_accepted-agreed'. + $field_instances['message-consent_agreement_accepted-agreed'] = array( + 'default_value' => array( + 0 => array( + 'value' => 0, + ), + ), + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'list', + 'settings' => array(), + 'type' => 'list_default', + 'weight' => 0, + ), + ), + 'entity_type' => 'message', + 'field_name' => 'agreed', + 'label' => 'Agreed', + 'required' => 0, + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'options', + 'settings' => array( + 'display_label' => 0, + ), + 'type' => 'options_onoff', + 'weight' => 1, + ), + ); + + // Exported field_instance: + // 'message-consent_agreement_accepted-agreement'. + $field_instances['message-consent_agreement_accepted-agreement'] = array( + 'default_value' => NULL, + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'entityreference', + 'settings' => array( + 'bypass_access' => FALSE, + 'link' => FALSE, + ), + 'type' => 'entityreference_label', + 'weight' => 1, + ), + ), + 'entity_type' => 'message', + 'field_name' => 'agreement', + 'label' => 'Agreement', + 'required' => 0, + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'options', + 'settings' => array(), + 'type' => 'options_select', + 'weight' => 2, + ), + ); + + // Exported field_instance: 'message-consent_agreement_accepted-notes'. + $field_instances['message-consent_agreement_accepted-notes'] = array( + 'default_value' => NULL, + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'text', + 'settings' => array(), + 'type' => 'text_default', + 'weight' => 2, + ), + ), + 'entity_type' => 'message', + 'field_name' => 'notes', + 'label' => 'Notes', + 'required' => 0, + 'settings' => array( + 'text_processing' => 0, + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'text', + 'settings' => array( + 'rows' => 5, + ), + 'type' => 'text_textarea', + 'weight' => 3, + ), + ); + + // Exported field_instance: 'message-consent_agreement_accepted-owner_user'. + $field_instances['message-consent_agreement_accepted-owner_user'] = array( + 'default_value' => NULL, + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'entityreference', + 'settings' => array( + 'bypass_access' => FALSE, + 'link' => FALSE, + ), + 'type' => 'entityreference_label', + 'weight' => 3, + ), + ), + 'entity_type' => 'message', + 'field_name' => 'owner_user', + 'label' => 'User', + 'required' => 0, + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'entityreference', + 'settings' => array( + 'match_operator' => 'CONTAINS', + 'path' => '', + 'size' => 60, + ), + 'type' => 'entityreference_autocomplete', + 'weight' => 4, + ), + ); + + // Exported field_instance: + // 'message-consent_agreement_accepted-user_accepted'. + $field_instances['message-consent_agreement_accepted-user_accepted'] = array( + 'default_value' => NULL, + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'entityreference', + 'settings' => array( + 'bypass_access' => FALSE, + 'link' => FALSE, + ), + 'type' => 'entityreference_label', + 'weight' => 4, + ), + ), + 'entity_type' => 'message', + 'field_name' => 'user_accepted', + 'label' => 'User Accepted', + 'required' => 0, + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'entityreference', + 'settings' => array( + 'match_operator' => 'CONTAINS', + 'path' => '', + 'size' => 60, + ), + 'type' => 'entityreference_autocomplete', + 'weight' => 5, + ), + ); + + $bundles = array('consent_agreement_accepted', 'consent_agreement_revoked'); + + foreach ($field_instances as $field_instance) { + foreach ($bundles as $bundle) { + if (!field_info_instance($field_instance['entity_type'], $field_instance['field_name'], $bundle)) { + $field_instance['bundle'] = $bundle; + field_create_instance($field_instance); + } + } + } +} diff --git a/gdpr_consent/gdpr_consent.module b/gdpr_consent/gdpr_consent.module new file mode 100644 index 0000000..5065c66 --- /dev/null +++ b/gdpr_consent/gdpr_consent.module @@ -0,0 +1,794 @@ + 'Agreements', + 'description' => 'List Agreement Entities', + 'page callback' => 'gdpr_consent_collected_agreements', + 'page arguments' => array(1), + 'access arguments' => array('manage gdpr agreements'), + 'menu_name' => 'navigation', + 'file' => 'includes/gdpr_consent.agreements.inc', + ); + $items['admin/gdpr/agreements/%gdpr_consent_agreement'] = array( + 'title' => 'Consent Agreement', + 'page callback' => 'gdpr_consent_agreement_view_entity', + //'page callback' => 'entity_ui_entity_page_view', + 'page arguments' => array(3), + 'access callback' => TRUE, + 'file' => 'includes/gdpr_consent.agreements.inc', + ); + // 'View' tab for an individual entity. + $items['admin/gdpr/agreements/%gdpr_consent_agreement/view'] = array( + 'title' => 'View', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + // 'Edit' tab for an individual entity. + $items['admin/gdpr/agreements/%gdpr_consent_agreement/edit'] = array( + 'title' => 'Edit', + 'page callback' => 'entity_ui_get_form', + 'page arguments' => array('gdpr_consent_agreement', 3), + 'access arguments' => array('manage gdpr agreements'), + 'type' => MENU_LOCAL_TASK, + 'weight' => -5, + 'file' => 'includes/gdpr_consent.admin.inc', + ); + // 'Revisions' tab for an individual entity. + $items['admin/gdpr/agreements/%gdpr_consent_agreement/revisions'] = array( + 'title' => 'Revisions', + 'page callback' => 'gdpr_consent_agreement_revision_overview', + 'page arguments' => array(3), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('manage gdpr agreements'), + 'file' => 'includes/gdpr_consent.admin.inc', + 'weight' => -3, + ); + // An individual revision view page. + $items['admin/gdpr/agreements/%gdpr_consent_agreement/revisions/%/view'] = array( + 'title' => 'Revision', + 'page callback' => 'gdpr_consent_agreement_view_revision', + 'page arguments' => array(5), + 'access arguments' => array('manage gdpr agreements'), + 'file' => 'includes/gdpr_consent.agreements.inc', + 'weight' => -3, + ); + // 'Delete' tab for an individual entity. + $items['admin/gdpr/agreements/%gdpr_consent_agreement/delete'] = array( + 'title' => 'Delete', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('gdpr_consent_agreement_form', 3), + 'access arguments' => array('manage gdpr agreements'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'includes/gdpr_consent.admin.inc', + 'weight' => 0, + ); + + return $items; +} + +/** + * Implements hook_permission(). + */ +function gdpr_consent_permission() { + $permissions = array( + 'manage gdpr agreements' => array( + 'title' => t('Manage GDPR Agreements'), + ), + 'grant any consent' => array( + 'title' => t('Grant Any Consent'), + ), + 'grant own consent' => array( + 'title' => t('Grant Own Consent'), + ), + ); + + return $permissions; +} + +/** + * Implements hook_entity_info(). + */ +function gdpr_consent_entity_info() { + $info = array(); + $info['gdpr_consent_agreement'] = array( + 'label' => t('GDPR Consent Agreement'), + 'base table' => 'gdpr_consent_agreement', + 'revision table' => 'gdpr_consent_agreement_revision', + 'entity class' => 'ConsentAgreement', + 'controller class' => 'ConsentAgreementController', + 'fieldable' => TRUE, + 'bundles' => array( + 'gdpr_consent_agreement' => array( + 'label' => t('GDPR Consent Agreement'), + 'admin' => array( + 'path' => 'admin/gdpr/agreements', + 'access arguments' => array('administer site configuration'), + ), + ), + ), + 'entity keys' => array( + 'id' => 'id', + 'name' => 'name', + 'label' => 'title', + 'revision' => 'revision_id', + ), + 'uri callback' => 'entity_class_uri', + 'access callback' => 'gdpr_consent_access_callback', + 'admin ui' => array( + 'path' => 'admin/gdpr/agreements', + 'controller class' => 'ConsentAgreementEntityUIController', + 'menu wildcard' => '%gdpr_consent_agreement', + 'file' => 'includes/gdpr_consent.admin.inc', + ), + 'module' => 'gdpr_consent', + ); + + return $info; +} + +/** + * Implements hook_entity_property_info(). + */ +function gdpr_consent_entity_property_info() { + $info = array(); + $properties = &$info['gdpr_consent_agreement']['properties']; + + $properties['id'] = array( + 'label' => t('GDPR Consent Agreement ID'), + 'description' => t('The uniquie ID of the consent agreement entity.'), + 'type' => 'integer', + 'schema field' => 'id', + ); + + $properties['title'] = array( + 'label' => t('Title'), + 'description' => t('Title of the agreement'), + 'type' => 'text', + 'schema field' => 'title', + ); + + $properties['agreement_type'] = array( + 'label' => t('Agreement Type'), + 'description' => t('Whether consent is implicit or explicit. Set to "Explicit" if the user needs to explicitly agree, otherwise "Implicit'), + 'type' => 'boolean', + 'schema field' => 'agreement_type', + ); + + $properties['description'] = array( + 'label' => t('Description'), + 'description' => t('Text displayed to the user on the form'), + 'type' => 'text', + 'schema field' => 'description', + ); + + $properties['long_description'] = array( + 'label' => t('Long Description'), + 'description' => t('Text shown when the user clicks for more details'), + 'type' => 'text', + 'schema field' => 'long_description', + ); + + $properties['notes'] = array( + 'label' => t('Notes'), + 'description' => t('This should contain the rationale behind the agreement.'), + 'type' => 'text', + 'schema field' => 'notes', + ); + + $properties['created'] = array( + 'label' => t('Created date'), + 'description' => t('Date the consent agreement was created'), + 'type' => 'date', + 'schema field' => 'created', + ); + + $properties['changed'] = array( + 'label' => t('Updated date'), + 'description' => t('Date the consent agreement was last edited'), + 'type' => 'date', + 'schema field' => 'changed', + ); + + $properties['author_uid'] = array( + 'label' => t('Authored by'), + 'description' => t('The user ID of author of the Consent Agreement entity'), + 'type' => 'user', + 'schema field' => 'author_uid', + ); + + $properties['revision_uid'] = array( + 'label' => t('Revised by'), + 'description' => t('The user ID of author of the Consent Agreement revision'), + 'type' => 'user', + 'schema field' => 'revision_uid', + ); + + + $properties['status'] = array( + 'label' => t('Publishing status'), + 'description' => t('A boolean indicating whether the Consent Agreement is published.'), + 'type' => 'boolean', + 'schema field' => 'status', + ); + + return $info; +} + +/** + * Access callback for agreements. + */ +function gdpr_consent_access_callback($op, $entity = NULL, $account = NULL) { + return user_access('manage gdpr agreements'); +} + +/** + * Menu autoloader for gdpr agreements. + */ +function gdpr_consent_agreement_load($id, $revision_id = NULL) { + if (is_numeric($revision_id)) { + $entity = entity_revision_load('gdpr_consent_agreement', $revision_id); + return $entity; + } + else { + $entity = entity_load_single('gdpr_consent_agreement', $id); + } + return $entity; +} + +/** + * Loads multiple agreements. + */ +function gdpr_consent_agreement_load_multiple($ids = array(), $conditions = array(), $reset = FALSE) { + if (empty($ids)) { + $ids = FALSE; + } + + return entity_load('gdpr_consent_agreement', $ids, $conditions, $reset); +} + +/** + * Gets an array of all agreements, keyed by the machine name. + */ +function gdpr_consent_agreement_load_multiple_by_name($name = NULL) { + $signups = entity_load_multiple_by_name('gdpr_consent_agreement', isset($name) ? array($name) : FALSE); + return isset($name) ? reset($signups) : $signups; +} + +/** + * Implements hook_field_info(). + * + * Provides a user consent field type. + */ +function gdpr_consent_field_info() { + return array( + 'gdpr_user_consent' => array( + 'label' => t('GDPR Consent'), + 'description' => t('Stores user consent for a particular agreement'), + 'default_widget' => 'gdpr_consent_widget', + 'default_formatter' => 'gdpr_consent_formatter', + 'property_type' => 'struct', + 'property_callbacks' => array( + 'gdpr_consent_field_gdpr_user_consent_property_callback', + ), + ), + ); +} + +/** + * Property info for gdpr consent fields. + */ +function gdpr_consent_field_gdpr_user_consent_property_callback(&$info, $entity_type, $field, $instance, $field_type) { + $property = &$info[$entity_type]['bundles'][$instance['bundle']['properties'][$field['field_name']]; + + $property['getter callback'] = 'entity_metadata_field_verbatim_get'; + $property['setter callback'] = 'entity_metadata_field_verbatim_set'; + $property['auto creation'] = 'entity_property_create_array'; + $property['type'] = ($field['cardinality'] != 1) ? 'list' : 'struct'; + + $property['property info']['target_id'] = array( + 'label' => 'Agreement ID', + 'type' => 'int', + 'getter callback' => 'entity_metadata_verbatim_get', + 'schema field' => 'target_id', + ); + $property['property info']['target_revision_id'] = array( + 'label' => 'Agreement Revision ID', + 'type' => 'int', + 'getter callback' => 'entity_metadata_verbatim_get', + 'schema field' => 'target_revision_id', + ); + $property['property info']['entity'] = array( + 'label' => 'Consent Agreement', + 'type' => 'gdpr_consent_agreement', + 'getter callback' => 'gdpr_consent_field_property_entity_get', + ); + $property['property info']['date'] = array( + 'label' => 'Date Agreed', + 'type' => 'date', + 'getter callback' = 'entity_metadata_verbatim_get', + 'schema field' => 'date', + ); + $property['property info']['agreed'] = array( + 'label' => 'Agreed', + 'type' => 'boolean', + 'getter callback' => 'entity_metadata_verbatim_get', + 'schema field' => 'agreed', + ); + $property['property info']['consenter'] = array( + 'label' => 'The Consenting User', + 'type' => 'user', + 'getter callback' => 'entity_metadata_verbatim_get', + 'schema field' => 'consenter', + ); + $property['property info']['recorder'] = array( + 'label' => 'The User recording the Consent', + 'type' => 'user', + 'getter callback' => 'entity_metadata_verbatim_get', + 'schema field' => 'recorder', + ); + $property['property info']['notes'] = array( + 'label' => 'Notes', + 'type' => 'text', + 'getter callback' => 'entity_metadata_verbatim_get', + 'schema field' => 'notes', + ); +} + +/** + * Getter callback for the consent agreement entity. + */ +function gdpr_consent_field_property_entity_get($data, array $options, $name, $type, $info) { + if ( + !empty($data['target_id']) + && !empty($data['target_revision_id']) + && ($consent = entity_revision_load('gdpr_consent_agreement', $data['target_revision_id'])) + ) { + return $consent; + } +} + +/** + * Implements hook_field_validate(). + * + * @TODO: This validate function isn't doing anything. Please fix. + */ +function gdpr_consent_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) { + $valid = TRUE; + + foreach ($items as $delta => $item) { + if (!empty($item['target_id'])) { + if (!$valid) { + $errors[$field['field_name']][$langcode][$delta][] = array( + 'error' => 'gdpr_user_consent_invalid', + 'message' => t('Referenced consent agreement entity is invalid.'), + ); + } + } + } +} + +/** + * Implements hook_field_attach_submit(). + * + * If consent field changes grant/revoke consent and log audit trail. + */ +function gdpr_consent_field_attach_submit($entity_type, $entity, $form, &$form_state) { + $is_new = FALSE; + + list($id, , $bundle) = entity_extract_ids($entity_type, $entity); + // If $entity does not have an id that means it's new. + if (!isset($id)) { + $is_new = TRUE; + } + $fields = gdpr_consent_get_consent_fields($entity_type, $bundle); + + if (!$is_new) { + $original = entity_load_unchanged($entity_type, $id); + } + + foreach ($fields as $field) { + if (!empty($form_state['values'][$field])) { + $values = $form_state['values'][$field]; + $new = isset($values[LANGUAGE_NONE][0]['agreed']) ? (int) $values[LANGUAGE_NONE][0]['agreed'] : NULL; + + // Newly created entities should only log accepted agreements. + if ($is_new) { + $old = 0; + } + else { + $old = isset($original->{$field}[LANGUAGE_NONE][0]['agreed']) ? (int) $original->{$field}[LANGUAGE_NONE][0]['agreed'] : NULL; + } + + if ($new !== $old) { + global $user; + $agreement = entity_load_single('gdpr_consent_agreement', $values[LANGUAGE_NONE][0]['target_id']); + + if ($new) { + $message = message_create('consent_agreement_accepted', array('uid' => $user->uid)); + } + else { + $message = message_create('consent_agreement_revoked', array('uid' => $user->uid)); + } + + $wrapper = entity_metadata_wrapper('message', $message); + + $wrapper->owner_user->set($user->uid); + $wrapper->user_accepted->set($values[LANGUAGE_NONE][0]['recorder']); + $wrapper->agreement->set($agreement); + $wrapper->notes->set($values[LANGUAGE_NONE][0]['notes']); + $wrapper->agreed->set($values[LANGUAGE_NONE][0]['agreed']); + $wrapper->save(); + } + } + } +} + + +/** + * Fetches a list of consent fields for an entity. + * + * @param string $entity_type + * The entity type to get fields for. + * @param string|null $bundle + * (Optional) the bundle to get fields for. If none provided, all bundles + * will be checked. + * + * @return array + * List of consent field names. + */ +function gdpr_consent_get_consent_fields($entity_type, $bundle = NULL) { + $instances = field_info_instances($entity_type, $bundle); + $fields = array(); + + foreach ($instances as $field_name => $instance) { + $field = field_info_field($field_name); + if ($field['type'] == 'gdpr_user_consent') { + $fields[] = $field_name; + } + } + + return $fields; +} + +/** + * Implements hook_field_is_empty(). + */ +function gdpr_consent_field_is_empty($item, $field) { + return empty($item['target_id']); +} + +/** + * Implements hook_field_formatter_info(). + */ +function gdpr_consent_field_formatter_info() { + return array( + 'gdpr_consent' => array( + 'label' => t('GDPR user consent formatter'), + 'field types' => array('gdpr_user_consent'), + ), + ); +} + +/** + * Implements hook_field_settings_form(). + */ +function gdpr_consent_field_settings_form($field, $instance, $has_data) { + $settings = $field['settings']; + + $agreements = array(); + + foreach (entity_load('gdpr_consent_agreement') as $agreement) { + $agreements[$agreement->id] = $agreement->title; + } + + $form['target_id'] = array( + '#type' => 'select', + '#title' => t('User consent agreement'), + '#default_value' => (isset($settings['target_id'])) ? $settings['target_id'] : '', + '#required' => FALSE, + '#options' => $agreements, + '#element_validate' => array( + 'element_validate_integer_positive', + ), + '#description' => t('The GDPR User Consent Agreement to display'), + ); + + return $form; +} + +/** + * Implements hook_field_formatter_view(). + */ +function gdpr_consent_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + $element = array(); + + switch ($display['type']) { + case 'gdpr_consent': + foreach ($items as $delta => $item) { + $element[$delta] = array( + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => t('User Consent ID: @entity', array('@entity' => $item['target_id'])), + ); + } + break; + } + + return $element; +} + +/** + * Implements hook_field_widget_info(). + * + * Field widget to show consent information. + */ +function gdpr_consent_field_widget_info() { + return array( + 'gdpr_consent_widget' => array( + 'label' => t('GDPR Consent'), + 'field types' => array('gdpr_user_consent'), + ), + ); +} + +/** + * Implements hook_field_widget_form(). + */ +function gdpr_consent_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { + global $user; + + $agreed = isset($items[$delta]['agreed']) ? $items[$delta]['agreed'] : ''; + $notes = isset($items[$delta]['notes']) ? $items[$delta]['notes'] : ''; + + $widget = $element; + $widget['#delta'] = $delta; + + // Get current revision of the referenced agreement entity. + $entity_type = 'gdpr_consent_agreement'; + $entity = entity_load_single('gdpr_consent_agreement', $field['settings']['target_id']); + + list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); + + switch ($instance['widget']['type']) { + + case 'gdpr_consent_widget': + $element['agreed'] = array( + '#type' => 'checkbox', + '#title' => $entity->title, + '#default_value' => $agreed, + '#weight' => 0, + '#attributes' => array( + 'class' => array('gdpr_consent_agreement'), + ), + '#attached' => array( + 'css' => array(drupal_get_path('module', 'gdpr_consent') . '/css/gdpr_consent.css'), + 'js' => array(drupal_get_path('module', 'gdpr_consent') . '/js/gdpr_consent.js'), + ), + '#description' => $entity->long_description, + ) + $widget; + $element['notes'] = array( + '#type' => 'textfield', + '#title' => 'Consent Notes', + '#default_value' => $notes, + '#weight' => 10, + '#description' => '', + '#access' => user_access('grant any consent', $user), + ) + $widget; + $element['target_id'] = array( + '#type' => 'hidden', + '#value' => $field['settings']['target_id'], + ) + $widget; + $element['target_revision_id'] = array( + '#type' => 'hidden', + '#value' => $vid, + ) + $widget; + $element['date'] = array( + '#type' => 'hidden', + '#value' => REQUEST_TIME, + ) + $widget; + $element['recorder'] = array( + '#type' => 'hidden', + '#value' => $user->uid, + ) + $widget; + + break; + } + + return $element; +} + +/** + * Implements hook_default_message_type(). + */ +function gdpr_consent_default_message_type() { + $items = array(); + + $items['consent_agreement_accepted'] = entity_import('message_type', '{ + "name" : "consent_agreement_accepted", + "description" : "GDPR Consent Agreement accepted", + "argument_keys" : [], + "argument" : [], + "category" : "message_type", + "data" : { + "token options" : { "clear" : 0 }, + "purge" : { "override" : 1, "enabled" : 1, "quota" : "1000", "days" : "30" } + }, + "language" : "", + "arguments" : null, + "message_text" : { "und" : [ + { + "value" : "\u003Cp\u003EAccepted Agreement: \u003Ca href=\u0022\/admin\/gdpr\/agreements\/[message:agreement:id]\u0022\u003E[message:agreement:title]\u003C\/a\u003E\u003Cbr \/\u003E\r\nAgreed: [message:agreed]\u003Cbr \/\u003E\r\nNotes: [message:notes]\u003C\/p\u003E", + "format" : "filtered_html", + "safe_value" : "\u003Cp\u003Eu003EAccepted Agreement: \u003Ca href=\u0022\/admin\/gdpr\/agreements\/[message:agreement:id]\u0022\u003E[message:agreement:title]\u003C\/a\u003E\u003Cbr \/\u003E\nAgreed: [message:agreed]\u003Cbr \/\u003E\nNotes: [message:notes]\u003C\/p\u003E\n" + } + ] + }, + "rdf_mapping" : [] + }'); + + $items['consent_agreement_revoked'] = entity_import('message_type', '{ + "name" : "consent_agreement_revoked", + "description" : "GDPR Consent Agreement revoked", + "argument_keys" : [], + "argument" : [], + "category" : "message_type", + "data" : { + "token options" : { "clear" : 0 }, + "purge" : { "override" : 1, "enabled" : 1, "quota" : "1000", "days" : "30" } + }, + "language" : "", + "arguments" : null, + "message_text" : { "und" : [ + { + "value" : "\u003Cp\u003ERevoked Agreement: \u003Ca href=\u0022\/admin\/gdpr\/agreements\/[message:agreement:id]\u0022\u003E[message:agreement:title]\u003C\/a\u003E\u003Cbr \/\u003E\r\nAgreed: [message:agreed]\u003Cbr \/\u003E\r\nNotes: [message:notes]\u003C\/p\u003E", + "format" : "filtered_html", + "safe_value" : "\u003Cp\u003ERevoked Agreement: \u003Ca href=\u0022\/admin\/gdpr\/agreements\/[message:agreement:id]\u0022\u003E[message:agreement:title]\u003C\/a\u003E\u003Cbr \/\u003E\nAgreed: [message:agreed]\u003Cbr \/\u003E\nNotes: [message:notes]\u003C\/p\u003E\n" + } + ] + }, + "rdf_mapping" : [] + }'); + + return $items; +} + +/** + * Our custom entity class. + */ +class ConsentAgreement extends Entity { + /** + * Implement a custom default URI. + */ + public function defaultUri() { + return array('path' => 'admin/gdpr/agreements/' . $this->identifier()); + } + + /** + * {@inheritdoc} + */ + public function identifier() { + return $this->internalIdentifier(); + } +} + +/** + * Custom controller for the gdpr_consent_agreement entity type. + */ +class ConsentAgreementController extends EntityAPIControllerExportable { + + /** + * {@inheritdoc} + */ + public function save($entity, DatabaseTransaction $transaction = NULL) { + if (isset($entity->is_new)) { + global $user; + + $entity->created = REQUEST_TIME; + $entity->author_uid = $user->uid; + } + + $entity->changed = REQUEST_TIME; + + // Always save new revisions. + $entity->is_new_revision = TRUE; + $entity->timestamp = REQUEST_TIME; + $entity->revision_uid = $user->uid; + + return parent::save($entity, $transaction); + } + + /** + * {@inheritdoc} + */ + public function export($entity, $prefix = '') { + $vars = get_object_vars($entity); + unset($vars[$this->statusKey], $vars[$this->moduleKey], $vars['is_new']); + if ($this->nameKey != $this->idKey) { + unset($vars[$this->idKey]); + } + if ($this->revisionKey) { + unset($vars[$this->revisionKey]); + } + return entity_var_json_export($vars, $prefix); + } + +} + +/** + * Custom UI controller for the gdpr_consent_agreement entity type. + */ +class ConsentAgreementEntityUIController extends EntityDefaultUIController { + + /** + * {@inheritdoc} + */ + public function buildContent($entity, $view_mode = 'default', $langcode = NULL, $content = array()) { + $build = parent::buildContent($entity, $view_mode, $langcode, $content); + + $build['description'] = array( + '#type' => 'markup', + '#markup' => check_plain($entity->description), + ); + + $build['long_description'] = array( + '#type' => 'markup', + '#markup' => check_plain($entity->long_description), + ); + + $build['notes'] = array( + '#type' => 'markup', + '#markup' => check_plain($entity->notes), + ); + + $build['agreement_type'] = array( + '#type' => 'markup', + '#markup' => !empty($entity->agreement_type) ? t('Explicit') : t('Implicit'), + ); + + return $build; + } + + /** + * {@inheritdoc} + */ + protected function overviewTableHeaders($conditions, $rows, $additional_header = array()) { + $additional_header = array( + t('Type'), + ); + return parent::overviewTableHeaders($conditions, $rows, $additional_header); + } + + /** + * {@inheritdoc} + */ + protected function overviewTableRow($conditions, $id, $entity, $additional_cols = array()) { + $additional_cols = array( + $entity->agreement_type ? 'Explicit' : 'Implicit', + ); + + $row = parent::overviewTableRow($conditions, $id, $entity, $additional_cols); + return $row; + } + +} diff --git a/gdpr_consent/includes/gdpr_consent.admin.inc b/gdpr_consent/includes/gdpr_consent.admin.inc new file mode 100644 index 0000000..9830c00 --- /dev/null +++ b/gdpr_consent/includes/gdpr_consent.admin.inc @@ -0,0 +1,158 @@ + t('Title'), + '#type' => 'textfield', + '#default_value' => isset($entity->title) ? $entity->title : '', + '#description' => t('Agreement Title'), + '#required' => TRUE, + '#weight' => -50, + ); + $form['name'] = array( + '#type' => 'machine_name', + '#default_value' => isset($entity->name) ? $entity->name : '', + '#disabled' => $entity->hasStatus(ENTITY_IN_CODE), + '#machine_name' => array( + 'exists' => 'gdpr_consent_agreement_load_multiple_by_name', + 'source' => array('title'), + ), + '#description' => t('A unique machine-readable name for this agreement. It must only contain lowercase letters, numbers, and underscores.'), + '#weight' => -50, + ); + $form['agreement_type'] = array( + '#title' => t('Agreement Type'), + '#type' => 'select', + '#options' => array( + 0 => t('Implicit'), + 1 => t('Explicit'), + ), + '#default_value' => isset($entity->agreement_type) ? $entity->agreement_type : '', + '#description' => t('Whether consent is implicit or explicit. Set to "Explicit" if the user needs to explicitly agree, otherwise "Implicit'), + ); + $form['description'] = array( + '#title' => t('Description'), + '#type' => 'textfield', + '#default_value' => isset($entity->description) ? $entity->description : '', + '#description' => t('Text displayed to the user on the form'), + ); + $form['long_description'] = array( + '#title' => t('Long Description'), + '#type' => 'textarea', + '#default_value' => isset($entity->long_description) ? $entity->long_description : '', + '#description' => t('Text shown when the user clicks for more details'), + ); + $form['notes'] = array( + '#title' => t('Notes'), + '#type' => 'textarea', + '#default_value' => isset($entity->notes) ? $entity->notes : '', + '#description' => t('This should contain the rationale behind the agreement.'), + ); + $form['revision'] = array( + '#type' => 'checkbox', + '#title' => t('Create new revision'), + '#default_value' => 1, + ); + $form['log'] = array( + '#type' => 'textarea', + '#title' => t('Revision log message'), + '#rows' => 4, + '#default_value' => !empty($entity->log) ? $entity->log : '', + '#description' => t('Briefly describe the changes you have made.'), + ); + + field_attach_form('gdpr_consent_agreement', $entity, $form, $form_state); + + $form['actions'] = array( + '#type' => 'actions', + 'submit' => array( + '#type' => 'submit', + '#value' => isset($entity->id) ? t('Update consent agreement') : t('Save consent agreement'), + ), + ); + + return $form; +} + +/** + * Submit handler for consent agreement entity form. + */ +function gdpr_consent_agreement_form_submit(&$form, &$form_state) { + $entity = entity_ui_form_submit_build_entity($form, $form_state); + $entity->save(); + drupal_set_message(t('@title agreement has been saved.', array('@title' => $entity->title))); + $form_state['redirect'] = 'admin/gdpr/agreements'; +} + +/** + * Generates an overview table of older revisions of a Consent Agreement. + * + * @param $agreement + * A consent agreement entity. + * + * @return array + * An array as expected by drupal_render(). + */ +function gdpr_consent_agreement_revision_overview($agreement) { + drupal_set_title(t('Revisions for @title', array('@title' => $agreement->title))); + + $header = array(t('Revision'), array('data' => t('Operations'), 'colspan' => 2)); + + $revisions = gdpr_consent_revision_list($agreement); + + $rows = array(); + + foreach ($revisions as $revision) { + $row = array(); + $operations = array(); + + if ($revision->current_revision > 0) { + $row[] = array('data' => t('!date by !username', array('!date' => l(format_date($revision->timestamp, 'short'), "admin/gdpr/agreements/$agreement->id"), '!username' => theme('username', array('account' => $revision)))) + . (($revision->log != '') ? '

' . filter_xss($revision->log) . '

' : ''), + 'class' => array('revision-current')); + $operations[] = array('data' => drupal_placeholder(t('current revision')), 'class' => array('revision-current'), 'colspan' => 2); + } + else { + $row[] = t('!date by !username', array('!date' => l(format_date($revision->timestamp, 'short'), "admin/gdpr/agreements/$agreement->id/revisions/$revision->revision_id/view"), '!username' => theme('username', array('account' => $revision)))) + . (($revision->log != '') ? '

' . filter_xss($revision->log) . '

' : ''); + $operations[] = l(t('revert'), "admin/gdpr/agreements/$agreement->id/revisions/$revision->revision_id/revert"); + $operations[] = l(t('delete'), "admin/gdpr/agreements/$agreement->id/revisions/$revision->revision_id/delete"); + } + $rows[] = array_merge($row, $operations); + } + + $build['gdpr_consent_revisions_table'] = array( + '#theme' => 'table', + '#rows' => $rows, + '#header' => $header, + ); + + return $build; +} + +/** + * Returns a list of all the existing revision numbers. + * + * @param $agreement + * The Consent Agreement entity. + * + * @return + * An associative array keyed by revision number. + */ +function gdpr_consent_revision_list($agreement) { + $revisions = array(); + $result = db_query('SELECT r.revision_id, a.title, r.log, r.uid, a.revision_id AS current_revision, r.timestamp, u.name FROM {gdpr_consent_agreement_revision} r LEFT JOIN {gdpr_consent_agreement} a ON a.revision_id = r.revision_id INNER JOIN {users} u ON u.uid = r.uid WHERE r.id = :id ORDER BY r.revision_id DESC', array(':id' => $agreement->id)); + foreach ($result as $revision) { + $revisions[$revision->revision_id] = $revision; + } + + return $revisions; +} diff --git a/gdpr_consent/includes/gdpr_consent.agreements.inc b/gdpr_consent/includes/gdpr_consent.agreements.inc new file mode 100644 index 0000000..ec6cd2e --- /dev/null +++ b/gdpr_consent/includes/gdpr_consent.agreements.inc @@ -0,0 +1,93 @@ +entityCondition('entity_type', 'gdpr_consent_agreement'); + + $result = $query->execute(); + + if (isset($result['gdpr_consent_agreement'])) { + $consent_ids = array_keys($result['gdpr_consent_agreement']); + $consent_items = entity_load('gdpr_consent_agreement', $consent_ids); + } + + return MENU_ACCESS_DENIED; +} + +/** + * Callback for /admin/gdpr/agreements/{gdpr_consent_agreement ID} page. + * + * As we load the entity for display, we're responsible for invoking a number + * of hooks in their proper order. + * + * @see hook_entity_prepare_view() + * @see hook_entity_view() + * @see hook_entity_view_alter() + */ +function gdpr_consent_agreement_view_entity($entity, $view_mode = 'default') { + // Our entity type, for convenience. + $entity_type = 'gdpr_consent_agreement'; + // Start setting up the content. + $entity->content = array( + '#view_mode' => $view_mode, + ); + // Build fields content - this is where the Field API really comes in to play. + // The task has very little code here because it all gets taken care of by + // field module. + // field_attach_prepare_view() lets the fields load any data they need + // before viewing. + field_attach_prepare_view($entity_type, array($entity->id => $entity), + $view_mode); + // We call entity_prepare_view() so it can invoke hook_entity_prepare_view() + // for us. + entity_prepare_view($entity_type, array($entity->id => $entity)); + // Now field_attach_view() generates the content for the fields. + $entity->content += field_attach_view($entity_type, $entity, $view_mode); + + // OK, Field API done, now we can set up some of our own data. + $entity->content['created'] = array( + '#type' => 'item', + '#title' => t('Created date'), + '#markup' => format_date($entity->created), + ); + $entity->content['description'] = array( + '#type' => 'item', + '#title' => t('Description'), + '#markup' => $entity->description, + ); + + // Now to invoke some hooks. We need the language code for + // hook_entity_view(), so let's get that. + global $language; + $langcode = $language->language; + // And now invoke hook_entity_view(). + module_invoke_all('entity_view', $entity, $entity_type, $view_mode, + $langcode); + // Now invoke hook_entity_view_alter(). + drupal_alter(array('gdpr_consent_agreement_view_entity', 'entity_view'), + $entity->content, $entity_type); + + // And finally return the content. + return $entity->content; +} + +/** + * Callback for /admin/gdpr/agreements/{gdpr_consent_agreement ID}/{revision ID} page. + */ +function gdpr_consent_agreement_view_revision($revision_id) { + $agreement = entity_revision_load('gdpr_consent_agreement', $revision_id); + $output = gdpr_consent_agreement_view_entity($agreement); + drupal_set_title(t('@title revision @id', array('@title' => $agreement->title, '@id' => $agreement->revision_id))); + + return $output; +} diff --git a/gdpr_consent/js/gdpr_consent.js b/gdpr_consent/js/gdpr_consent.js new file mode 100644 index 0000000..339d617 --- /dev/null +++ b/gdpr_consent/js/gdpr_consent.js @@ -0,0 +1,36 @@ +(function ($) { + $(function () { + // Hide the description for any GDPR checkboxes. + var container = $('.gdpr_consent_agreement').parent(); + var desc = container.find('.description'); + + if (!desc.length) { + container = container.parent(); + desc = container.find('.description'); + } + + desc.hide(); + + $('?') + .insertAfter(container.find('label')) + .click(function () { + var desc = $(this).find('.description'); + if (!desc.length) { + desc = $(this).parent().find('.description'); + } + + desc.slideToggle() + }); + + // Do the same for implicit + container = $('.gdpr_consent_implicit').parent(); + desc = container.next('.description'); + desc.hide(); + + $('?') + .appendTo(container) + .click(function () { + $(this).next('.description').slideToggle() + }); + }); +})(jQuery);