From c1fd9129410284282bf40d77cc274be7e130f78a Mon Sep 17 00:00:00 2001 From: Akanksha Date: Tue, 4 Jun 2024 09:53:10 -0300 Subject: [PATCH] DDST-166: Make handle suffix configurable DDST-166: Make handle suffix configurable DDST-166: Update readme DDST-166: Code sniffer fixes for the module DDST-166: Hardening DDST-166: Hardening DDST-166: More Hardening DDST-166: Rework implementation DDST-166: Clean up DDST-166: More clean-up --- .../src/Plugin/Action/DeleteArkIdentifier.php | 2 +- modules/dgi_actions_handle/README.md | 21 +++ .../schema/dgi_actions_handle.schema.yml | 4 + .../dgi_actions_handle_constraints/README.md | 63 ++++++++ ...gi_actions_handle_constraints.settings.yml | 9 ++ .../dgi_actions_handle_constraints.schema.yml | 27 ++++ .../dgi_actions_handle_constraints.info.yml | 7 + .../dgi_actions_handle_constraints.module | 137 ++++++++++++++++++ .../src/Plugin/ServiceDataType/Handle.php | 6 + .../src/Utility/HandleTrait.php | 9 +- src/Form/DataProfileDeleteForm.php | 2 +- src/Plugin/Action/IdentifierAction.php | 12 +- src/Plugin/Action/MintIdentifier.php | 2 +- src/Plugin/Condition/EntityHasIdentifier.php | 6 +- src/Plugin/DataProfileManager.php | 2 +- src/Plugin/ServiceDataTypeManager.php | 2 +- 16 files changed, 296 insertions(+), 15 deletions(-) create mode 100644 modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/README.md create mode 100644 modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/config/install/dgi_actions_handle_constraints.settings.yml create mode 100644 modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/config/schema/dgi_actions_handle_constraints.schema.yml create mode 100644 modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/dgi_actions_handle_constraints.info.yml create mode 100644 modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/dgi_actions_handle_constraints.module diff --git a/modules/dgi_actions_ark_identifier/src/Plugin/Action/DeleteArkIdentifier.php b/modules/dgi_actions_ark_identifier/src/Plugin/Action/DeleteArkIdentifier.php index f925a28..fb41b4f 100644 --- a/modules/dgi_actions_ark_identifier/src/Plugin/Action/DeleteArkIdentifier.php +++ b/modules/dgi_actions_ark_identifier/src/Plugin/Action/DeleteArkIdentifier.php @@ -3,10 +3,10 @@ namespace Drupal\dgi_actions_ark_identifier\Plugin\Action; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\dgi_actions_ezid\Utility\EzidTrait; use Drupal\dgi_actions\Plugin\Action\DeleteIdentifier; use Drupal\dgi_actions\Plugin\Action\HttpActionDeleteTrait; use Drupal\dgi_actions\Utility\IdentifierUtils; +use Drupal\dgi_actions_ezid\Utility\EzidTrait; use GuzzleHttp\ClientInterface; use GuzzleHttp\RequestOptions; use Psr\Http\Message\ResponseInterface; diff --git a/modules/dgi_actions_handle/README.md b/modules/dgi_actions_handle/README.md index 2e73e17..42b431f 100644 --- a/modules/dgi_actions_handle/README.md +++ b/modules/dgi_actions_handle/README.md @@ -16,6 +16,27 @@ Install as usual, see [this](https://www.drupal.org/docs/extending-drupal/installing-modules) for further information. +## Features + +- Provides Action Plugins for minting and deleting Handle.net Handles. +- Provides a service data type plugin for Handle.net Handles. +- Uses uuid by default as handle suffix. Also supports custom suffixes. + +## Usage +- See DGI Actions readme for general usage. +- Create a new service data type with the type `handle`. +- There is no UI for configuring the handle suffix field. It must be set in the + configuration file. The default suffix field is `uuid`. + +## User Notes + +- The field that is to be used for handle suffix must be made unique and required + in the content type configuration so that the value does not change. If the field is not unique, + handle generation will fail for the duplicate value. +- The handle also must be unique and should be non-editable. +- To make the suffix and handle fields non-editable and required, an additional module is provided + `dgi_actions_handle_constraints`. This module should be enabled and configured to make the necessary field validation changes. +- See dgi_actions_handle_constraints README for more information. ## Troubleshooting/Issues diff --git a/modules/dgi_actions_handle/config/schema/dgi_actions_handle.schema.yml b/modules/dgi_actions_handle/config/schema/dgi_actions_handle.schema.yml index ad91744..f962843 100644 --- a/modules/dgi_actions_handle/config/schema/dgi_actions_handle.schema.yml +++ b/modules/dgi_actions_handle/config/schema/dgi_actions_handle.schema.yml @@ -19,6 +19,10 @@ dgi_actions.service_data_type.handle: label: 'Prefix' type: string description: 'Handle prefix as specified from Handle.net.' + suffix_field: + label: 'Suffix' + type: string + description: 'Field to use as handle suffix.' action.configuration.dgi_actions_delete_handle: type: dgi_actions_action_delete_base diff --git a/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/README.md b/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/README.md new file mode 100644 index 0000000..114bc6e --- /dev/null +++ b/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/README.md @@ -0,0 +1,63 @@ +# DGI Actions Handle Constraints Module + +## Introduction + +This module is part of the DGI Actions Handle package. It provides functionality to add unique constraints to certain fields of an entity and handles the preservation of these fields during entity save operations. + +## Requirements + +This module requires the following modules/libraries: + +* [DGI Actions Handle](https://github.com/discoverygarden/dgi_actions_handle) + +## Installation + +Install as usual, see +[this](https://www.drupal.org/docs/extending-drupal/installing-modules) for +further information. + +## Features + +1. **Unique Constraints**: The module can add a unique constraint to specified fields of an entity. + +2. **Entity Base Field Alteration**: The module implements the `hook_entity_base_field_info_alter` hook to add unique constraints to the fields of an entity if the entity type is an instance of `ContentEntityTypeInterface`. + +3. **Entity Bundle Field Alteration**: The module implements the `hook_entity_bundle_field_info_alter` hook to add unique constraints to the fields of an entity bundle if the entity type is an instance of `ContentEntityTypeInterface`. + +4. **Entity Pre-save**: The module implements the `hook_entity_presave` hook to revert the value of the suffix field if it is changed. This is needed for spreadsheet ingest. + +5. **Form Alteration**: The module implements the `hook_form_alter` hook to disable the suffix/identifier fields that are not allowed to be changed. + +## Usage + +This module is used as part of the DGI Actions Handle package. +It is used to ensure that the fields that are used as suffixes for the handle are unique and required. The module also ensures that the suffix field is not changed during entity save operations. +Once the configuration is set up, the module will handle the rest. + +## Configuration + +The module uses the `dgi_actions_handle_constraints.settings` configuration, which should be set up with the appropriate constraint settings. +For each field that is used as a suffix or identifier, a new value should be added to the constraint_settings array. An example configuration file which +uses the field_pid as suffix and field_handle as identifier is provided with the module. + +## Troubleshooting/Issues + +Having problems or solved a problem? Contact +[discoverygarden](http://support.discoverygarden.ca). + +## Maintainers/Sponsors + +Current maintainers: + +* [discoverygarden](http://www.discoverygarden.ca) + +## Development + +If you would like to contribute to this module, please check out the helpful +[Documentation](https://github.com/Islandora/islandora/wiki#wiki-documentation-for-developers), +[Developers](http://islandora.ca/developers) section on Islandora.ca and +contact [discoverygarden](http://support.discoverygarden.ca). + +## License + +[GPLv3](http://www.gnu.org/licenses/gpl-3.0.txt) diff --git a/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/config/install/dgi_actions_handle_constraints.settings.yml b/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/config/install/dgi_actions_handle_constraints.settings.yml new file mode 100644 index 0000000..a89cfe3 --- /dev/null +++ b/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/config/install/dgi_actions_handle_constraints.settings.yml @@ -0,0 +1,9 @@ +constraint_settings: + - entity_type: node + entity_bundle: islandora_object + field_name: field_pid + field_usage: suffix + - entity_type: node + entity_bundle: islandora_object + field_name: field_handle + field_usage: identifier diff --git a/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/config/schema/dgi_actions_handle_constraints.schema.yml b/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/config/schema/dgi_actions_handle_constraints.schema.yml new file mode 100644 index 0000000..0474aab --- /dev/null +++ b/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/config/schema/dgi_actions_handle_constraints.schema.yml @@ -0,0 +1,27 @@ +--- +dgi_action_handle_constraints.settings: + type: config_object + label: 'DGI Actions constraint settings' + mapping: + constraint_settings: + type: mapping + label: 'Constraint data' + mapping: + entity_type: + type: string + label: 'Entity type' + description: 'The entity type to add the constraint to.' + entity_bundle: + type: string + label: 'Entity bundle' + description: 'The entity bundle to add the constraint to.' + field_name: + type: string + label: 'Field name' + description: 'The field name to add the constraint to.' + field_usage: + type: string + label: 'Field usage' + description: 'Either identifier or suffix.' + constraints: + Regex: '/^(identifier|suffix)$/' diff --git a/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/dgi_actions_handle_constraints.info.yml b/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/dgi_actions_handle_constraints.info.yml new file mode 100644 index 0000000..69eb564 --- /dev/null +++ b/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/dgi_actions_handle_constraints.info.yml @@ -0,0 +1,7 @@ +name: 'DGI Actions Handle Constraints' +description: "Add constraints to handle fields." +type: module +package: DGI Actions Handle Constraints +core_version_requirement: ^9 || ^10 +dependencies: + - dgi_actions_handle:dgi_actions_handle diff --git a/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/dgi_actions_handle_constraints.module b/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/dgi_actions_handle_constraints.module new file mode 100644 index 0000000..61c435f --- /dev/null +++ b/modules/dgi_actions_handle/modules/dgi_actions_handle_constraints/dgi_actions_handle_constraints.module @@ -0,0 +1,137 @@ +get('constraint_settings'); + // Constraint settings is an array of values for each field that should be modified. + foreach ($constraint_settings as $constraintSetting) { + if (!isset($fields[$constraintSetting['field_name']]) || $constraintSetting['entity_type'] !== $entity_type->id()) { + continue; + } + // Both identifier and suffix fields should be unique. + $fields[$constraintSetting['field_name']]->addConstraint('UniqueField'); + + // If the field is a suffix field, set it as required. + if ($constraintSetting['field_usage'] === 'suffix') { + $fields[$constraintSetting['field_name']]->setRequired(TRUE); + } + } + } + catch (\Exception $e) { + Drupal::logger('dgi_actions_handle_constraints')->error($e->getMessage()); + } +} + +/** + * Implements hook_entity_base_field_info_alter(). + */ +function dgi_actions_handle_constraints_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type): void { + if ($entity_type instanceof ContentEntityTypeInterface) { + _dgi_actions_handle_constraints_suffix_validation_add_constraint($fields, $entity_type); + } +} + +/** + * Implements hook_entity_bundle_field_info_alter(). + */ +function dgi_actions_handle_constraints_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type): void { + if ($entity_type instanceof ContentEntityTypeInterface) { + _dgi_actions_handle_constraints_suffix_validation_add_constraint($fields, $entity_type); + } +} + +/** + * Implements hook_entity_presave(). + * + * Reverts the value of the suffix field if it is changed. + * This is needed for spreadsheet ingest. + */ +function dgi_actions_handle_constraints_entity_presave(EntityInterface $entity): void { + try { + $config = \Drupal::config('dgi_actions_handle_constraints.settings'); + $constraint_settings = $config->get('constraint_settings'); + // Constraint settings is an array of values for each field that should be modified. + foreach ($constraint_settings as $constraintSetting) { + // Only revert the suffix field if it is changed. + if ($constraintSetting['field_usage'] !== 'suffix' + || $entity->getEntityTypeId() !== $constraintSetting['entity_type'] + || $entity->isNew() || !$entity->hasField($constraintSetting['field_name'])) { + continue; + } + $original_entity = Drupal::entityTypeManager() + ->getStorage($entity->getEntityTypeId()) + ->loadUnchanged($entity->id()); + if (!$original_entity) { + return; + } + if ($entity->{$constraintSetting['field_name']}->value !== $original_entity->{$constraintSetting['field_name']}->value) { + $entity->{$constraintSetting['field_name']}->value = $original_entity->{$constraintSetting['field_name']}->value; + Drupal::messenger()->addWarning('The suffix field cannot be changed.'); + } + } + } + catch (\Exception $e) { + Drupal::logger('dgi_actions_handle_constraints')->error($e->getMessage()); + } +} + +/** + * Implements hook_form_alter(). + * + * Disables the suffix/identifier fields that are not allowed to be changed. + */ +function dgi_actions_handle_constraints_form_alter(array &$form, FormStateInterface $form_state, $form_id): void { + try { + $config = \Drupal::config('dgi_actions_handle_constraints.settings'); + $constraint_settings = $config->get('constraint_settings'); + // Constraint settings is an array of values for each field that should be modified. + foreach ($constraint_settings as $constraintSetting) { + // If the form id is not of the content form or the content edit form, return. + if (!($form_id === $constraintSetting['entity_type'] . '_' . $constraintSetting['entity_bundle'] . '_form' + || $form_id === $constraintSetting['entity_type'] . '_' . $constraintSetting['entity_bundle'] . '_edit_form')) { + return; + } + $entity = $form_state->getFormObject()->getEntity(); + if (!$entity instanceof ContentEntityInterface || !isset($form[$constraintSetting['field_name']])) { + continue; + } + // For the suffix field, set the description and access. + if ($constraintSetting['field_usage'] === 'suffix') { + $form[$constraintSetting['field_name']]['widget'][0]['value']['#description'] = t('This field is used as the handle suffix. + The suffix field, once set, cannot be changed.'); + $form[$constraintSetting['field_name']]['#access'] = TRUE; + + // Disable the suffix field if it has a value. + if ($entity->{$constraintSetting['field_name']}->value && !$entity->isNew()) { + $form[$constraintSetting['field_name']]['#disabled'] = TRUE; + } + } + + // If the field is an identifier field and the entity is not new, disable it. + if ($constraintSetting['field_usage'] === 'identifier' && !$entity->isNew()) { + $form[$constraintSetting['field_name']]['#disabled'] = TRUE; + } + + } + } + catch (\Exception $e) { + Drupal::logger('dgi_actions_handle_constraints')->error($e->getMessage()); + } +} diff --git a/modules/dgi_actions_handle/src/Plugin/ServiceDataType/Handle.php b/modules/dgi_actions_handle/src/Plugin/ServiceDataType/Handle.php index 89d5def..ab50bf0 100644 --- a/modules/dgi_actions_handle/src/Plugin/ServiceDataType/Handle.php +++ b/modules/dgi_actions_handle/src/Plugin/ServiceDataType/Handle.php @@ -40,6 +40,7 @@ public function defaultConfiguration(): array { 'username' => NULL, 'password' => NULL, 'prefix' => NULL, + 'suffix_field' => NULL, ]; } @@ -76,6 +77,10 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta '#default_value' => $this->configuration['prefix'], '#required' => TRUE, ]; + $form['suffix_field'] = [ + '#type' => 'hidden', + '#default_value' => $this->configuration['suffix_field'], + ]; return $form; } @@ -96,6 +101,7 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s $this->configuration['host'] = $form_state->getValue('host'); $this->configuration['prefix'] = $form_state->getValue('prefix'); $this->configuration['username'] = $form_state->getValue('username'); + $this->configuration['suffix_field'] = $form_state->getValue('suffix_field'); $this->configuration['password'] = !empty($form_state->getValue('password')) ? $form_state->getValue('password') : $this->configuration['password']; // Handle the scenario where the user did not modify the password as this // gets stored on the entity. diff --git a/modules/dgi_actions_handle/src/Utility/HandleTrait.php b/modules/dgi_actions_handle/src/Utility/HandleTrait.php index 7ed92d6..b20d3a8 100644 --- a/modules/dgi_actions_handle/src/Utility/HandleTrait.php +++ b/modules/dgi_actions_handle/src/Utility/HandleTrait.php @@ -62,7 +62,14 @@ public function getPrefix(): string { * Gets the suffix for the entity. */ public function getSuffix(): ?string { - // XXX: Should this be something different? + $suffix_field = $this->getIdentifier()->getServiceData()->getData()['suffix_field']; + + // If a field is configured, use that. + if ($suffix_field && $this->getEntity()->hasField($suffix_field)) { + return $this->getEntity()->get($suffix_field)->value; + } + + // Use uuid by default. return $this->getEntity()->uuid(); } diff --git a/src/Form/DataProfileDeleteForm.php b/src/Form/DataProfileDeleteForm.php index 2bc2a18..6d109c3 100644 --- a/src/Form/DataProfileDeleteForm.php +++ b/src/Form/DataProfileDeleteForm.php @@ -4,8 +4,8 @@ use Drupal\Core\Entity\EntityConfirmFormBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Url; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Url; /** * Builds the form to delete Data Profile setting entities. diff --git a/src/Plugin/Action/IdentifierAction.php b/src/Plugin/Action/IdentifierAction.php index eafa4be..3f656a6 100644 --- a/src/Plugin/Action/IdentifierAction.php +++ b/src/Plugin/Action/IdentifierAction.php @@ -2,16 +2,16 @@ namespace Drupal\dgi_actions\Plugin\Action; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\dgi_actions\Entity\IdentifierInterface; -use Drupal\dgi_actions\Utility\IdentifierUtils; use Drupal\Core\Action\ConfigurableActionBase; -use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Psr\Log\LoggerInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\dgi_actions\Entity\IdentifierInterface; +use Drupal\dgi_actions\Utility\IdentifierUtils; +use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Base class for Identifier Actions. diff --git a/src/Plugin/Action/MintIdentifier.php b/src/Plugin/Action/MintIdentifier.php index 8b36cca..02b66d6 100644 --- a/src/Plugin/Action/MintIdentifier.php +++ b/src/Plugin/Action/MintIdentifier.php @@ -2,8 +2,8 @@ namespace Drupal\dgi_actions\Plugin\Action; -use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException; +use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\EntityReferenceFieldItemList; use Drupal\Core\Form\FormStateInterface; diff --git a/src/Plugin/Condition/EntityHasIdentifier.php b/src/Plugin/Condition/EntityHasIdentifier.php index 6fa6873..8652220 100644 --- a/src/Plugin/Condition/EntityHasIdentifier.php +++ b/src/Plugin/Condition/EntityHasIdentifier.php @@ -6,12 +6,12 @@ use Drupal\Core\Condition\ConditionPluginBase; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\dgi_actions\Utility\IdentifierUtils; use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Condition to check an Entity for an existing persistent identifier. diff --git a/src/Plugin/DataProfileManager.php b/src/Plugin/DataProfileManager.php index 38bad1b..4ffd01e 100644 --- a/src/Plugin/DataProfileManager.php +++ b/src/Plugin/DataProfileManager.php @@ -2,9 +2,9 @@ namespace Drupal\dgi_actions\Plugin; -use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\DefaultPluginManager; /** * Provides the Data profile plugin manager. diff --git a/src/Plugin/ServiceDataTypeManager.php b/src/Plugin/ServiceDataTypeManager.php index a1c1950..d875944 100644 --- a/src/Plugin/ServiceDataTypeManager.php +++ b/src/Plugin/ServiceDataTypeManager.php @@ -2,9 +2,9 @@ namespace Drupal\dgi_actions\Plugin; -use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\DefaultPluginManager; /** * Provides the Service data type plugin manager.