You are here

class ProcessEntities in Field Encryption 3.0.x

Service class to process entities and fields for encryption.

Hierarchy

Expanded class hierarchy of ProcessEntities

11 files declare their use of ProcessEntities
BaseFieldTest.php in tests/src/Functional/BaseFieldTest.php
ConfigurableFieldTest.php in tests/src/Functional/ConfigurableFieldTest.php
EncryptingExistingDataTest.php in tests/src/Functional/EncryptingExistingDataTest.php
EncryptUserTest.php in tests/src/Functional/EncryptUserTest.php
EntityTypeForm.php in src/Form/EntityTypeForm.php

... See full list

1 string reference to 'ProcessEntities'
field_encrypt.services.yml in ./field_encrypt.services.yml
field_encrypt.services.yml
1 service uses ProcessEntities
field_encrypt.process_entities in ./field_encrypt.services.yml
Drupal\field_encrypt\ProcessEntities

File

src/ProcessEntities.php, line 12

Namespace

Drupal\field_encrypt
View source
class ProcessEntities {

  /**
   * The name of the field that stores encrypted data.
   */
  const ENCRYPTED_FIELD_STORAGE_NAME = 'encrypted_field_storage';

  /**
   * This value is used in place of the real value in the database.
   */
  const ENCRYPTED_VALUE = '🔒';

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * Constructs a ProcessEntities object.
   *
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   */
  public function __construct(ModuleHandlerInterface $module_handler) {
    $this->moduleHandler = $module_handler;
  }

  /**
   * Encrypts an entity's encrypted fields.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity to encrypt.
   *
   * @see field_encrypt_entity_presave()
   * @see field_encrypt_entity_update()
   * @see field_encrypt_entity_insert()
   * @see field_encrypt_module_implements_alter()
   */
  public function encryptEntity(ContentEntityInterface $entity) {

    // Make sure there is a base field to store encrypted data.
    if (!$entity
      ->hasField(static::ENCRYPTED_FIELD_STORAGE_NAME)) {
      return;
    }

    // Check if encryption is prevent due to implementations of
    // hook_field_encrypt_allow_encryption().
    foreach ($this->moduleHandler
      ->getImplementations('field_encrypt_allow_encryption') as $module) {
      $result = $this->moduleHandler
        ->invoke($module, 'field_encrypt_allow_encryption', [
        $entity,
      ]);

      // If the implementation returns a FALSE boolean value, disable
      // encryption.
      if ($result === FALSE) {
        return;
      }
    }

    // Process all language variants of the entity.
    $languages = $entity
      ->getTranslationLanguages();
    foreach ($languages as $language) {
      $translated_entity = $entity
        ->getTranslation($language
        ->getId());
      $field = $translated_entity
        ->get(static::ENCRYPTED_FIELD_STORAGE_NAME);

      // Before encrypting the entity ensure the encrypted field storage is
      // empty so that any changes to encryption settings are processed as
      // expected.
      if (!$field
        ->isEmpty()) {
        $field
          ->removeItem(0);
        $field
          ->appendItem();
      }
      foreach ($this
        ->getEncryptedFields($translated_entity) as $field) {
        $this
          ->encryptField($translated_entity, $field);
      }

      // The entity storage handler has clever logic to ensure that configurable
      // fields are only saved if necessary. If entity->original is set we need
      // to ensure the field values are the values in the database and not the
      // unencrypted values so that they are saved if necessary. This is
      // particularly important when a previously encrypted field is set to be
      // unencrypted.
      // @see \Drupal\Core\Entity\Sql\SqlContentEntityStorage::saveToDedicatedTables()
      // @see \Drupal\Core\Entity\ContentEntityStorageBase::hasFieldValueChanged()
      if (isset($translated_entity->original) && $translated_entity->original instanceof ContentEntityInterface) {
        if ($translated_entity->original
          ->hasTranslation($language
          ->getId())) {
          $translated_original = $translated_entity->original
            ->getTranslation($language
            ->getId());
          $this
            ->setEncryptedFieldValues($translated_original, 'getUnencryptedPlaceholderValue');
        }
      }

      // All the encrypted fields have now being processed and their values
      // moved to encrypted field storage. It's time to encrypt that field.
      $translated_entity
        ->get(static::ENCRYPTED_FIELD_STORAGE_NAME)[0]
        ->encrypt();
    }
  }

  /**
   * Decrypts an entity's encrypted fields.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity to decrypt.
   *
   * @see field_encrypt_entity_storage_load()
   */
  public function decryptEntity(ContentEntityInterface $entity) {

    // Make sure there is a base field to store encrypted data.
    if (!$entity
      ->hasField(static::ENCRYPTED_FIELD_STORAGE_NAME)) {
      return;
    }

    // Process all language variants of the entity.
    $languages = $entity
      ->getTranslationLanguages();
    foreach ($languages as $language) {
      $translated_entity = $entity
        ->getTranslation($language
        ->getId());
      $this
        ->setEncryptedFieldValues($translated_entity);
    }
  }

  /**
   * Encrypts a field.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity being encrypted.
   * @param \Drupal\Core\Field\FieldItemListInterface $field
   *   The field to encrypt.
   */
  protected function encryptField(ContentEntityInterface $entity, FieldItemListInterface $field) {
    $definition = $field
      ->getFieldDefinition();
    $storage = $definition
      ->getFieldStorageDefinition();
    $field_value = $field
      ->getValue();

    // Get encryption settings from storage.
    if ($storage
      ->isBaseField()) {
      $properties = $storage
        ->getSetting('field_encrypt.properties') ?? [];
    }
    else {

      /** @var \Drupal\field\FieldStorageConfigInterface $storage */
      $properties = $storage
        ->getThirdPartySetting('field_encrypt', 'properties', []);
    }

    // Process the field with the given encryption provider.
    foreach ($field_value as $delta => &$value) {

      // Process each of the field properties that exist.
      foreach ($properties as $property_name) {
        if (isset($value[$property_name])) {
          $value[$property_name] = $this
            ->encryptFieldValue($entity, $field, $delta, $property_name, $value[$property_name]);
        }
      }
    }

    // Set the new value. Calling setValue() updates the entity too.
    $field
      ->setValue($field_value);
  }

  /**
   * Sets an entity's encrypted fields to a value.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity to set the values on.
   * @param string|null $method
   *   (optional) The method call to set the value. By default, this code sets
   *   the encrypted field to the decrypted value. If $method is set then it is
   *   called with the entity, the field and the property name.
   */
  protected function setEncryptedFieldValues(ContentEntityInterface $entity, string $method = NULL) {
    $storage = $entity
      ->get(static::ENCRYPTED_FIELD_STORAGE_NAME)->decrypted_value ?? [];
    foreach ($storage as $field_name => $decrypted_field) {
      if (!$entity
        ->hasField($field_name)) {
        continue;
      }
      $field = $entity
        ->get($field_name);
      $field_value = $field
        ->getValue();

      // Process each of the field properties that exist.
      foreach ($field_value as $delta => &$value) {
        if (!isset($storage[$field_name][$delta])) {
          continue;
        }

        // Process each of the field properties that exist.
        foreach ($decrypted_field[$delta] as $property_name => $decrypted_value) {
          if ($method) {

            // @see \Drupal\field_encrypt\ProcessEntities::getUnencryptedPlaceholderValue()
            $value[$property_name] = $this
              ->{$method}($entity, $field, $property_name);
          }
          else {
            $value[$property_name] = $decrypted_value;
          }
        }
      }

      // Set the new value. Calling setValue() updates the entity too.
      $field
        ->setValue($field_value);
    }
  }

  /**
   * Gets the encrypted fields from the entity.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity with encrypted fields.
   *
   * @return iterable
   *   An iterator over the fields which are configured to be encrypted.
   */
  protected function getEncryptedFields(ContentEntityInterface $entity) {
    foreach ($entity
      ->getFields() as $field) {
      $storage = $field
        ->getFieldDefinition()
        ->getFieldStorageDefinition();
      $is_base_field = $storage
        ->isBaseField();

      // Check if the field is encrypted.
      if ($is_base_field && $storage
        ->getSetting('field_encrypt.encrypt') || !$is_base_field && $storage
        ->getThirdPartySetting('field_encrypt', 'encrypt', FALSE)) {
        (yield $field);
      }
    }
  }

  /**
   * Moves the unencrypted value to the encrypted field storage.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity to process.
   * @param \Drupal\Core\Field\FieldItemListInterface $field
   *   The field to process.
   * @param int $delta
   *   The field delta.
   * @param string $property_name
   *   The name of the property.
   * @param mixed $value
   *   The value to decrypt.
   *
   * @return mixed
   *   The encrypted field database value.
   */
  protected function encryptFieldValue(ContentEntityInterface $entity, FieldItemListInterface $field, int $delta, string $property_name, $value = '') {

    // Do not modify empty strings.
    if ($value === '') {
      return '';
    }
    $storage = $entity
      ->get(static::ENCRYPTED_FIELD_STORAGE_NAME)->decrypted_value ?? [];

    // Return value to store for unencrypted property.
    // We can't set this to NULL, because then the field values are not
    // saved, so we can't replace them with their unencrypted value on load.
    $placeholder_value = $this
      ->getUnencryptedPlaceholderValue($entity, $field, $property_name);
    if ($placeholder_value !== $value) {
      $storage[$field
        ->getName()][$delta][$property_name] = $value;
      $entity
        ->get(static::ENCRYPTED_FIELD_STORAGE_NAME)->decrypted_value = $storage;
      return $placeholder_value;
    }

    // If not allowed, but we still have an encrypted value remove it.
    if (isset($storage[$field
      ->getName()][$delta][$property_name])) {
      unset($storage[$field
        ->getName()][$delta][$property_name]);
      $entity
        ->get(static::ENCRYPTED_FIELD_STORAGE_NAME)->decrypted_value = $storage;
    }
    return $value;
  }

  /**
   * Render a placeholder value to be stored in the unencrypted field storage.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity to encrypt fields on.
   * @param \Drupal\Core\Field\FieldItemListInterface $field
   *   The field to encrypt.
   * @param string $property_name
   *   The property to encrypt.
   *
   * @return mixed
   *   The unencrypted placeholder value.
   */
  protected function getUnencryptedPlaceholderValue(ContentEntityInterface $entity, FieldItemListInterface $field, string $property_name) {
    $unencrypted_storage_value = NULL;
    $property_definitions = $field
      ->getFieldDefinition()
      ->getFieldStorageDefinition()
      ->getPropertyDefinitions();
    $data_type = $property_definitions[$property_name]
      ->getDataType();
    switch ($data_type) {
      case "string":
      case "email":
      case "datetime_iso8601":
      case "duration_iso8601":
      case "uri":
      case "filter_format":

        // Decimal fields are string data type, but get stored as number.
        if ($field
          ->getFieldDefinition()
          ->getType() == "decimal") {
          $unencrypted_storage_value = 0;
        }
        else {
          $unencrypted_storage_value = static::ENCRYPTED_VALUE;
        }
        break;
      case "integer":
      case "boolean":
      case "float":
        $unencrypted_storage_value = 0;
        break;
    }

    // Allow field storages to override the placeholders.
    $field_storage = $field
      ->getFieldDefinition()
      ->getFieldStorageDefinition();
    if ($field_storage
      ->isBaseField()) {
      $placeholder_overrides = $field_storage
        ->getSetting('field_encrypt.placeholders') ?? [];
    }
    else {
      $placeholder_overrides = $field_storage
        ->getThirdPartySetting('field_encrypt', 'placeholders') ?? [];
    }
    return $placeholder_overrides[$property_name] ?? $unencrypted_storage_value;
  }

  /**
   * Sets an entity's encrypted field's cache tags appropriately.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity being viewed.
   * @param array $build
   *   A renderable array representing the entity content.
   *
   * @see field_encrypt_entity_view()
   */
  public function entitySetCacheTags(ContentEntityInterface $entity, array &$build) {
    foreach ($this
      ->getEncryptedFields($entity) as $field) {
      $build[$field
        ->getName()]['#cache']['max-age'] = 0;
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ProcessEntities::$moduleHandler protected property The module handler.
ProcessEntities::decryptEntity public function Decrypts an entity's encrypted fields.
ProcessEntities::ENCRYPTED_FIELD_STORAGE_NAME constant The name of the field that stores encrypted data.
ProcessEntities::ENCRYPTED_VALUE constant This value is used in place of the real value in the database.
ProcessEntities::encryptEntity public function Encrypts an entity's encrypted fields.
ProcessEntities::encryptField protected function Encrypts a field.
ProcessEntities::encryptFieldValue protected function Moves the unencrypted value to the encrypted field storage.
ProcessEntities::entitySetCacheTags public function Sets an entity's encrypted field's cache tags appropriately.
ProcessEntities::getEncryptedFields protected function Gets the encrypted fields from the entity.
ProcessEntities::getUnencryptedPlaceholderValue protected function Render a placeholder value to be stored in the unencrypted field storage.
ProcessEntities::setEncryptedFieldValues protected function Sets an entity's encrypted fields to a value.
ProcessEntities::__construct public function Constructs a ProcessEntities object.