You are here

ProcessEntities.php in Field Encryption 3.0.x

File

src/ProcessEntities.php
View source
<?php

namespace Drupal\field_encrypt;

use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Entity\ContentEntityInterface;

/**
 * Service class to process entities and fields for encryption.
 */
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;
    }
  }

}

Classes

Namesort descending Description
ProcessEntities Service class to process entities and fields for encryption.